Merge remote-tracking branch 'origin/main' into ajf-snippet-fix

HactarCE and Cole Miller created

Co-authored-by: Cole Miller <cole@zed.dev>

Change summary

.cargo/ci-config.toml                                                                       |    6 
.config/hakari.toml                                                                         |   42 
.config/nextest.toml                                                                        |   14 
.github/ISSUE_TEMPLATE/06_bug_windows_beta.yml                                              |   35 
.github/ISSUE_TEMPLATE/11_crash_report.yml                                                  |    3 
.github/actions/run_tests/action.yml                                                        |    5 
.github/workflows/ci.yml                                                                    |   40 
.gitignore                                                                                  |    1 
.zed/settings.json                                                                          |    2 
Cargo.lock                                                                                  |  462 
Cargo.toml                                                                                  |   52 
README.md                                                                                   |    2 
assets/keymaps/default-linux.json                                                           |   35 
assets/keymaps/default-macos.json                                                           |   36 
assets/keymaps/default-windows.json                                                         |   36 
assets/keymaps/linux/cursor.json                                                            |    4 
assets/keymaps/linux/emacs.json                                                             |   46 
assets/keymaps/macos/cursor.json                                                            |    4 
assets/keymaps/macos/emacs.json                                                             |   42 
assets/keymaps/vim.json                                                                     |  107 
assets/settings/default.json                                                                |   36 
assets/themes/gruvbox/gruvbox.json                                                          |   30 
clippy.toml                                                                                 |    2 
crates/acp_thread/Cargo.toml                                                                |    1 
crates/acp_thread/src/acp_thread.rs                                                         |   75 
crates/acp_thread/src/diff.rs                                                               |   10 
crates/acp_thread/src/terminal.rs                                                           |   74 
crates/acp_tools/Cargo.toml                                                                 |    1 
crates/acp_tools/src/acp_tools.rs                                                           |    9 
crates/action_log/Cargo.toml                                                                |    1 
crates/activity_indicator/Cargo.toml                                                        |    1 
crates/activity_indicator/src/activity_indicator.rs                                         |    9 
crates/agent/Cargo.toml                                                                     |   77 
crates/agent/src/agent.rs                                                                   | 1648 
crates/agent/src/agent_profile.rs                                                           |  341 
crates/agent/src/context_server_tool.rs                                                     |  140 
crates/agent/src/db.rs                                                                      |  124 
crates/agent/src/edit_agent.rs                                                              |    0 
crates/agent/src/edit_agent/create_file_parser.rs                                           |    0 
crates/agent/src/edit_agent/edit_parser.rs                                                  |    0 
crates/agent/src/edit_agent/evals.rs                                                        |  109 
crates/agent/src/edit_agent/evals/fixtures/add_overwrite_test/before.rs                     |    0 
crates/agent/src/edit_agent/evals/fixtures/delete_run_git_blame/after.rs                    |    0 
crates/agent/src/edit_agent/evals/fixtures/delete_run_git_blame/before.rs                   |    0 
crates/agent/src/edit_agent/evals/fixtures/disable_cursor_blinking/before.rs                |    0 
crates/agent/src/edit_agent/evals/fixtures/disable_cursor_blinking/possible-01.diff         |    0 
crates/agent/src/edit_agent/evals/fixtures/disable_cursor_blinking/possible-02.diff         |    0 
crates/agent/src/edit_agent/evals/fixtures/disable_cursor_blinking/possible-03.diff         |    0 
crates/agent/src/edit_agent/evals/fixtures/disable_cursor_blinking/possible-04.diff         |    0 
crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/before.rs          |    0 
crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-01.diff   |    0 
crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-02.diff   |    0 
crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-03.diff   |    0 
crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-04.diff   |    0 
crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-05.diff   |    0 
crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-06.diff   |    0 
crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-07.diff   |    0 
crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-08.diff   |    0 
crates/agent/src/edit_agent/evals/fixtures/from_pixels_constructor/before.rs                |    0 
crates/agent/src/edit_agent/evals/fixtures/translate_doc_comments/before.rs                 |    0 
crates/agent/src/edit_agent/evals/fixtures/use_wasi_sdk_in_compile_parser_to_wasm/before.rs |    0 
crates/agent/src/edit_agent/evals/fixtures/zode/prompt.md                                   |    0 
crates/agent/src/edit_agent/evals/fixtures/zode/react.py                                    |    0 
crates/agent/src/edit_agent/evals/fixtures/zode/react_test.py                               |    0 
crates/agent/src/edit_agent/streaming_fuzzy_matcher.rs                                      |   21 
crates/agent/src/history_store.rs                                                           |  130 
crates/agent/src/legacy_thread.rs                                                           |  402 
crates/agent/src/native_agent_server.rs                                                     |    7 
crates/agent/src/outline.rs                                                                 |  158 
crates/agent/src/prompts/stale_files_prompt_header.txt                                      |    3 
crates/agent/src/templates.rs                                                               |    0 
crates/agent/src/templates/create_file_prompt.hbs                                           |    0 
crates/agent/src/templates/diff_judge.hbs                                                   |    0 
crates/agent/src/templates/edit_file_prompt_diff_fenced.hbs                                 |    0 
crates/agent/src/templates/edit_file_prompt_xml.hbs                                         |    0 
crates/agent/src/templates/system_prompt.hbs                                                |    0 
crates/agent/src/tests/mod.rs                                                               |   27 
crates/agent/src/tests/test_tools.rs                                                        |    0 
crates/agent/src/thread.rs                                                                  | 3417 
crates/agent/src/thread_store.rs                                                            | 1287 
crates/agent/src/tool_schema.rs                                                             |   43 
crates/agent/src/tool_use.rs                                                                |  575 
crates/agent/src/tools.rs                                                                   |   94 
crates/agent/src/tools/context_server_registry.rs                                           |   13 
crates/agent/src/tools/copy_path_tool.rs                                                    |    0 
crates/agent/src/tools/create_directory_tool.rs                                             |    0 
crates/agent/src/tools/delete_path_tool.rs                                                  |    0 
crates/agent/src/tools/diagnostics_tool.rs                                                  |    0 
crates/agent/src/tools/edit_file_tool.rs                                                    |   30 
crates/agent/src/tools/fetch_tool.rs                                                        |    0 
crates/agent/src/tools/find_path_tool.rs                                                    |    0 
crates/agent/src/tools/grep_tool.rs                                                         |    0 
crates/agent/src/tools/list_directory_tool.rs                                               |    0 
crates/agent/src/tools/move_path_tool.rs                                                    |    0 
crates/agent/src/tools/now_tool.rs                                                          |    0 
crates/agent/src/tools/open_tool.rs                                                         |    0 
crates/agent/src/tools/read_file_tool.rs                                                    |    3 
crates/agent/src/tools/terminal_tool.rs                                                     |    0 
crates/agent/src/tools/thinking_tool.rs                                                     |    0 
crates/agent/src/tools/web_search_tool.rs                                                   |    2 
crates/agent2/Cargo.toml                                                                    |  102 
crates/agent2/src/agent.rs                                                                  | 1588 
crates/agent2/src/agent2.rs                                                                 |   19 
crates/agent2/src/thread.rs                                                                 | 2663 
crates/agent2/src/tool_schema.rs                                                            |   43 
crates/agent2/src/tools.rs                                                                  |   60 
crates/agent_servers/Cargo.toml                                                             |    1 
crates/agent_servers/src/acp.rs                                                             |   76 
crates/agent_settings/Cargo.toml                                                            |    1 
crates/agent_settings/src/agent_settings.rs                                                 |   19 
crates/agent_settings/src/prompts/summarize_thread_detailed_prompt.txt                      |    0 
crates/agent_settings/src/prompts/summarize_thread_prompt.txt                               |    0 
crates/agent_ui/Cargo.toml                                                                  |   10 
crates/agent_ui/src/acp/completion_provider.rs                                              |   43 
crates/agent_ui/src/acp/entry_view_state.rs                                                 |   10 
crates/agent_ui/src/acp/message_editor.rs                                                   |   53 
crates/agent_ui/src/acp/mode_selector.rs                                                    |    8 
crates/agent_ui/src/acp/model_selector_popover.rs                                           |   10 
crates/agent_ui/src/acp/thread_history.rs                                                   |   25 
crates/agent_ui/src/acp/thread_view.rs                                                      |  108 
crates/agent_ui/src/agent_configuration.rs                                                  |   58 
crates/agent_ui/src/agent_configuration/add_llm_provider_modal.rs                           |   31 
crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs                   |   11 
crates/agent_ui/src/agent_configuration/configure_context_server_tools_modal.rs             |   37 
crates/agent_ui/src/agent_configuration/manage_profiles_modal.rs                            |   58 
crates/agent_ui/src/agent_configuration/tool_picker.rs                                      |  100 
crates/agent_ui/src/agent_diff.rs                                                           |  107 
crates/agent_ui/src/agent_model_selector.rs                                                 |   10 
crates/agent_ui/src/agent_panel.rs                                                          |  356 
crates/agent_ui/src/agent_ui.rs                                                             |   21 
crates/agent_ui/src/buffer_codegen.rs                                                       |   13 
crates/agent_ui/src/context.rs                                                              |  166 
crates/agent_ui/src/context_picker.rs                                                       |  157 
crates/agent_ui/src/context_picker/completion_provider.rs                                   |  126 
crates/agent_ui/src/context_picker/fetch_context_picker.rs                                  |    3 
crates/agent_ui/src/context_picker/file_context_picker.rs                                   |    6 
crates/agent_ui/src/context_picker/rules_context_picker.rs                                  |   24 
crates/agent_ui/src/context_picker/symbol_context_picker.rs                                 |    6 
crates/agent_ui/src/context_picker/thread_context_picker.rs                                 |  219 
crates/agent_ui/src/context_store.rs                                                        |  112 
crates/agent_ui/src/context_strip.rs                                                        |   50 
crates/agent_ui/src/inline_assistant.rs                                                     |   60 
crates/agent_ui/src/inline_prompt_editor.rs                                                 |   42 
crates/agent_ui/src/message_editor.rs                                                       |   38 
crates/agent_ui/src/profile_selector.rs                                                     |    3 
crates/agent_ui/src/slash_command_picker.rs                                                 |    4 
crates/agent_ui/src/terminal_inline_assistant.rs                                            |   20 
crates/agent_ui/src/text_thread_editor.rs                                                   |  374 
crates/agent_ui/src/ui/burn_mode_tooltip.rs                                                 |    7 
crates/agent_ui/src/ui/context_pill.rs                                                      |   20 
crates/ai_onboarding/Cargo.toml                                                             |    1 
crates/ai_onboarding/src/ai_onboarding.rs                                                   |   87 
crates/anthropic/Cargo.toml                                                                 |    1 
crates/askpass/Cargo.toml                                                                   |    1 
crates/assets/Cargo.toml                                                                    |    1 
crates/assistant_context/LICENSE-GPL                                                        |    1 
crates/assistant_slash_command/Cargo.toml                                                   |    1 
crates/assistant_slash_commands/Cargo.toml                                                  |    1 
crates/assistant_slash_commands/src/selection_command.rs                                    |    2 
crates/assistant_text_thread/Cargo.toml                                                     |    5 
crates/assistant_text_thread/LICENSE-GPL                                                    |    0 
crates/assistant_text_thread/src/assistant_text_thread.rs                                   |   15 
crates/assistant_text_thread/src/assistant_text_thread_tests.rs                             |  430 
crates/assistant_text_thread/src/text_thread.rs                                             |  328 
crates/assistant_text_thread/src/text_thread_store.rs                                       |  343 
crates/assistant_tool/Cargo.toml                                                            |   50 
crates/assistant_tool/LICENSE-GPL                                                           |    1 
crates/assistant_tool/src/assistant_tool.rs                                                 |  269 
crates/assistant_tool/src/tool_registry.rs                                                  |   74 
crates/assistant_tool/src/tool_working_set.rs                                               |  415 
crates/assistant_tools/Cargo.toml                                                           |   92 
crates/assistant_tools/LICENSE-GPL                                                          |    1 
crates/assistant_tools/src/assistant_tools.rs                                               |  167 
crates/assistant_tools/src/copy_path_tool.rs                                                |  123 
crates/assistant_tools/src/copy_path_tool/description.md                                    |    6 
crates/assistant_tools/src/create_directory_tool.rs                                         |  100 
crates/assistant_tools/src/create_directory_tool/description.md                             |    3 
crates/assistant_tools/src/delete_path_tool.rs                                              |  144 
crates/assistant_tools/src/delete_path_tool/description.md                                  |    1 
crates/assistant_tools/src/diagnostics_tool.rs                                              |  171 
crates/assistant_tools/src/diagnostics_tool/description.md                                  |   21 
crates/assistant_tools/src/edit_file_tool.rs                                                | 2423 
crates/assistant_tools/src/edit_file_tool/description.md                                    |    8 
crates/assistant_tools/src/fetch_tool.rs                                                    |  178 
crates/assistant_tools/src/fetch_tool/description.md                                        |    1 
crates/assistant_tools/src/find_path_tool.rs                                                |  472 
crates/assistant_tools/src/find_path_tool/description.md                                    |    7 
crates/assistant_tools/src/grep_tool.rs                                                     | 1308 
crates/assistant_tools/src/grep_tool/description.md                                         |    9 
crates/assistant_tools/src/list_directory_tool.rs                                           |  869 
crates/assistant_tools/src/list_directory_tool/description.md                               |    1 
crates/assistant_tools/src/move_path_tool.rs                                                |  132 
crates/assistant_tools/src/move_path_tool/description.md                                    |    5 
crates/assistant_tools/src/now_tool.rs                                                      |   84 
crates/assistant_tools/src/open_tool.rs                                                     |  170 
crates/assistant_tools/src/open_tool/description.md                                         |    9 
crates/assistant_tools/src/project_notifications_tool.rs                                    |  360 
crates/assistant_tools/src/project_notifications_tool/description.md                        |    3 
crates/assistant_tools/src/project_notifications_tool/prompt_header.txt                     |    3 
crates/assistant_tools/src/read_file_tool.rs                                                | 1190 
crates/assistant_tools/src/read_file_tool/description.md                                    |    3 
crates/assistant_tools/src/schema.rs                                                        |   60 
crates/assistant_tools/src/templates.rs                                                     |   32 
crates/assistant_tools/src/terminal_tool.rs                                                 |  883 
crates/assistant_tools/src/terminal_tool/description.md                                     |   11 
crates/assistant_tools/src/thinking_tool.rs                                                 |   69 
crates/assistant_tools/src/thinking_tool/description.md                                     |    1 
crates/assistant_tools/src/ui.rs                                                            |    5 
crates/assistant_tools/src/ui/tool_call_card_header.rs                                      |  131 
crates/assistant_tools/src/ui/tool_output_preview.rs                                        |  115 
crates/assistant_tools/src/web_search_tool.rs                                               |  327 
crates/audio/Cargo.toml                                                                     |    3 
crates/audio/src/rodio_ext.rs                                                               |    2 
crates/auto_update/Cargo.toml                                                               |    1 
crates/auto_update_helper/Cargo.toml                                                        |    1 
crates/auto_update_ui/Cargo.toml                                                            |    1 
crates/aws_http_client/Cargo.toml                                                           |    1 
crates/bedrock/Cargo.toml                                                                   |    1 
crates/breadcrumbs/Cargo.toml                                                               |    1 
crates/breadcrumbs/src/breadcrumbs.rs                                                       |    4 
crates/buffer_diff/Cargo.toml                                                               |    1 
crates/buffer_diff/src/buffer_diff.rs                                                       |   40 
crates/call/Cargo.toml                                                                      |    1 
crates/channel/Cargo.toml                                                                   |    1 
crates/channel/src/channel_buffer.rs                                                        |   11 
crates/cli/Cargo.toml                                                                       |    1 
crates/cli/src/cli.rs                                                                       |    1 
crates/cli/src/main.rs                                                                      |    8 
crates/client/Cargo.toml                                                                    |    1 
crates/client/src/client.rs                                                                 |   25 
crates/client/src/proxy/socks_proxy.rs                                                      |    2 
crates/client/src/user.rs                                                                   |    2 
crates/clock/Cargo.toml                                                                     |    1 
crates/clock/src/clock.rs                                                                   |  175 
crates/cloud_api_client/Cargo.toml                                                          |    1 
crates/cloud_api_types/Cargo.toml                                                           |    1 
crates/cloud_llm_client/Cargo.toml                                                          |    1 
crates/cloud_zeta2_prompt/Cargo.toml                                                        |    1 
crates/codestral/Cargo.toml                                                                 |    1 
crates/collab/Cargo.toml                                                                    |   11 
crates/collab/src/db/queries/buffers.rs                                                     |   22 
crates/collab/src/db/queries/extensions.rs                                                  |    2 
crates/collab/src/db/queries/notifications.rs                                               |    4 
crates/collab/src/db/queries/projects.rs                                                    |   10 
crates/collab/src/db/tests.rs                                                               |    2 
crates/collab/src/db/tests/buffer_tests.rs                                                  |   44 
crates/collab/src/rpc.rs                                                                    |    1 
crates/collab/src/tests/editor_tests.rs                                                     |  173 
crates/collab/src/tests/following_tests.rs                                                  |  103 
crates/collab/src/tests/integration_tests.rs                                                |   54 
crates/collab/src/tests/test_server.rs                                                      |    2 
crates/collab_ui/Cargo.toml                                                                 |    1 
crates/collab_ui/src/channel_view.rs                                                        |   15 
crates/collab_ui/src/collab_panel.rs                                                        |    4 
crates/collab_ui/src/notification_panel.rs                                                  |    8 
crates/collections/Cargo.toml                                                               |    1 
crates/command_palette/Cargo.toml                                                           |    1 
crates/command_palette/src/command_palette.rs                                               |   11 
crates/command_palette_hooks/Cargo.toml                                                     |    1 
crates/component/Cargo.toml                                                                 |    1 
crates/context_server/Cargo.toml                                                            |    1 
crates/copilot/Cargo.toml                                                                   |    1 
crates/crashes/Cargo.toml                                                                   |    1 
crates/crashes/src/crashes.rs                                                               |   36 
crates/credentials_provider/Cargo.toml                                                      |    1 
crates/dap/Cargo.toml                                                                       |    1 
crates/dap/src/adapters.rs                                                                  |    4 
crates/dap_adapters/Cargo.toml                                                              |    1 
crates/dap_adapters/src/codelldb.rs                                                         |    9 
crates/dap_adapters/src/gdb.rs                                                              |    6 
crates/dap_adapters/src/go.rs                                                               |    3 
crates/dap_adapters/src/javascript.rs                                                       |   19 
crates/dap_adapters/src/python.rs                                                           |   18 
crates/db/Cargo.toml                                                                        |    1 
crates/debug_adapter_extension/Cargo.toml                                                   |    2 
crates/debug_adapter_extension/src/extension_dap_adapter.rs                                 |    3 
crates/debugger_tools/Cargo.toml                                                            |    1 
crates/debugger_ui/Cargo.toml                                                               |    1 
crates/debugger_ui/src/debugger_panel.rs                                                    |   33 
crates/debugger_ui/src/debugger_ui.rs                                                       |   11 
crates/debugger_ui/src/new_process_modal.rs                                                 |   97 
crates/debugger_ui/src/session/running.rs                                                   |    4 
crates/debugger_ui/src/session/running/breakpoint_list.rs                                   |   24 
crates/debugger_ui/src/session/running/console.rs                                           |   11 
crates/debugger_ui/src/session/running/stack_frame_list.rs                                  |    4 
crates/debugger_ui/src/session/running/variable_list.rs                                     |   10 
crates/debugger_ui/src/stack_trace_view.rs                                                  |    5 
crates/debugger_ui/src/tests/new_process_modal.rs                                           |    5 
crates/deepseek/Cargo.toml                                                                  |    1 
crates/denoise/Cargo.toml                                                                   |    1 
crates/diagnostics/Cargo.toml                                                               |    1 
crates/diagnostics/src/buffer_diagnostics.rs                                                |    6 
crates/diagnostics/src/diagnostics.rs                                                       |    6 
crates/diagnostics/src/diagnostics_tests.rs                                                 |    6 
crates/diagnostics/src/items.rs                                                             |   12 
crates/docs_preprocessor/Cargo.toml                                                         |    1 
crates/docs_preprocessor/src/main.rs                                                        |    9 
crates/edit_prediction/Cargo.toml                                                           |    1 
crates/edit_prediction_button/Cargo.toml                                                    |    1 
crates/edit_prediction_button/src/edit_prediction_button.rs                                 |   35 
crates/edit_prediction_context/Cargo.toml                                                   |    1 
crates/edit_prediction_context/src/edit_prediction_context.rs                               |   13 
crates/edit_prediction_context/src/syntax_index.rs                                          |    2 
crates/editor/Cargo.toml                                                                    |    2 
crates/editor/src/actions.rs                                                                |    2 
crates/editor/src/display_map.rs                                                            |   23 
crates/editor/src/display_map/block_map.rs                                                  |   86 
crates/editor/src/display_map/custom_highlights.rs                                          |   32 
crates/editor/src/display_map/fold_map.rs                                                   |   99 
crates/editor/src/display_map/inlay_map.rs                                                  |  220 
crates/editor/src/display_map/tab_map.rs                                                    |   89 
crates/editor/src/display_map/wrap_map.rs                                                   |   80 
crates/editor/src/editor.rs                                                                 |  523 
crates/editor/src/editor_settings.rs                                                        |  216 
crates/editor/src/editor_tests.rs                                                           |  316 
crates/editor/src/element.rs                                                                |  119 
crates/editor/src/git/blame.rs                                                              |    6 
crates/editor/src/hover_links.rs                                                            |  194 
crates/editor/src/hover_popover.rs                                                          |   23 
crates/editor/src/indent_guides.rs                                                          |    2 
crates/editor/src/inlays.rs                                                                 |  193 
crates/editor/src/inlays/inlay_hints.rs                                                     | 1900 
crates/editor/src/items.rs                                                                  |   48 
crates/editor/src/linked_editing_ranges.rs                                                  |    2 
crates/editor/src/lsp_colors.rs                                                             |   11 
crates/editor/src/mouse_context_menu.rs                                                     |    8 
crates/editor/src/movement.rs                                                               |    2 
crates/editor/src/proposed_changes_editor.rs                                                |   55 
crates/editor/src/scroll/actions.rs                                                         |   21 
crates/editor/src/scroll/autoscroll.rs                                                      |    4 
crates/editor/src/selections_collection.rs                                                  |  115 
crates/editor/src/signature_help.rs                                                         |   15 
crates/editor/src/tasks.rs                                                                  |    2 
crates/editor/src/test.rs                                                                   |    2 
crates/editor/src/test/editor_lsp_test_context.rs                                           |   51 
crates/editor/src/test/editor_test_context.rs                                               |   14 
crates/eval/Cargo.toml                                                                      |   10 
crates/eval/runner_settings.json                                                            |    6 
crates/eval/src/eval.rs                                                                     |   40 
crates/eval/src/example.rs                                                                  |  306 
crates/eval/src/examples/add_arg_to_trait_method.rs                                         |    6 
crates/eval/src/examples/code_block_citations.rs                                            |   19 
crates/eval/src/examples/comment_translation.rs                                             |   34 
crates/eval/src/examples/file_change_notification.rs                                        |    6 
crates/eval/src/examples/file_search.rs                                                     |   13 
crates/eval/src/examples/grep_params_escapement.rs                                          |    8 
crates/eval/src/examples/mod.rs                                                             |    3 
crates/eval/src/examples/overwrite_file.rs                                                  |   19 
crates/eval/src/examples/planets.rs                                                         |   22 
crates/eval/src/examples/threads/overwrite-file.json                                        |    2 
crates/eval/src/instance.rs                                                                 |  493 
crates/explorer_command_injector/Cargo.toml                                                 |    1 
crates/extension/Cargo.toml                                                                 |    1 
crates/extension_cli/Cargo.toml                                                             |    1 
crates/extension_host/Cargo.toml                                                            |    2 
crates/extension_host/benches/extension_compilation_benchmark.rs                            |    3 
crates/extension_host/src/extension_store_test.rs                                           |    1 
crates/extension_host/src/wasm_host.rs                                                      |   42 
crates/extension_host/src/wasm_host/wit/since_v0_1_0.rs                                     |    2 
crates/extension_host/src/wasm_host/wit/since_v0_6_0.rs                                     |    2 
crates/extensions_ui/Cargo.toml                                                             |    1 
crates/extensions_ui/src/components/extension_card.rs                                       |    9 
crates/extensions_ui/src/extensions_ui.rs                                                   |   71 
crates/feature_flags/Cargo.toml                                                             |    1 
crates/feedback/Cargo.toml                                                                  |    1 
crates/file_finder/Cargo.toml                                                               |    1 
crates/file_finder/src/file_finder.rs                                                       |   68 
crates/file_finder/src/file_finder_settings.rs                                              |    6 
crates/file_finder/src/file_finder_tests.rs                                                 |  145 
crates/file_icons/Cargo.toml                                                                |    1 
crates/fs/Cargo.toml                                                                        |    1 
crates/fs/src/fake_git_repo.rs                                                              |   54 
crates/fs/src/fs.rs                                                                         |   40 
crates/fs_benchmarks/Cargo.toml                                                             |    1 
crates/fsevent/Cargo.toml                                                                   |    1 
crates/fuzzy/Cargo.toml                                                                     |    1 
crates/fuzzy/src/paths.rs                                                                   |   31 
crates/git/Cargo.toml                                                                       |    1 
crates/git/src/repository.rs                                                                |    8 
crates/git_hosting_providers/Cargo.toml                                                     |    1 
crates/git_ui/Cargo.toml                                                                    |    1 
crates/git_ui/src/blame_ui.rs                                                               |   31 
crates/git_ui/src/branch_picker.rs                                                          |   52 
crates/git_ui/src/commit_modal.rs                                                           |    9 
crates/git_ui/src/commit_tooltip.rs                                                         |    3 
crates/git_ui/src/commit_view.rs                                                            |  366 
crates/git_ui/src/git_panel.rs                                                              |   38 
crates/git_ui/src/git_panel_settings.rs                                                     |   14 
crates/git_ui/src/git_ui.rs                                                                 |   23 
crates/git_ui/src/project_diff.rs                                                           |  128 
crates/git_ui/src/stash_picker.rs                                                           |   98 
crates/git_ui/src/text_diff_view.rs                                                         |    2 
crates/go_to_line/Cargo.toml                                                                |    1 
crates/go_to_line/src/cursor_position.rs                                                    |    4 
crates/go_to_line/src/go_to_line.rs                                                         |    6 
crates/google_ai/Cargo.toml                                                                 |    1 
crates/gpui/Cargo.toml                                                                      |    4 
crates/gpui/README.md                                                                       |    2 
crates/gpui/examples/focus_visible.rs                                                       |  214 
crates/gpui/examples/text_layout.rs                                                         |   10 
crates/gpui/src/app.rs                                                                      |   21 
crates/gpui/src/app/test_context.rs                                                         |    4 
crates/gpui/src/bounds_tree.rs                                                              |    5 
crates/gpui/src/element.rs                                                                  |   26 
crates/gpui/src/elements/div.rs                                                             |   20 
crates/gpui/src/elements/list.rs                                                            |   27 
crates/gpui/src/elements/uniform_list.rs                                                    |   63 
crates/gpui/src/inspector.rs                                                                |    2 
crates/gpui/src/key_dispatch.rs                                                             |   18 
crates/gpui/src/keymap.rs                                                                   |    8 
crates/gpui/src/platform.rs                                                                 |   14 
crates/gpui/src/platform/linux/x11/clipboard.rs                                             |    2 
crates/gpui/src/platform/mac/platform.rs                                                    |    6 
crates/gpui/src/platform/windows/events.rs                                                  |   20 
crates/gpui/src/platform/windows/keyboard.rs                                                |   32 
crates/gpui/src/platform/windows/platform.rs                                                |   19 
crates/gpui/src/platform/windows/window.rs                                                  |   19 
crates/gpui/src/taffy.rs                                                                    |    7 
crates/gpui/src/text_system.rs                                                              |    6 
crates/gpui/src/window.rs                                                                   |   57 
crates/gpui_macros/Cargo.toml                                                               |    1 
crates/gpui_tokio/Cargo.toml                                                                |    1 
crates/html_to_markdown/Cargo.toml                                                          |    1 
crates/http_client/Cargo.toml                                                               |    1 
crates/http_client_tls/Cargo.toml                                                           |    1 
crates/icons/Cargo.toml                                                                     |    1 
crates/image_viewer/Cargo.toml                                                              |    1 
crates/image_viewer/src/image_viewer.rs                                                     |   28 
crates/inspector_ui/Cargo.toml                                                              |    1 
crates/install_cli/Cargo.toml                                                               |    1 
crates/journal/Cargo.toml                                                                   |    1 
crates/json_schema_store/Cargo.toml                                                         |    1 
crates/keymap_editor/Cargo.toml                                                             |    1 
crates/keymap_editor/src/keymap_editor.rs                                                   |   79 
crates/language/Cargo.toml                                                                  |    2 
crates/language/src/buffer.rs                                                               |   73 
crates/language/src/buffer_tests.rs                                                         |   69 
crates/language/src/language.rs                                                             |   66 
crates/language/src/language_settings.rs                                                    |  127 
crates/language/src/proto.rs                                                                |   50 
crates/language/src/syntax_map/syntax_map_tests.rs                                          |   12 
crates/language/src/toolchain.rs                                                            |   10 
crates/language_extension/Cargo.toml                                                        |    1 
crates/language_model/Cargo.toml                                                            |    2 
crates/language_model/src/language_model.rs                                                 |    8 
crates/language_model/src/request.rs                                                        |    8 
crates/language_models/Cargo.toml                                                           |    1 
crates/language_models/src/provider/anthropic.rs                                            |    6 
crates/language_models/src/provider/bedrock.rs                                              |   21 
crates/language_models/src/provider/deepseek.rs                                             |    6 
crates/language_models/src/provider/google.rs                                               |    6 
crates/language_models/src/provider/mistral.rs                                              |   10 
crates/language_models/src/provider/ollama.rs                                               |   11 
crates/language_models/src/provider/open_ai.rs                                              |    6 
crates/language_models/src/provider/open_ai_compatible.rs                                   |    6 
crates/language_models/src/provider/open_router.rs                                          |    6 
crates/language_models/src/provider/vercel.rs                                               |    6 
crates/language_models/src/provider/x_ai.rs                                                 |    6 
crates/language_onboarding/Cargo.toml                                                       |    1 
crates/language_selector/Cargo.toml                                                         |    1 
crates/language_selector/src/active_buffer_language.rs                                      |    4 
crates/language_tools/Cargo.toml                                                            |    1 
crates/language_tools/src/key_context_view.rs                                               |   12 
crates/language_tools/src/lsp_button.rs                                                     |   10 
crates/language_tools/src/lsp_log_view.rs                                                   |   15 
crates/language_tools/src/syntax_tree_view.rs                                               |   13 
crates/languages/Cargo.toml                                                                 |    1 
crates/languages/src/bash/injections.scm                                                    |    3 
crates/languages/src/c/highlights.scm                                                       |   25 
crates/languages/src/c/injections.scm                                                       |    4 
crates/languages/src/cpp/highlights.scm                                                     |   39 
crates/languages/src/cpp/injections.scm                                                     |    4 
crates/languages/src/gitcommit/injections.scm                                               |    4 
crates/languages/src/go/injections.scm                                                      |    4 
crates/languages/src/gomod/injections.scm                                                   |    2 
crates/languages/src/gowork/injections.scm                                                  |    2 
crates/languages/src/javascript/highlights.scm                                              |   35 
crates/languages/src/javascript/injections.scm                                              |    4 
crates/languages/src/javascript/outline.scm                                                 |  146 
crates/languages/src/python.rs                                                              |  159 
crates/languages/src/python/injections.scm                                                  |    3 
crates/languages/src/rust/highlights.scm                                                    |   25 
crates/languages/src/rust/injections.scm                                                    |    4 
crates/languages/src/tsx/highlights.scm                                                     |   35 
crates/languages/src/tsx/injections.scm                                                     |    4 
crates/languages/src/typescript.rs                                                          |  220 
crates/languages/src/typescript/highlights.scm                                              |   35 
crates/languages/src/typescript/injections.scm                                              |    4 
crates/languages/src/typescript/outline.scm                                                 |  145 
crates/languages/src/vtsls.rs                                                               |   14 
crates/languages/src/yaml/injections.scm                                                    |    3 
crates/line_ending_selector/Cargo.toml                                                      |    1 
crates/line_ending_selector/src/line_ending_indicator.rs                                    |   68 
crates/line_ending_selector/src/line_ending_selector.rs                                     |   10 
crates/livekit_api/Cargo.toml                                                               |    1 
crates/livekit_client/Cargo.toml                                                            |    3 
crates/lmstudio/Cargo.toml                                                                  |    1 
crates/lsp/Cargo.toml                                                                       |    1 
crates/markdown/Cargo.toml                                                                  |    1 
crates/markdown_preview/Cargo.toml                                                          |    1 
crates/markdown_preview/src/markdown_elements.rs                                            |   33 
crates/markdown_preview/src/markdown_parser.rs                                              |  175 
crates/markdown_preview/src/markdown_preview_view.rs                                        |    8 
crates/markdown_preview/src/markdown_renderer.rs                                            |  342 
crates/media/Cargo.toml                                                                     |    1 
crates/menu/Cargo.toml                                                                      |    1 
crates/migrator/Cargo.toml                                                                  |    1 
crates/migrator/src/migrations.rs                                                           |    6 
crates/migrator/src/migrations/m_2025_10_16/settings.rs                                     |    5 
crates/migrator/src/migrations/m_2025_10_17/settings.rs                                     |   23 
crates/migrator/src/migrator.rs                                                             |   95 
crates/mistral/Cargo.toml                                                                   |    1 
crates/multi_buffer/Cargo.toml                                                              |    1 
crates/multi_buffer/src/anchor.rs                                                           |    8 
crates/multi_buffer/src/multi_buffer.rs                                                     | 1174 
crates/multi_buffer/src/multi_buffer_tests.rs                                               |   25 
crates/multi_buffer/src/path_key.rs                                                         |  417 
crates/multi_buffer/src/transaction.rs                                                      |  524 
crates/nc/Cargo.toml                                                                        |    1 
crates/net/Cargo.toml                                                                       |    1 
crates/node_runtime/Cargo.toml                                                              |    1 
crates/notifications/Cargo.toml                                                             |    1 
crates/notifications/src/notification_store.rs                                              |   14 
crates/ollama/Cargo.toml                                                                    |    1 
crates/onboarding/Cargo.toml                                                                |    2 
crates/onboarding/src/onboarding.rs                                                         |   10 
crates/onboarding/src/welcome.rs                                                            |   37 
crates/open_ai/Cargo.toml                                                                   |    1 
crates/open_router/Cargo.toml                                                               |    1 
crates/outline/Cargo.toml                                                                   |    1 
crates/outline/src/outline.rs                                                               |    7 
crates/outline_panel/Cargo.toml                                                             |    1 
crates/outline_panel/src/outline_panel.rs                                                   |   23 
crates/outline_panel/src/outline_panel_settings.rs                                          |   16 
crates/panel/Cargo.toml                                                                     |    1 
crates/paths/Cargo.toml                                                                     |    1 
crates/paths/src/paths.rs                                                                   |    2 
crates/picker/Cargo.toml                                                                    |    1 
crates/picker/src/picker.rs                                                                 |   10 
crates/prettier/Cargo.toml                                                                  |    1 
crates/project/Cargo.toml                                                                   |    1 
crates/project/src/buffer_store.rs                                                          |   21 
crates/project/src/debugger/dap_store.rs                                                    |   10 
crates/project/src/direnv.rs                                                                |   82 
crates/project/src/environment.rs                                                           |  291 
crates/project/src/git_store.rs                                                             |  121 
crates/project/src/git_store/conflict_set.rs                                                |   16 
crates/project/src/image_store.rs                                                           |    1 
crates/project/src/invalid_item_view.rs                                                     |   13 
crates/project/src/lsp_command.rs                                                           |   20 
crates/project/src/lsp_command/signature_help.rs                                            |  124 
crates/project/src/lsp_store.rs                                                             |  701 
crates/project/src/lsp_store/inlay_hint_cache.rs                                            |  221 
crates/project/src/lsp_store/vue_language_server_ext.rs                                     |  124 
crates/project/src/project.rs                                                               |  152 
crates/project/src/project_settings.rs                                                      |   73 
crates/project/src/project_tests.rs                                                         |   36 
crates/project/src/telemetry_snapshot.rs                                                    |  125 
crates/project/src/terminals.rs                                                             |  375 
crates/project/src/toolchain_store.rs                                                       |   21 
crates/project/src/worktree_store.rs                                                        |   35 
crates/project_panel/Cargo.toml                                                             |    1 
crates/project_panel/src/project_panel.rs                                                   |  111 
crates/project_panel/src/project_panel_settings.rs                                          |   40 
crates/project_panel/src/project_panel_tests.rs                                             |   52 
crates/project_symbols/Cargo.toml                                                           |    1 
crates/prompt_store/Cargo.toml                                                              |    1 
crates/proto/Cargo.toml                                                                     |    1 
crates/proto/proto/lsp.proto                                                                |    4 
crates/proto/src/proto.rs                                                                   |    2 
crates/recent_projects/Cargo.toml                                                           |    1 
crates/recent_projects/src/recent_projects.rs                                               |    9 
crates/refineable/Cargo.toml                                                                |    1 
crates/refineable/derive_refineable/Cargo.toml                                              |    1 
crates/release_channel/Cargo.toml                                                           |    1 
crates/remote/Cargo.toml                                                                    |    1 
crates/remote/src/transport/ssh.rs                                                          |  351 
crates/remote/src/transport/wsl.rs                                                          |    2 
crates/remote_server/Cargo.toml                                                             |    3 
crates/remote_server/src/headless_project.rs                                                |    3 
crates/remote_server/src/remote_editing_tests.rs                                            |   42 
crates/remote_server/src/unix.rs                                                            |    4 
crates/repl/Cargo.toml                                                                      |    3 
crates/repl/src/notebook/notebook_ui.rs                                                     |   28 
crates/repl/src/outputs/image.rs                                                            |    1 
crates/repl/src/repl_editor.rs                                                              |   10 
crates/repl/src/repl_sessions_ui.rs                                                         |    4 
crates/reqwest_client/Cargo.toml                                                            |    1 
crates/rich_text/Cargo.toml                                                                 |    1 
crates/rope/Cargo.toml                                                                      |    1 
crates/rope/benches/rope_benchmark.rs                                                       |   18 
crates/rope/src/chunk.rs                                                                    |  253 
crates/rope/src/rope.rs                                                                     |  247 
crates/rpc/Cargo.toml                                                                       |    1 
crates/rpc/src/proto_client.rs                                                              |    5 
crates/rules_library/Cargo.toml                                                             |    1 
crates/rules_library/src/rules_library.rs                                                   |  151 
crates/scheduler/Cargo.toml                                                                 |    1 
crates/schema_generator/Cargo.toml                                                          |    1 
crates/search/Cargo.toml                                                                    |    4 
crates/search/src/buffer_search.rs                                                          |   15 
crates/search/src/project_search.rs                                                         |  157 
crates/search/src/search.rs                                                                 |    4 
crates/search/src/search_bar.rs                                                             |    2 
crates/search/src/search_status_button.rs                                                   |    9 
crates/semantic_version/Cargo.toml                                                          |    1 
crates/session/Cargo.toml                                                                   |    1 
crates/settings/Cargo.toml                                                                  |    1 
crates/settings/src/base_keymap_setting.rs                                                  |   11 
crates/settings/src/serde_helper.rs                                                         |  135 
crates/settings/src/settings.rs                                                             |    2 
crates/settings/src/settings_content.rs                                                     |  285 
crates/settings/src/settings_content/agent.rs                                               |    7 
crates/settings/src/settings_content/editor.rs                                              |  119 
crates/settings/src/settings_content/language.rs                                            |    4 
crates/settings/src/settings_content/language_model.rs                                      |    3 
crates/settings/src/settings_content/project.rs                                             |   19 
crates/settings/src/settings_content/terminal.rs                                            |   34 
crates/settings/src/settings_content/theme.rs                                               |   70 
crates/settings/src/settings_content/workspace.rs                                           |   38 
crates/settings/src/settings_store.rs                                                       |  193 
crates/settings/src/vscode_import.rs                                                        |  814 
crates/settings_macros/Cargo.toml                                                           |    1 
crates/settings_profile_selector/Cargo.toml                                                 |    1 
crates/settings_ui/Cargo.toml                                                               |    2 
crates/settings_ui/src/components.rs                                                        |  104 
crates/settings_ui/src/components/font_picker.rs                                            |    0 
crates/settings_ui/src/components/icon_theme_picker.rs                                      |  189 
crates/settings_ui/src/components/input_field.rs                                            |   96 
crates/settings_ui/src/components/theme_picker.rs                                           |  179 
crates/settings_ui/src/page_data.rs                                                         |  543 
crates/settings_ui/src/settings_ui.rs                                                       |  573 
crates/snippet/Cargo.toml                                                                   |    1 
crates/snippet_provider/Cargo.toml                                                          |    1 
crates/snippets_ui/Cargo.toml                                                               |    1 
crates/sqlez/Cargo.toml                                                                     |    1 
crates/sqlez_macros/Cargo.toml                                                              |    1 
crates/story/Cargo.toml                                                                     |    1 
crates/storybook/Cargo.toml                                                                 |    1 
crates/streaming_diff/Cargo.toml                                                            |    1 
crates/sum_tree/Cargo.toml                                                                  |    1 
crates/sum_tree/src/cursor.rs                                                               |    4 
crates/sum_tree/src/sum_tree.rs                                                             |  126 
crates/sum_tree/src/tree_map.rs                                                             |    7 
crates/supermaven/Cargo.toml                                                                |    1 
crates/supermaven_api/Cargo.toml                                                            |    1 
crates/svg_preview/Cargo.toml                                                               |    1 
crates/system_specs/Cargo.toml                                                              |    1 
crates/tab_switcher/Cargo.toml                                                              |    1 
crates/task/Cargo.toml                                                                      |    1 
crates/tasks_ui/Cargo.toml                                                                  |    1 
crates/tasks_ui/src/modal.rs                                                                |   63 
crates/telemetry/Cargo.toml                                                                 |    1 
crates/telemetry_events/Cargo.toml                                                          |    1 
crates/terminal/Cargo.toml                                                                  |    1 
crates/terminal/src/terminal.rs                                                             |  531 
crates/terminal/src/terminal_settings.rs                                                    |   80 
crates/terminal_view/Cargo.toml                                                             |    1 
crates/terminal_view/src/persistence.rs                                                     |  102 
crates/terminal_view/src/terminal_panel.rs                                                  |  291 
crates/terminal_view/src/terminal_view.rs                                                   |   49 
crates/text/Cargo.toml                                                                      |    1 
crates/text/src/anchor.rs                                                                   |   27 
crates/text/src/operation_queue.rs                                                          |   16 
crates/text/src/tests.rs                                                                    |   58 
crates/text/src/text.rs                                                                     |   94 
crates/text/src/undo_map.rs                                                                 |   12 
crates/theme/Cargo.toml                                                                     |    1 
crates/theme/src/default_colors.rs                                                          |   22 
crates/theme/src/fallback_themes.rs                                                         |   10 
crates/theme/src/icon_theme.rs                                                              |    8 
crates/theme/src/schema.rs                                                                  |   36 
crates/theme/src/settings.rs                                                                |   11 
crates/theme/src/styles/colors.rs                                                           |   19 
crates/theme_extension/Cargo.toml                                                           |    1 
crates/theme_importer/Cargo.toml                                                            |    1 
crates/theme_selector/Cargo.toml                                                            |    1 
crates/time_format/Cargo.toml                                                               |    1 
crates/title_bar/Cargo.toml                                                                 |    1 
crates/title_bar/src/collab.rs                                                              |    9 
crates/title_bar/src/onboarding_banner.rs                                                   |    3 
crates/title_bar/src/title_bar.rs                                                           |   18 
crates/toolchain_selector/Cargo.toml                                                        |    1 
crates/toolchain_selector/src/toolchain_selector.rs                                         |    3 
crates/ui/Cargo.toml                                                                        |    1 
crates/ui/src/components/context_menu.rs                                                    |   39 
crates/ui/src/components/keybinding.rs                                                      |  316 
crates/ui/src/components/keybinding_hint.rs                                                 |   27 
crates/ui/src/components/stories/keybinding.rs                                              |   85 
crates/ui/src/components/tooltip.rs                                                         |   25 
crates/ui_input/Cargo.toml                                                                  |    3 
crates/ui_input/src/input_field.rs                                                          |  222 
crates/ui_input/src/number_field.rs                                                         |  115 
crates/ui_input/src/ui_input.rs                                                             |  230 
crates/ui_macros/Cargo.toml                                                                 |    1 
crates/ui_prompt/Cargo.toml                                                                 |    1 
crates/util/Cargo.toml                                                                      |    1 
crates/util/src/paths.rs                                                                    |    2 
crates/util/src/shell.rs                                                                    |    3 
crates/util/src/util.rs                                                                     |    7 
crates/util_macros/Cargo.toml                                                               |    1 
crates/vercel/Cargo.toml                                                                    |    1 
crates/vim/Cargo.toml                                                                       |    1 
crates/vim/src/change_list.rs                                                               |    9 
crates/vim/src/command.rs                                                                   |   22 
crates/vim/src/helix.rs                                                                     |   28 
crates/vim/src/helix/duplicate.rs                                                           |    3 
crates/vim/src/helix/paste.rs                                                               |    3 
crates/vim/src/insert.rs                                                                    |   22 
crates/vim/src/mode_indicator.rs                                                            |   76 
crates/vim/src/motion.rs                                                                    |    2 
crates/vim/src/normal.rs                                                                    |   28 
crates/vim/src/normal/convert.rs                                                            |    2 
crates/vim/src/normal/increment.rs                                                          |    2 
crates/vim/src/normal/mark.rs                                                               |   31 
crates/vim/src/normal/paste.rs                                                              |    5 
crates/vim/src/normal/scroll.rs                                                             |   10 
crates/vim/src/normal/substitute.rs                                                         |    5 
crates/vim/src/normal/yank.rs                                                               |    4 
crates/vim/src/replace.rs                                                                   |   12 
crates/vim/src/state.rs                                                                     |    4 
crates/vim/src/surrounds.rs                                                                 |   15 
crates/vim/src/test.rs                                                                      |    5 
crates/vim/src/vim.rs                                                                       |   17 
crates/vim/src/visual.rs                                                                    |   26 
crates/vim_mode_setting/Cargo.toml                                                          |    1 
crates/vim_mode_setting/src/vim_mode_setting.rs                                             |    6 
crates/watch/Cargo.toml                                                                     |    1 
crates/web_search/Cargo.toml                                                                |    1 
crates/web_search_providers/Cargo.toml                                                      |    1 
crates/workspace/Cargo.toml                                                                 |    1 
crates/workspace/src/dock.rs                                                                |   14 
crates/workspace/src/invalid_item_view.rs                                                   |  113 
crates/workspace/src/item.rs                                                                |   91 
crates/workspace/src/notifications.rs                                                       |   16 
crates/workspace/src/pane.rs                                                                |   58 
crates/workspace/src/pane_group.rs                                                          |    2 
crates/workspace/src/persistence/model.rs                                                   |    3 
crates/workspace/src/shared_screen.rs                                                       |    8 
crates/workspace/src/theme_preview.rs                                                       |   16 
crates/workspace/src/workspace.rs                                                           |  174 
crates/workspace/src/workspace_settings.rs                                                  |  116 
crates/worktree/Cargo.toml                                                                  |    1 
crates/worktree/src/worktree.rs                                                             |   12 
crates/worktree/src/worktree_settings.rs                                                    |   29 
crates/worktree_benchmarks/Cargo.toml                                                       |    1 
crates/x_ai/Cargo.toml                                                                      |    1 
crates/zed/Cargo.toml                                                                       |    5 
crates/zed/src/main.rs                                                                      |   13 
crates/zed/src/zed.rs                                                                       |   32 
crates/zed/src/zed/app_menus.rs                                                             |    4 
crates/zed/src/zed/component_preview.rs                                                     |   13 
crates/zed/src/zed/open_listener.rs                                                         |  131 
crates/zed/src/zed/quick_action_bar.rs                                                      |    4 
crates/zed/src/zed/quick_action_bar/preview.rs                                              |    3 
crates/zed/src/zed/quick_action_bar/repl_menu.rs                                            |    3 
crates/zed/src/zed/windows_only_instance.rs                                                 |    1 
crates/zed_actions/Cargo.toml                                                               |    1 
crates/zed_actions/src/lib.rs                                                               |   17 
crates/zed_env_vars/Cargo.toml                                                              |    1 
crates/zeta/Cargo.toml                                                                      |    1 
crates/zeta/src/rate_completion_modal.rs                                                    |   10 
crates/zeta/src/zeta.rs                                                                     |    4 
crates/zeta2/Cargo.toml                                                                     |    1 
crates/zeta2/src/zeta2.rs                                                                   |   35 
crates/zeta2_tools/Cargo.toml                                                               |    2 
crates/zeta2_tools/src/zeta2_tools.rs                                                       |  476 
crates/zeta_cli/Cargo.toml                                                                  |    1 
crates/zeta_cli/src/main.rs                                                                 |    6 
crates/zlog/Cargo.toml                                                                      |    1 
crates/zlog_settings/Cargo.toml                                                             |    1 
crates/zlog_settings/src/zlog_settings.rs                                                   |    2 
docs/src/SUMMARY.md                                                                         |    3 
docs/src/additional-learning-materials.md                                                   |    1 
docs/src/ai/edit-prediction.md                                                              |    8 
docs/src/ai/external-agents.md                                                              |   12 
docs/src/ai/mcp.md                                                                          |   19 
docs/src/ai/models.md                                                                       |    5 
docs/src/ai/overview.md                                                                     |    8 
docs/src/ai/text-threads.md                                                                 |    4 
docs/src/configuring-languages.md                                                           |   18 
docs/src/configuring-zed.md                                                                 |    5 
docs/src/development/release-notes.md                                                       |   29 
docs/src/development/releases.md                                                            |   66 
docs/src/extensions/developing-extensions.md                                                |   13 
docs/src/git.md                                                                             |   40 
docs/src/languages/cpp.md                                                                   |    1 
docs/src/languages/deno.md                                                                  |    3 
docs/src/languages/php.md                                                                   |   36 
docs/src/languages/yaml.md                                                                  |    2 
docs/src/linux.md                                                                           |   11 
docs/src/themes.md                                                                          |   22 
docs/src/troubleshooting.md                                                                 |   80 
docs/src/visual-customization.md                                                            |    6 
docs/src/workspace-persistence.md                                                           |   31 
extensions/html/languages/html/injections.scm                                               |    4 
extensions/html/src/html.rs                                                                 |   22 
extensions/slash-commands-example/README.md                                                 |    3 
renovate.json                                                                               |    2 
script/bundle-linux                                                                         |    2 
script/bundle-mac                                                                           |    2 
script/danger/dangerfile.ts                                                                 |   11 
script/licenses/zed-licenses.toml                                                           |  144 
script/new-crate                                                                            |    1 
script/update-workspace-hack                                                                |   20 
script/update-workspace-hack.ps1                                                            |   36 
tooling/perf/Cargo.toml                                                                     |    1 
tooling/perf/src/implementation.rs                                                          |   11 
tooling/perf/src/main.rs                                                                    |    2 
tooling/workspace-hack/.gitattributes                                                       |    4 
tooling/workspace-hack/.ignore                                                              |    2 
tooling/workspace-hack/Cargo.toml                                                           |  583 
tooling/workspace-hack/LICENSE-GPL                                                          |    1 
tooling/workspace-hack/build.rs                                                             |    2 
tooling/workspace-hack/src/lib.rs                                                           |    1 
tooling/xtask/Cargo.toml                                                                    |    1 
tooling/xtask/src/tasks/package_conformity.rs                                               |    5 
817 files changed, 22,502 insertions(+), 32,661 deletions(-)

Detailed changes

.cargo/ci-config.toml 🔗

@@ -5,12 +5,16 @@
 # Arrays are merged together though. See: https://doc.rust-lang.org/cargo/reference/config.html#hierarchical-structure
 # The intent for this file is to configure CI build process with a divergance from Zed developers experience; for example, in this config file
 # we use `-D warnings` for rustflags (which makes compilation fail in presence of warnings during build process). Placing that in developers `config.toml`
-# would be incovenient.
+# would be inconvenient.
 # The reason for not using the RUSTFLAGS environment variable is that doing so would override all the settings in the config.toml file, even if the contents of the latter are completely nonsensical. See: https://github.com/rust-lang/cargo/issues/5376
 # Here, we opted to use `[target.'cfg(all())']` instead of `[build]` because `[target.'**']` is guaranteed to be cumulative.
 [target.'cfg(all())']
 rustflags = ["-D", "warnings"]
 
+# We don't need fullest debug information for dev stuff (tests etc.) in CI.
+[profile.dev]
+debug = "limited"
+
 # Use Mold on Linux, because it's faster than GNU ld and LLD.
 #
 # We no longer set this in the default `config.toml` so that developers can opt in to Wild, which

.config/hakari.toml 🔗

@@ -1,42 +0,0 @@
-# This file contains settings for `cargo hakari`.
-# See https://docs.rs/cargo-hakari/latest/cargo_hakari/config for a full list of options.
-
-hakari-package = "workspace-hack"
-
-resolver = "2"
-dep-format-version = "4"
-workspace-hack-line-style = "workspace-dotted"
-
-# this should be the same list as "targets" in ../rust-toolchain.toml
-platforms = [
-    "x86_64-apple-darwin",
-    "aarch64-apple-darwin",
-    "x86_64-unknown-linux-gnu",
-    "aarch64-unknown-linux-gnu",
-    "x86_64-pc-windows-msvc",
-    "x86_64-unknown-linux-musl", # remote server
-]
-
-[traversal-excludes]
-workspace-members = [
-    "remote_server",
-]
-third-party = [
-    { name = "reqwest", version = "0.11.27" },
-    # build of remote_server should not include scap / its x11 dependency
-    { name = "zed-scap", git = "https://github.com/zed-industries/scap", rev = "4afea48c3b002197176fb19cd0f9b180dd36eaac", version = "0.0.8-zed" },
-    # build of remote_server should not need to include on libalsa through rodio
-    { name = "rodio", git = "https://github.com/RustAudio/rodio" },
-]
-
-[final-excludes]
-workspace-members = [
-    "zed_extension_api",
-
-    # exclude all extensions
-    "zed_glsl",
-    "zed_html",
-    "zed_proto",
-    "slash_commands_example",
-    "zed_test_extension",
-]

.config/nextest.toml 🔗

@@ -4,3 +4,17 @@ sequential-db-tests = { max-threads = 1 }
 [[profile.default.overrides]]
 filter = 'package(db)'
 test-group = 'sequential-db-tests'
+
+# Run slowest tests first.
+#
+[[profile.default.overrides]]
+filter = 'package(worktree) and test(test_random_worktree_changes)'
+priority = 100
+
+[[profile.default.overrides]]
+filter = 'package(collab) and (test(random_project_collaboration_tests) or test(random_channel_buffer_tests) or test(test_contact_requests) or test(test_basic_following))'
+priority = 99
+
+[[profile.default.overrides]]
+filter = 'package(extension_host) and test(test_extension_store_with_test_extension)'
+priority = 99

.github/ISSUE_TEMPLATE/06_bug_windows_beta.yml 🔗

@@ -1,35 +0,0 @@
-name: Bug Report (Windows Beta)
-description: Zed Windows Beta Related Bugs
-type: "Bug"
-labels: ["windows"]
-title: "Windows Beta: <a short description of the Windows bug>"
-body:
-  - type: textarea
-    attributes:
-      label: Summary
-      description: Describe the bug with a one-line summary, and provide detailed reproduction steps
-      value: |
-        <!-- Please insert a one-line summary of the issue below -->
-        SUMMARY_SENTENCE_HERE
-
-        ### Description
-        <!--  Describe with sufficient detail to reproduce from a clean Zed install. -->
-        Steps to trigger the problem:
-        1.
-        2.
-        3.
-
-        **Expected Behavior**:
-        **Actual Behavior**:
-
-    validations:
-      required: true
-  - type: textarea
-    id: environment
-    attributes:
-      label: Zed Version and System Specs
-      description: 'Open Zed, and in the command palette select "zed: copy system specs into clipboard"'
-      placeholder: |
-        Output of "zed: copy system specs into clipboard"
-    validations:
-      required: true

.github/ISSUE_TEMPLATE/11_crash_report.yml 🔗

@@ -33,9 +33,10 @@ body:
       required: true
   - type: textarea
     attributes:
-      label: If applicable, attach your `~/Library/Logs/Zed/Zed.log` file to this issue.
+      label: If applicable, attach your `Zed.log` file to this issue.
       description: |
         macOS: `~/Library/Logs/Zed/Zed.log`
+        Windows: `C:\Users\YOU\AppData\Local\Zed\logs\Zed.log`
         Linux: `~/.local/share/zed/logs/Zed.log` or $XDG_DATA_HOME
         If you only need the most recent lines, you can run the `zed: open log` command palette action to see the last 1000.
       value: |

.github/actions/run_tests/action.yml 🔗

@@ -15,8 +15,11 @@ runs:
         node-version: "18"
 
     - name: Limit target directory size
+      env:
+        MAX_SIZE: ${{ runner.os == 'macOS' && 300 || 100 }}
       shell: bash -euxo pipefail {0}
-      run: script/clear-target-dir-if-larger-than 100
+      # Use the variable in the run command
+      run: script/clear-target-dir-if-larger-than ${{ env.MAX_SIZE }}
 
     - name: Run tests
       shell: bash -euxo pipefail {0}

.github/workflows/ci.yml 🔗

@@ -130,39 +130,6 @@ jobs:
           input: "crates/proto/proto/"
           against: "https://github.com/${GITHUB_REPOSITORY}.git#branch=${BUF_BASE_BRANCH},subdir=crates/proto/proto/"
 
-  workspace_hack:
-    timeout-minutes: 60
-    name: Check workspace-hack crate
-    needs: [job_spec]
-    if: |
-      github.repository_owner == 'zed-industries' &&
-      needs.job_spec.outputs.run_tests == 'true'
-    runs-on:
-      - namespace-profile-8x16-ubuntu-2204
-    steps:
-      - name: Checkout repo
-        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
-      - name: Add Rust to the PATH
-        run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
-      - name: Install cargo-hakari
-        uses: clechasseur/rs-cargo@8435b10f6e71c2e3d4d3b7573003a8ce4bfc6386 # v2
-        with:
-          command: install
-          args: cargo-hakari@0.9.35
-
-      - name: Check workspace-hack Cargo.toml is up-to-date
-        run: |
-          cargo hakari generate --diff || {
-            echo "To fix, run script/update-workspace-hack or script/update-workspace-hack.ps1";
-            false
-          }
-      - name: Check all crates depend on workspace-hack
-        run: |
-          cargo hakari manage-deps --dry-run || {
-            echo "To fix, run script/update-workspace-hack or script/update-workspace-hack.ps1"
-            false
-          }
-
   style:
     timeout-minutes: 60
     name: Check formatting and spelling
@@ -210,7 +177,7 @@ jobs:
         uses: ./.github/actions/check_style
 
       - name: Check for typos
-        uses: crate-ci/typos@8e6a4285bcbde632c5d79900a7779746e8b7ea3f # v1.24.6
+        uses: crate-ci/typos@80c8a4945eec0f6d464eaf9e65ed98ef085283d1 # v1.38.1
         with:
           config: ./typos.toml
 
@@ -507,7 +474,6 @@ jobs:
       - actionlint
       - migration_checks
       # run_tests: If adding required tests, add them here and to script below.
-      - workspace_hack
       - linux_tests
       - build_remote_server
       - macos_tests
@@ -533,7 +499,6 @@ jobs:
 
           # Only check test jobs if they were supposed to run
           if [[ "${{ needs.job_spec.outputs.run_tests }}" == "true" ]]; then
-            [[ "${{ needs.workspace_hack.result }}"       != 'success' ]] && { RET_CODE=1; echo "Workspace Hack failed"; }
             [[ "${{ needs.macos_tests.result }}"          != 'success' ]] && { RET_CODE=1; echo "macOS tests failed"; }
             [[ "${{ needs.linux_tests.result }}"          != 'success' ]] && { RET_CODE=1; echo "Linux tests failed"; }
             [[ "${{ needs.windows_tests.result }}"        != 'success' ]] && { RET_CODE=1; echo "Windows tests failed"; }
@@ -882,7 +847,8 @@ jobs:
   auto-release-preview:
     name: Auto release preview
     if: |
-      startsWith(github.ref, 'refs/tags/v')
+      false
+      && startsWith(github.ref, 'refs/tags/v')
       && endsWith(github.ref, '-pre') && !endsWith(github.ref, '.0-pre')
     needs: [bundle-mac, bundle-linux-x86_x64, bundle-linux-aarch64, bundle-windows-x64]
     runs-on:

.gitignore 🔗

@@ -25,6 +25,7 @@
 /crates/collab/seed.json
 /crates/theme/schemas/theme.json
 /crates/zed/resources/flatpak/flatpak-cargo-sources.json
+/crates/project_panel/benches/linux_repo_snapshot.txt
 /dev.zed.Zed*.json
 /node_modules/
 /plugins/bin

.zed/settings.json 🔗

@@ -48,7 +48,7 @@
   "remove_trailing_whitespace_on_save": true,
   "ensure_final_newline_on_save": true,
   "file_scan_exclusions": [
-    "crates/assistant_tools/src/edit_agent/evals/fixtures",
+    "crates/agent/src/edit_agent/evals/fixtures",
     "crates/eval/worktrees/",
     "crates/eval/repos/",
     "**/.git",

Cargo.lock 🔗

@@ -26,7 +26,7 @@ dependencies = [
  "portable-pty",
  "project",
  "prompt_store",
- "rand 0.9.1",
+ "rand 0.9.2",
  "serde",
  "serde_json",
  "settings",
@@ -39,7 +39,6 @@ dependencies = [
  "util",
  "uuid",
  "watch",
- "workspace-hack",
 ]
 
 [[package]]
@@ -59,7 +58,6 @@ dependencies = [
  "ui",
  "util",
  "workspace",
- "workspace-hack",
 ]
 
 [[package]]
@@ -78,13 +76,12 @@ dependencies = [
  "log",
  "pretty_assertions",
  "project",
- "rand 0.9.1",
+ "rand 0.9.2",
  "serde_json",
  "settings",
  "text",
  "util",
  "watch",
- "workspace-hack",
  "zlog",
 ]
 
@@ -106,23 +103,22 @@ dependencies = [
  "ui",
  "util",
  "workspace",
- "workspace-hack",
 ]
 
 [[package]]
 name = "addr2line"
-version = "0.24.2"
+version = "0.25.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
+checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b"
 dependencies = [
- "gimli",
+ "gimli 0.32.3",
 ]
 
 [[package]]
 name = "adler2"
-version = "2.0.0"
+version = "2.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
+checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
 
 [[package]]
 name = "aes"
@@ -139,90 +135,14 @@ dependencies = [
 [[package]]
 name = "agent"
 version = "0.1.0"
-dependencies = [
- "action_log",
- "agent_settings",
- "anyhow",
- "assistant_context",
- "assistant_tool",
- "assistant_tools",
- "chrono",
- "client",
- "cloud_llm_client",
- "collections",
- "component",
- "context_server",
- "convert_case 0.8.0",
- "fs",
- "futures 0.3.31",
- "git",
- "gpui",
- "heed",
- "http_client",
- "icons",
- "indoc",
- "language",
- "language_model",
- "log",
- "parking_lot",
- "paths",
- "postage",
- "pretty_assertions",
- "project",
- "prompt_store",
- "rand 0.9.1",
- "ref-cast",
- "rope",
- "schemars 1.0.1",
- "serde",
- "serde_json",
- "settings",
- "smol",
- "sqlez",
- "telemetry",
- "text",
- "theme",
- "thiserror 2.0.12",
- "time",
- "util",
- "uuid",
- "workspace",
- "workspace-hack",
- "zed_env_vars",
- "zstd 0.11.2+zstd.1.5.2",
-]
-
-[[package]]
-name = "agent-client-protocol"
-version = "0.4.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3aaa2bd05a2401887945f8bfd70026e90bc3cf96c62ab9eba2779835bf21dc60"
-dependencies = [
- "anyhow",
- "async-broadcast",
- "async-trait",
- "futures 0.3.31",
- "log",
- "parking_lot",
- "schemars 1.0.1",
- "serde",
- "serde_json",
-]
-
-[[package]]
-name = "agent2"
-version = "0.1.0"
 dependencies = [
  "acp_thread",
  "action_log",
- "agent",
  "agent-client-protocol",
  "agent_servers",
  "agent_settings",
  "anyhow",
- "assistant_context",
- "assistant_tool",
- "assistant_tools",
+ "assistant_text_thread",
  "chrono",
  "client",
  "clock",
@@ -231,6 +151,7 @@ dependencies = [
  "context_server",
  "ctor",
  "db",
+ "derive_more 0.99.20",
  "editor",
  "env_logger 0.11.8",
  "fs",
@@ -254,21 +175,26 @@ dependencies = [
  "pretty_assertions",
  "project",
  "prompt_store",
+ "rand 0.9.2",
+ "regex",
  "reqwest_client",
  "rust-embed",
- "schemars 1.0.1",
+ "schemars 1.0.4",
  "serde",
  "serde_json",
  "settings",
+ "smallvec",
  "smol",
  "sqlez",
+ "streaming_diff",
+ "strsim",
  "task",
  "telemetry",
  "tempfile",
  "terminal",
  "text",
  "theme",
- "thiserror 2.0.12",
+ "thiserror 2.0.17",
  "tree-sitter-rust",
  "ui",
  "unindent",
@@ -276,13 +202,43 @@ dependencies = [
  "uuid",
  "watch",
  "web_search",
- "workspace-hack",
  "worktree",
  "zed_env_vars",
  "zlog",
  "zstd 0.11.2+zstd.1.5.2",
 ]
 
+[[package]]
+name = "agent-client-protocol"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f655394a107cd601bd2e5375c2d909ea83adc65678a0e0e8d77613d3c848a7d"
+dependencies = [
+ "agent-client-protocol-schema",
+ "anyhow",
+ "async-broadcast",
+ "async-trait",
+ "derive_more 2.0.1",
+ "futures 0.3.31",
+ "log",
+ "parking_lot",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "agent-client-protocol-schema"
+version = "0.4.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61be4454304d7df1a5b44c4ae55e707ffe72eac4dfb1ef8762510ce8d8f6d924"
+dependencies = [
+ "anyhow",
+ "derive_more 2.0.1",
+ "schemars 1.0.4",
+ "serde",
+ "serde_json",
+]
+
 [[package]]
 name = "agent_servers"
 version = "0.1.0"
@@ -318,12 +274,11 @@ dependencies = [
  "task",
  "tempfile",
  "terminal",
- "thiserror 2.0.12",
+ "thiserror 2.0.17",
  "ui",
  "util",
  "uuid",
  "watch",
- "workspace-hack",
 ]
 
 [[package]]
@@ -339,13 +294,12 @@ dependencies = [
  "language_model",
  "paths",
  "project",
- "schemars 1.0.1",
+ "schemars 1.0.4",
  "serde",
  "serde_json",
  "serde_json_lenient",
  "settings",
  "util",
- "workspace-hack",
 ]
 
 [[package]]
@@ -356,17 +310,14 @@ dependencies = [
  "action_log",
  "agent",
  "agent-client-protocol",
- "agent2",
  "agent_servers",
  "agent_settings",
  "ai_onboarding",
  "anyhow",
  "arrayvec",
- "assistant_context",
  "assistant_slash_command",
  "assistant_slash_commands",
- "assistant_tool",
- "assistant_tools",
+ "assistant_text_thread",
  "audio",
  "buffer_diff",
  "chrono",
@@ -410,11 +361,12 @@ dependencies = [
  "project",
  "prompt_store",
  "proto",
- "rand 0.9.1",
+ "rand 0.9.2",
+ "ref-cast",
  "release_channel",
  "rope",
  "rules_library",
- "schemars 1.0.1",
+ "schemars 1.0.4",
  "search",
  "serde",
  "serde_json",
@@ -440,7 +392,6 @@ dependencies = [
  "util",
  "watch",
  "workspace",
- "workspace-hack",
  "zed_actions",
 ]
 
@@ -450,24 +401,24 @@ version = "0.7.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9"
 dependencies = [
- "getrandom 0.2.15",
+ "getrandom 0.2.16",
  "once_cell",
  "version_check",
 ]
 
 [[package]]
 name = "ahash"
-version = "0.8.11"
+version = "0.8.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
+checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
 dependencies = [
  "cfg-if",
  "const-random",
- "getrandom 0.2.15",
+ "getrandom 0.3.4",
  "once_cell",
  "serde",
  "version_check",
- "zerocopy 0.7.35",
+ "zerocopy",
 ]
 
 [[package]]
@@ -492,7 +443,6 @@ dependencies = [
  "smallvec",
  "telemetry",
  "ui",
- "workspace-hack",
  "zed_actions",
 ]
 
@@ -503,7 +453,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3cb5f4f1ef69bdb8b2095ddd14b09dd74ee0303aae8bd5372667a54cff689a1b"
 dependencies = [
  "base64 0.22.1",
- "bitflags 2.9.0",
+ "bitflags 2.9.4",
  "home",
  "libc",
  "log",
@@ -512,7 +462,7 @@ dependencies = [
  "piper",
  "polling",
  "regex-automata",
- "rustix 1.0.7",
+ "rustix 1.1.2",
  "rustix-openpty",
  "serde",
  "signal-hook",
@@ -529,9 +479,12 @@ checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd"
 
 [[package]]
 name = "aligned-vec"
-version = "0.5.0"
+version = "0.6.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4aa90d7ce82d4be67b64039a3d588d38dbcc6736577de4a847025ce5b0c468d1"
+checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b"
+dependencies = [
+ "equator",
+]
 
 [[package]]
 name = "alloc-no-stdlib"
@@ -561,7 +514,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43"
 dependencies = [
  "alsa-sys",
- "bitflags 2.9.0",
+ "bitflags 2.9.4",
  "cfg-if",
  "libc",
 ]
@@ -595,12 +548,6 @@ dependencies = [
  "url",
 ]
 
-[[package]]
-name = "android-tzdata"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
-
 [[package]]
 name = "android_system_properties"
 version = "0.1.5"
@@ -618,9 +565,9 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
 
 [[package]]
 name = "anstream"
-version = "0.6.18"
+version = "0.6.21"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b"
+checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a"
 dependencies = [
  "anstyle",
  "anstyle-parse",
@@ -633,37 +580,37 @@ dependencies = [
 
 [[package]]
 name = "anstyle"
-version = "1.0.10"
+version = "1.0.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
+checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
 
 [[package]]
 name = "anstyle-parse"
-version = "0.2.6"
+version = "0.2.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9"
+checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
 dependencies = [
  "utf8parse",
 ]
 
 [[package]]
 name = "anstyle-query"
-version = "1.1.2"
+version = "1.1.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c"
+checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2"
 dependencies = [
- "windows-sys 0.59.0",
+ "windows-sys 0.60.2",
 ]
 
 [[package]]
 name = "anstyle-wincon"
-version = "3.0.7"
+version = "3.0.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e"
+checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a"
 dependencies = [
  "anstyle",
- "once_cell",
- "windows-sys 0.59.0",
+ "once_cell_polyfill",
+ "windows-sys 0.60.2",
 ]
 
 [[package]]
@@ -674,13 +621,12 @@ dependencies = [
  "chrono",
  "futures 0.3.31",
  "http_client",
- "schemars 1.0.1",
+ "schemars 1.0.4",
  "serde",
  "serde_json",
  "settings",
  "strum 0.27.2",
- "thiserror 2.0.12",
- "workspace-hack",
+ "thiserror 2.0.17",
 ]
 
 [[package]]
@@ -691,9 +637,9 @@ checksum = "34cd60c5e3152cef0a592f1b296f1cc93715d89d2551d85315828c3a09575ff4"
 
 [[package]]
 name = "anyhow"
-version = "1.0.98"
+version = "1.0.100"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
+checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
 
 [[package]]
 name = "approx"
@@ -706,9 +652,9 @@ dependencies = [
 
 [[package]]
 name = "arbitrary"
-version = "1.4.1"
+version = "1.4.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223"
+checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
 dependencies = [
  "derive_arbitrary",
 ]
@@ -721,7 +667,7 @@ checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.101",
+ "syn 2.0.106",
 ]
 
 [[package]]
@@ -803,13 +749,13 @@ dependencies = [
  "enumflags2",
  "futures-channel",
  "futures-util",
- "rand 0.9.1",
+ "rand 0.9.2",
  "serde",
  "serde_repr",
  "url",
  "wayland-backend",
  "wayland-client",
- "wayland-protocols 0.32.6",
+ "wayland-protocols 0.32.9",
  "zbus",
 ]
 
@@ -824,7 +770,7 @@ dependencies = [
  "enumflags2",
  "futures-channel",
  "futures-util",
- "rand 0.9.1",
+ "rand 0.9.2",
  "serde",
  "serde_repr",
  "url",
@@ -843,8 +789,7 @@ dependencies = [
  "smol",
  "tempfile",
  "util",
- "windows 0.61.1",
- "workspace-hack",
+ "windows 0.61.3",
  "zeroize",
 ]
 
@@ -855,55 +800,6 @@ dependencies = [
  "anyhow",
  "gpui",
  "rust-embed",
- "workspace-hack",
-]
-
-[[package]]
-name = "assistant_context"
-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",
- "language",
- "language_model",
- "log",
- "open_ai",
- "parking_lot",
- "paths",
- "pretty_assertions",
- "project",
- "prompt_store",
- "proto",
- "rand 0.9.1",
- "regex",
- "rpc",
- "serde",
- "serde_json",
- "settings",
- "smallvec",
- "smol",
- "telemetry_events",
- "text",
- "ui",
- "unindent",
- "util",
- "uuid",
- "workspace",
- "workspace-hack",
- "zed_env_vars",
 ]
 
 [[package]]
@@ -913,7 +809,7 @@ dependencies = [
  "anyhow",
  "async-trait",
  "collections",
- "derive_more",
+ "derive_more 0.99.20",
  "extension",
  "futures 0.3.31",
  "gpui",
@@ -926,7 +822,6 @@ dependencies = [
  "ui",
  "util",
  "workspace",
- "workspace-hack",
 ]
 
 [[package]]
@@ -960,109 +855,55 @@ dependencies = [
  "ui",
  "util",
  "workspace",
- "workspace-hack",
  "worktree",
  "zlog",
 ]
 
 [[package]]
-name = "assistant_tool"
-version = "0.1.0"
-dependencies = [
- "action_log",
- "anyhow",
- "buffer_diff",
- "clock",
- "collections",
- "ctor",
- "derive_more",
- "gpui",
- "icons",
- "indoc",
- "language",
- "language_model",
- "log",
- "parking_lot",
- "pretty_assertions",
- "project",
- "rand 0.9.1",
- "regex",
- "serde",
- "serde_json",
- "settings",
- "text",
- "util",
- "workspace",
- "workspace-hack",
- "zlog",
-]
-
-[[package]]
-name = "assistant_tools"
+name = "assistant_text_thread"
 version = "0.1.0"
 dependencies = [
- "action_log",
  "agent_settings",
  "anyhow",
- "assistant_tool",
- "buffer_diff",
+ "assistant_slash_command",
+ "assistant_slash_commands",
  "chrono",
  "client",
  "clock",
  "cloud_llm_client",
  "collections",
- "component",
- "derive_more",
- "diffy",
- "editor",
- "feature_flags",
+ "context_server",
  "fs",
  "futures 0.3.31",
+ "fuzzy",
  "gpui",
- "gpui_tokio",
- "handlebars 4.5.0",
- "html_to_markdown",
- "http_client",
  "indoc",
- "itertools 0.14.0",
  "language",
  "language_model",
- "language_models",
  "log",
- "lsp",
- "markdown",
- "open",
+ "open_ai",
+ "parking_lot",
  "paths",
- "portable-pty",
  "pretty_assertions",
  "project",
  "prompt_store",
- "rand 0.9.1",
+ "proto",
+ "rand 0.9.2",
  "regex",
- "reqwest_client",
- "rust-embed",
- "schemars 1.0.1",
+ "rpc",
  "serde",
  "serde_json",
  "settings",
  "smallvec",
  "smol",
- "streaming_diff",
- "strsim",
- "task",
- "tempfile",
- "terminal",
- "terminal_view",
- "theme",
- "tree-sitter-rust",
+ "telemetry_events",
+ "text",
  "ui",
  "unindent",
  "util",
- "watch",
- "web_search",
+ "uuid",
  "workspace",
- "workspace-hack",
- "zlog",
+ "zed_env_vars",
 ]
 
 [[package]]
@@ -1081,7 +922,7 @@ version = "0.7.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532"
 dependencies = [
- "event-listener 5.4.0",
+ "event-listener 5.4.1",
  "event-listener-strategy",
  "futures-core",
  "pin-project-lite",
@@ -1100,9 +941,9 @@ dependencies = [
 
 [[package]]
 name = "async-channel"
-version = "2.3.1"
+version = "2.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a"
+checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2"
 dependencies = [
  "concurrent-queue",
  "event-listener-strategy",
@@ -1112,9 +953,9 @@ dependencies = [
 
 [[package]]
 name = "async-compat"
-version = "0.2.4"
+version = "0.2.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7bab94bde396a3f7b4962e396fdad640e241ed797d4d8d77fc8c237d14c58fc0"
+checksum = "a1ba85bc55464dcbf728b56d97e119d673f4cf9062be330a9a26f3acf504a590"
 dependencies = [
  "futures-core",
  "futures-io",
@@ -1125,15 +966,14 @@ dependencies = [
 
 [[package]]
 name = "async-compression"
-version = "0.4.22"
+version = "0.4.32"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "59a194f9d963d8099596278594b3107448656ba73831c9d8c783e613ce86da64"
+checksum = "5a89bce6054c720275ac2432fbba080a66a2106a44a1b804553930ca6909f4e0"
 dependencies = [
- "deflate64",
- "flate2",
+ "compression-codecs",
+ "compression-core",
  "futures-core",
  "futures-io",
- "memchr",
  "pin-project-lite",
 ]
 
@@ -1149,26 +989,27 @@ dependencies = [
 
 [[package]]
 name = "async-executor"
-version = "1.13.1"
+version = "1.13.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "30ca9a001c1e8ba5149f91a74362376cc6bc5b919d92d988668657bd570bdcec"
+checksum = "497c00e0fd83a72a79a39fcbd8e3e2f055d6f6c7e025f3b3d91f4f8e76527fb8"
 dependencies = [
  "async-task",
  "concurrent-queue",
  "fastrand 2.3.0",
- "futures-lite 2.6.0",
+ "futures-lite 2.6.1",
+ "pin-project-lite",
  "slab",
 ]
 
 [[package]]
 name = "async-fs"
-version = "2.1.3"
+version = "2.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "09f7e37c0ed80b2a977691c47dae8625cfb21e205827106c64f7c588766b2e50"
+checksum = "8034a681df4aed8b8edbd7fbe472401ecf009251c8b40556b304567052e294c5"
 dependencies = [
  "async-lock 3.4.1",
  "blocking",
- "futures-lite 2.6.0",
+ "futures-lite 2.6.1",
 ]
 
 [[package]]
@@ -1177,31 +1018,31 @@ version = "2.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c"
 dependencies = [
- "async-channel 2.3.1",
+ "async-channel 2.5.0",
  "async-executor",
  "async-io",
  "async-lock 3.4.1",
  "blocking",
- "futures-lite 2.6.0",
+ "futures-lite 2.6.1",
  "once_cell",
 ]
 
 [[package]]
 name = "async-io"
-version = "2.5.0"
+version = "2.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "19634d6336019ef220f09fd31168ce5c184b295cbf80345437cc36094ef223ca"
+checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc"
 dependencies = [
- "async-lock 3.4.1",
+ "autocfg",
  "cfg-if",
  "concurrent-queue",
  "futures-io",
- "futures-lite 2.6.0",
+ "futures-lite 2.6.1",
  "parking",
  "polling",
- "rustix 1.0.7",
+ "rustix 1.1.2",
  "slab",
- "windows-sys 0.60.2",
+ "windows-sys 0.61.2",
 ]
 
 [[package]]
@@ -1219,7 +1060,7 @@ version = "3.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5fd03604047cee9b6ce9de9f70c6cd540a0520c813cbd49bae61f33ab80ed1dc"
 dependencies = [
- "event-listener 5.4.0",
+ "event-listener 5.4.1",
  "event-listener-strategy",
  "pin-project-lite",
 ]
@@ -1232,7 +1073,7 @@ checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7"
 dependencies = [
  "async-io",
  "blocking",
- "futures-lite 2.6.0",
+ "futures-lite 2.6.1",
 ]
 
 [[package]]
@@ -1246,21 +1087,20 @@ dependencies = [
 
 [[package]]
 name = "async-process"
-version = "2.3.0"
+version = "2.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "63255f1dc2381611000436537bbedfe83183faa303a5a0edaf191edef06526bb"
+checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75"
 dependencies = [
- "async-channel 2.3.1",
+ "async-channel 2.5.0",
  "async-io",
  "async-lock 3.4.1",
  "async-signal",
  "async-task",
  "blocking",
  "cfg-if",
- "event-listener 5.4.0",
- "futures-lite 2.6.0",
- "rustix 0.38.44",
- "tracing",
+ "event-listener 5.4.1",
+ "futures-lite 2.6.1",
+ "rustix 1.1.2",
 ]
 
 [[package]]
@@ -1271,14 +1111,14 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.101",
+ "syn 2.0.106",
 ]
 
 [[package]]
 name = "async-signal"
-version = "0.2.10"
+version = "0.2.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "637e00349800c0bdf8bfc21ebbc0b6524abea702b0da4168ac00d070d0c0b9f3"
+checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c"
 dependencies = [
  "async-io",
  "async-lock 3.4.1",
@@ -1286,17 +1126,17 @@ dependencies = [
  "cfg-if",
  "futures-core",
  "futures-io",
- "rustix 0.38.44",
+ "rustix 1.1.2",
  "signal-hook-registry",
  "slab",
- "windows-sys 0.59.0",
+ "windows-sys 0.61.2",
 ]
 
 [[package]]
 name = "async-std"
-version = "1.13.1"
+version = "1.13.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "730294c1c08c2e0f85759590518f6333f0d5a0a766a27d519c1b244c3dfd8a24"
+checksum = "2c8e079a4ab67ae52b7403632e4618815d6db36d2a010cfe41b02c1b1578f93b"
 dependencies = [
  "async-attributes",
  "async-channel 1.9.0",
@@ -1308,7 +1148,7 @@ dependencies = [
  "futures-channel",
  "futures-core",
  "futures-io",
- "futures-lite 2.6.0",
+ "futures-lite 2.6.1",
  "gloo-timers",
  "kv-log-macro",
  "log",
@@ -1339,14 +1179,14 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.101",
+ "syn 2.0.106",
 ]
 
 [[package]]
 name = "async-tar"
-version = "0.5.0"
+version = "0.5.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a42f905d4f623faf634bbd1e001e84e0efc24694afa64be9ad239bf6ca49e1f8"
+checksum = "d1937db2d56578aa3919b9bdb0e5100693fd7d1c0f145c53eb81fbb03e217550"
 dependencies = [
  "async-std",
  "filetime",
@@ -1370,14 +1210,14 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.101",
+ "syn 2.0.106",
 ]
 
 [[package]]
 name = "async-tungstenite"
-version = "0.29.1"
+version = "0.31.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ef0f7efedeac57d9b26170f72965ecfd31473ca52ca7a64e925b0b6f5f079886"
+checksum = "ee88b4c88ac8c9ea446ad43498955750a4bbe64c4392f21ccfe5d952865e318f"
 dependencies = [
  "atomic-waker",
  "futures-core",

Cargo.toml 🔗

@@ -6,7 +6,6 @@ members = [
     "crates/action_log",
     "crates/activity_indicator",
     "crates/agent",
-    "crates/agent2",
     "crates/agent_servers",
     "crates/agent_settings",
     "crates/agent_ui",
@@ -14,11 +13,9 @@ members = [
     "crates/anthropic",
     "crates/askpass",
     "crates/assets",
-    "crates/assistant_context",
+    "crates/assistant_text_thread",
     "crates/assistant_slash_command",
     "crates/assistant_slash_commands",
-    "crates/assistant_tool",
-    "crates/assistant_tools",
     "crates/audio",
     "crates/auto_update",
     "crates/auto_update_helper",
@@ -73,6 +70,7 @@ members = [
     "crates/file_finder",
     "crates/file_icons",
     "crates/fs",
+    "crates/fs_benchmarks",
     "crates/fsevent",
     "crates/fuzzy",
     "crates/git",
@@ -221,8 +219,7 @@ members = [
     #
 
     "tooling/perf",
-    "tooling/workspace-hack",
-    "tooling/xtask", "crates/fs_benchmarks", "crates/worktree_benchmarks",
+    "tooling/xtask",
 ]
 default-members = ["crates/zed"]
 
@@ -240,7 +237,6 @@ acp_tools = { path = "crates/acp_tools" }
 acp_thread = { path = "crates/acp_thread" }
 action_log = { path = "crates/action_log" }
 agent = { path = "crates/agent" }
-agent2 = { path = "crates/agent2" }
 activity_indicator = { path = "crates/activity_indicator" }
 agent_ui = { path = "crates/agent_ui" }
 agent_settings = { path = "crates/agent_settings" }
@@ -250,11 +246,9 @@ ai_onboarding = { path = "crates/ai_onboarding" }
 anthropic = { path = "crates/anthropic" }
 askpass = { path = "crates/askpass" }
 assets = { path = "crates/assets" }
-assistant_context = { path = "crates/assistant_context" }
+assistant_text_thread = { path = "crates/assistant_text_thread" }
 assistant_slash_command = { path = "crates/assistant_slash_command" }
 assistant_slash_commands = { path = "crates/assistant_slash_commands" }
-assistant_tool = { path = "crates/assistant_tool" }
-assistant_tools = { path = "crates/assistant_tools" }
 audio = { path = "crates/audio" }
 auto_update = { path = "crates/auto_update" }
 auto_update_helper = { path = "crates/auto_update_helper" }
@@ -378,7 +372,7 @@ remote_server = { path = "crates/remote_server" }
 repl = { path = "crates/repl" }
 reqwest_client = { path = "crates/reqwest_client" }
 rich_text = { path = "crates/rich_text" }
-rodio = { git = "https://github.com/RustAudio/rodio" }
+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" }
@@ -444,7 +438,7 @@ zlog_settings = { path = "crates/zlog_settings" }
 # External crates
 #
 
-agent-client-protocol = { version = "0.4.3", features = ["unstable"] }
+agent-client-protocol = { version = "0.5.0", features = ["unstable"] }
 aho-corasick = "1.1"
 alacritty_terminal = "0.25.1-rc1"
 any_vec = "0.14"
@@ -458,10 +452,10 @@ 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.0"
+async-tar = "0.5.1"
 async-task = "4.7"
 async-trait = "0.1"
-async-tungstenite = "0.29.1"
+async-tungstenite = "0.31.0"
 async_zip = { version = "0.0.17", features = ["deflate", "deflate64"] }
 aws-config = { version = "1.6.1", features = ["behavior-version-latest"] }
 aws-credential-types = { version = "1.2.2", features = [
@@ -487,10 +481,10 @@ chrono = { version = "0.4", features = ["serde"] }
 ciborium = "0.2"
 circular-buffer = "1.0"
 clap = { version = "4.4", features = ["derive"] }
-cocoa = "0.26"
-cocoa-foundation = "0.2.0"
+cocoa = "=0.26.0"
+cocoa-foundation = "=0.2.0"
 convert_case = "0.8.0"
-core-foundation = "0.10.0"
+core-foundation = "=0.10.0"
 core-foundation-sys = "0.8.6"
 core-video = { version = "0.4.3", features = ["metal"] }
 cpal = "0.16"
@@ -553,7 +547,7 @@ nix = "0.29"
 num-format = "0.4.4"
 num-traits = "0.2"
 objc = "0.2"
-objc2-foundation = { version = "0.3", default-features = false, features = [
+objc2-foundation = { version = "=0.3.1", default-features = false, features = [
     "NSArray",
     "NSAttributedString",
     "NSBundle",
@@ -586,14 +580,14 @@ partial-json-fixer = "0.5.3"
 parse_int = "0.9"
 pciid-parser = "0.8.0"
 pathdiff = "0.2"
-pet = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" }
-pet-conda = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" }
-pet-core = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" }
-pet-fs = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" }
-pet-pixi = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" }
-pet-poetry = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" }
-pet-reporter = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" }
-pet-virtualenv = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" }
+pet = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "e97b9508befa0062929da65a01054d25c4be861c" }
+pet-conda = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "e97b9508befa0062929da65a01054d25c4be861c" }
+pet-core = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "e97b9508befa0062929da65a01054d25c4be861c" }
+pet-fs = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "e97b9508befa0062929da65a01054d25c4be861c" }
+pet-pixi = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "e97b9508befa0062929da65a01054d25c4be861c" }
+pet-poetry = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "e97b9508befa0062929da65a01054d25c4be861c" }
+pet-reporter = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "e97b9508befa0062929da65a01054d25c4be861c" }
+pet-virtualenv = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "e97b9508befa0062929da65a01054d25c4be861c" }
 portable-pty = "0.9.0"
 postage = { version = "0.5", features = ["futures-traits"] }
 pretty_assertions = { version = "1.3.0", features = ["unstable"] }
@@ -719,7 +713,6 @@ wasmtime-wasi = "29"
 which = "6.0.0"
 windows-core = "0.61"
 wit-component = "0.221"
-workspace-hack = "0.1.0"
 yawc = "0.2.5"
 zeroize = "1.8"
 zstd = "0.11"
@@ -780,9 +773,6 @@ notify = { git = "https://github.com/zed-industries/notify.git", rev = "bbb9ea5a
 notify-types = { git = "https://github.com/zed-industries/notify.git", rev = "bbb9ea5ae52b253e095737847e367c30653a2e96" }
 windows-capture = { git = "https://github.com/zed-industries/windows-capture.git", rev = "f0d6c1b6691db75461b732f6d5ff56eed002eeb9" }
 
-# Makes the workspace hack crate refer to the local one, but only when you're building locally
-workspace-hack = { path = "tooling/workspace-hack" }
-
 [profile.dev]
 split-debuginfo = "unpacked"
 codegen-units = 16
@@ -910,5 +900,5 @@ ignored = [
     "serde",
     "component",
     "documented",
-    "workspace-hack",
+    "sea-orm-macros",
 ]

README.md 🔗

@@ -9,7 +9,7 @@ Welcome to Zed, a high-performance, multiplayer code editor from the creators of
 
 ### Installation
 
-On macOS, Linux and Windows you can [download Zed directly](https://zed.dev/download) or [install Zed via your local package manager](https://zed.dev/docs/linux#installing-via-a-package-manager).
+On macOS, Linux, and Windows you can [download Zed directly](https://zed.dev/download) or [install Zed via your local package manager](https://zed.dev/docs/linux#installing-via-a-package-manager).
 
 Other platforms are not yet available:
 

assets/keymaps/default-linux.json 🔗

@@ -139,7 +139,7 @@
       "find": "buffer_search::Deploy",
       "ctrl-f": "buffer_search::Deploy",
       "ctrl-h": "buffer_search::DeployReplace",
-      "ctrl->": "agent::QuoteSelection",
+      "ctrl->": "agent::AddSelectionToThread",
       "ctrl-<": "assistant::InsertIntoEditor",
       "ctrl-alt-e": "editor::SelectEnclosingSymbol",
       "ctrl-shift-backspace": "editor::GoToPreviousChange",
@@ -243,7 +243,7 @@
       "ctrl-shift-i": "agent::ToggleOptionsMenu",
       "ctrl-alt-shift-n": "agent::ToggleNewThreadMenu",
       "shift-alt-escape": "agent::ExpandMessageEditor",
-      "ctrl->": "agent::QuoteSelection",
+      "ctrl->": "agent::AddSelectionToThread",
       "ctrl-alt-e": "agent::RemoveAllContext",
       "ctrl-shift-e": "project_panel::ToggleFocus",
       "ctrl-shift-enter": "agent::ContinueThread",
@@ -269,14 +269,14 @@
     }
   },
   {
-    "context": "AgentPanel && prompt_editor",
+    "context": "AgentPanel && text_thread",
     "bindings": {
       "ctrl-n": "agent::NewTextThread",
       "ctrl-alt-t": "agent::NewThread"
     }
   },
   {
-    "context": "AgentPanel && external_agent_thread",
+    "context": "AgentPanel && acp_thread",
     "use_key_equivalents": true,
     "bindings": {
       "ctrl-n": "agent::NewExternalAgentThread",
@@ -539,7 +539,7 @@
       "ctrl-k ctrl-0": "editor::FoldAll",
       "ctrl-k ctrl-j": "editor::UnfoldAll",
       "ctrl-space": "editor::ShowCompletions",
-      "ctrl-shift-space": "editor::ShowSignatureHelp",
+      "ctrl-shift-space": "editor::ShowWordCompletions",
       "ctrl-.": "editor::ToggleCodeActions",
       "ctrl-k r": "editor::RevealInFileManager",
       "ctrl-k p": "editor::CopyPath",
@@ -799,7 +799,7 @@
       "ctrl-shift-e": "pane::RevealInProjectPanel",
       "ctrl-f8": "editor::GoToHunk",
       "ctrl-shift-f8": "editor::GoToPreviousHunk",
-      "ctrl-i": "assistant::InlineAssist",
+      "ctrl-enter": "assistant::InlineAssist",
       "ctrl-:": "editor::ToggleInlayHints"
     }
   },
@@ -1080,7 +1080,8 @@
   {
     "context": "StashList || (StashList > Picker > Editor)",
     "bindings": {
-      "ctrl-shift-backspace": "stash_picker::DropStashItem"
+      "ctrl-shift-backspace": "stash_picker::DropStashItem",
+      "ctrl-shift-v": "stash_picker::ShowStashItem"
     }
   },
   {
@@ -1093,7 +1094,7 @@
       "paste": "terminal::Paste",
       "shift-insert": "terminal::Paste",
       "ctrl-shift-v": "terminal::Paste",
-      "ctrl-i": "assistant::InlineAssist",
+      "ctrl-enter": "assistant::InlineAssist",
       "alt-b": ["terminal::SendText", "\u001bb"],
       "alt-f": ["terminal::SendText", "\u001bf"],
       "alt-.": ["terminal::SendText", "\u001b."],
@@ -1266,12 +1267,22 @@
       "ctrl-pagedown": "settings_editor::FocusNextFile"
     }
   },
+  {
+    "context": "StashDiff > Editor",
+    "bindings": {
+      "ctrl-space": "git::ApplyCurrentStash",
+      "ctrl-shift-space": "git::PopCurrentStash",
+      "ctrl-shift-backspace": "git::DropCurrentStash"
+    }
+  },
   {
     "context": "SettingsWindow > NavigationMenu",
     "use_key_equivalents": true,
     "bindings": {
       "up": "settings_editor::FocusPreviousNavEntry",
+      "shift-tab": "settings_editor::FocusPreviousNavEntry",
       "down": "settings_editor::FocusNextNavEntry",
+      "tab": "settings_editor::FocusNextNavEntry",
       "right": "settings_editor::ExpandNavEntry",
       "left": "settings_editor::CollapseNavEntry",
       "pageup": "settings_editor::FocusPreviousRootNavEntry",
@@ -1279,5 +1290,13 @@
       "home": "settings_editor::FocusFirstNavEntry",
       "end": "settings_editor::FocusLastNavEntry"
     }
+  },
+  {
+    "context": "Zeta2Feedback > Editor",
+    "bindings": {
+      "enter": "editor::Newline",
+      "ctrl-enter up": "dev::Zeta2RatePredictionPositive",
+      "ctrl-enter down": "dev::Zeta2RatePredictionNegative"
+    }
   }
 ]

assets/keymaps/default-macos.json 🔗

@@ -142,7 +142,7 @@
       "cmd-\"": "editor::ExpandAllDiffHunks",
       "cmd-alt-g b": "git::Blame",
       "cmd-alt-g m": "git::OpenModifiedFiles",
-      "cmd-shift-space": "editor::ShowSignatureHelp",
+      "cmd-i": "editor::ShowSignatureHelp",
       "f9": "editor::ToggleBreakpoint",
       "shift-f9": "editor::EditLogBreakpoint",
       "ctrl-f12": "editor::GoToDeclaration",
@@ -163,7 +163,7 @@
       "cmd-alt-f": "buffer_search::DeployReplace",
       "cmd-alt-l": ["buffer_search::Deploy", { "selection_search_enabled": true }],
       "cmd-e": ["buffer_search::Deploy", { "focus": false }],
-      "cmd->": "agent::QuoteSelection",
+      "cmd->": "agent::AddSelectionToThread",
       "cmd-<": "assistant::InsertIntoEditor",
       "cmd-alt-e": "editor::SelectEnclosingSymbol",
       "alt-enter": "editor::OpenSelectionsInMultibuffer"
@@ -282,7 +282,7 @@
       "cmd-shift-i": "agent::ToggleOptionsMenu",
       "cmd-alt-shift-n": "agent::ToggleNewThreadMenu",
       "shift-alt-escape": "agent::ExpandMessageEditor",
-      "cmd->": "agent::QuoteSelection",
+      "cmd->": "agent::AddSelectionToThread",
       "cmd-alt-e": "agent::RemoveAllContext",
       "cmd-shift-e": "project_panel::ToggleFocus",
       "cmd-ctrl-b": "agent::ToggleBurnMode",
@@ -307,7 +307,7 @@
     }
   },
   {
-    "context": "AgentPanel && prompt_editor",
+    "context": "AgentPanel && text_thread",
     "use_key_equivalents": true,
     "bindings": {
       "cmd-n": "agent::NewTextThread",
@@ -315,7 +315,7 @@
     }
   },
   {
-    "context": "AgentPanel && external_agent_thread",
+    "context": "AgentPanel && acp_thread",
     "use_key_equivalents": true,
     "bindings": {
       "cmd-n": "agent::NewExternalAgentThread",
@@ -864,7 +864,7 @@
       "cmd-shift-e": "pane::RevealInProjectPanel",
       "cmd-f8": "editor::GoToHunk",
       "cmd-shift-f8": "editor::GoToPreviousHunk",
-      "cmd-i": "assistant::InlineAssist",
+      "ctrl-enter": "assistant::InlineAssist",
       "ctrl-:": "editor::ToggleInlayHints"
     }
   },
@@ -1153,7 +1153,8 @@
     "context": "StashList || (StashList > Picker > Editor)",
     "use_key_equivalents": true,
     "bindings": {
-      "ctrl-shift-backspace": "stash_picker::DropStashItem"
+      "ctrl-shift-backspace": "stash_picker::DropStashItem",
+      "ctrl-shift-v": "stash_picker::ShowStashItem"
     }
   },
   {
@@ -1167,7 +1168,7 @@
       "cmd-a": "editor::SelectAll",
       "cmd-k": "terminal::Clear",
       "cmd-n": "workspace::NewTerminal",
-      "cmd-i": "assistant::InlineAssist",
+      "ctrl-enter": "assistant::InlineAssist",
       "ctrl-_": null, // emacs undo
       // Some nice conveniences
       "cmd-backspace": ["terminal::SendText", "\u0015"], // ctrl-u: clear line
@@ -1371,12 +1372,23 @@
       "cmd-}": "settings_editor::FocusNextFile"
     }
   },
+  {
+    "context": "StashDiff > Editor",
+    "use_key_equivalents": true,
+    "bindings": {
+      "ctrl-space": "git::ApplyCurrentStash",
+      "ctrl-shift-space": "git::PopCurrentStash",
+      "ctrl-shift-backspace": "git::DropCurrentStash"
+    }
+  },
   {
     "context": "SettingsWindow > NavigationMenu",
     "use_key_equivalents": true,
     "bindings": {
       "up": "settings_editor::FocusPreviousNavEntry",
+      "shift-tab": "settings_editor::FocusPreviousNavEntry",
       "down": "settings_editor::FocusNextNavEntry",
+      "tab": "settings_editor::FocusNextNavEntry",
       "right": "settings_editor::ExpandNavEntry",
       "left": "settings_editor::CollapseNavEntry",
       "pageup": "settings_editor::FocusPreviousRootNavEntry",
@@ -1384,5 +1396,13 @@
       "home": "settings_editor::FocusFirstNavEntry",
       "end": "settings_editor::FocusLastNavEntry"
     }
+  },
+  {
+    "context": "Zeta2Feedback > Editor",
+    "bindings": {
+      "enter": "editor::Newline",
+      "cmd-enter up": "dev::Zeta2RatePredictionPositive",
+      "cmd-enter down": "dev::Zeta2RatePredictionNegative"
+    }
   }
 ]

assets/keymaps/default-windows.json 🔗

@@ -134,7 +134,7 @@
       "ctrl-k z": "editor::ToggleSoftWrap",
       "ctrl-f": "buffer_search::Deploy",
       "ctrl-h": "buffer_search::DeployReplace",
-      "ctrl-shift-.": "agent::QuoteSelection",
+      "ctrl-shift-.": "agent::AddSelectionToThread",
       "ctrl-shift-,": "assistant::InsertIntoEditor",
       "shift-alt-e": "editor::SelectEnclosingSymbol",
       "ctrl-shift-backspace": "editor::GoToPreviousChange",
@@ -244,7 +244,7 @@
       "ctrl-shift-i": "agent::ToggleOptionsMenu",
       // "ctrl-shift-alt-n": "agent::ToggleNewThreadMenu",
       "shift-alt-escape": "agent::ExpandMessageEditor",
-      "ctrl-shift-.": "agent::QuoteSelection",
+      "ctrl-shift-.": "agent::AddSelectionToThread",
       "shift-alt-e": "agent::RemoveAllContext",
       "ctrl-shift-e": "project_panel::ToggleFocus",
       "ctrl-shift-enter": "agent::ContinueThread",
@@ -270,7 +270,7 @@
     }
   },
   {
-    "context": "AgentPanel && prompt_editor",
+    "context": "AgentPanel && text_thread",
     "use_key_equivalents": true,
     "bindings": {
       "ctrl-n": "agent::NewTextThread",
@@ -278,7 +278,7 @@
     }
   },
   {
-    "context": "AgentPanel && external_agent_thread",
+    "context": "AgentPanel && acp_thread",
     "use_key_equivalents": true,
     "bindings": {
       "ctrl-n": "agent::NewExternalAgentThread",
@@ -548,7 +548,7 @@
       "ctrl-k ctrl-0": "editor::FoldAll",
       "ctrl-k ctrl-j": "editor::UnfoldAll",
       "ctrl-space": "editor::ShowCompletions",
-      "ctrl-shift-space": "editor::ShowSignatureHelp",
+      "ctrl-shift-space": "editor::ShowWordCompletions",
       "ctrl-.": "editor::ToggleCodeActions",
       "ctrl-k r": "editor::RevealInFileManager",
       "ctrl-k p": "editor::CopyPath",
@@ -812,7 +812,7 @@
       "ctrl-shift-e": "pane::RevealInProjectPanel",
       "ctrl-f8": "editor::GoToHunk",
       "ctrl-shift-f8": "editor::GoToPreviousHunk",
-      "ctrl-i": "assistant::InlineAssist",
+      "ctrl-enter": "assistant::InlineAssist",
       "ctrl-shift-;": "editor::ToggleInlayHints"
     }
   },
@@ -1106,7 +1106,8 @@
     "context": "StashList || (StashList > Picker > Editor)",
     "use_key_equivalents": true,
     "bindings": {
-      "ctrl-shift-backspace": "stash_picker::DropStashItem"
+      "ctrl-shift-backspace": "stash_picker::DropStashItem",
+      "ctrl-shift-v": "stash_picker::ShowStashItem"
     }
   },
   {
@@ -1119,7 +1120,7 @@
       "shift-insert": "terminal::Paste",
       "ctrl-v": "terminal::Paste",
       "ctrl-shift-v": "terminal::Paste",
-      "ctrl-i": "assistant::InlineAssist",
+      "ctrl-enter": "assistant::InlineAssist",
       "alt-b": ["terminal::SendText", "\u001bb"],
       "alt-f": ["terminal::SendText", "\u001bf"],
       "alt-.": ["terminal::SendText", "\u001b."],
@@ -1294,12 +1295,23 @@
       "ctrl-pagedown": "settings_editor::FocusNextFile"
     }
   },
+  {
+    "context": "StashDiff > Editor",
+    "use_key_equivalents": true,
+    "bindings": {
+      "ctrl-space": "git::ApplyCurrentStash",
+      "ctrl-shift-space": "git::PopCurrentStash",
+      "ctrl-shift-backspace": "git::DropCurrentStash"
+    }
+  },
   {
     "context": "SettingsWindow > NavigationMenu",
     "use_key_equivalents": true,
     "bindings": {
       "up": "settings_editor::FocusPreviousNavEntry",
+      "shift-tab": "settings_editor::FocusPreviousNavEntry",
       "down": "settings_editor::FocusNextNavEntry",
+      "tab": "settings_editor::FocusNextNavEntry",
       "right": "settings_editor::ExpandNavEntry",
       "left": "settings_editor::CollapseNavEntry",
       "pageup": "settings_editor::FocusPreviousRootNavEntry",
@@ -1307,5 +1319,13 @@
       "home": "settings_editor::FocusFirstNavEntry",
       "end": "settings_editor::FocusLastNavEntry"
     }
+  },
+  {
+    "context": "Zeta2Feedback > Editor",
+    "bindings": {
+      "enter": "editor::Newline",
+      "ctrl-enter up": "dev::Zeta2RatePredictionPositive",
+      "ctrl-enter down": "dev::Zeta2RatePredictionNegative"
+    }
   }
 ]

assets/keymaps/linux/cursor.json 🔗

@@ -17,8 +17,8 @@
     "bindings": {
       "ctrl-i": "agent::ToggleFocus",
       "ctrl-shift-i": "agent::ToggleFocus",
-      "ctrl-shift-l": "agent::QuoteSelection", // In cursor uses "Ask" mode
-      "ctrl-l": "agent::QuoteSelection", // In cursor uses "Agent" mode
+      "ctrl-shift-l": "agent::AddSelectionToThread", // In cursor uses "Ask" mode
+      "ctrl-l": "agent::AddSelectionToThread", // In cursor uses "Agent" mode
       "ctrl-k": "assistant::InlineAssist",
       "ctrl-shift-k": "assistant::InsertIntoEditor"
     }

assets/keymaps/linux/emacs.json 🔗

@@ -8,13 +8,23 @@
       "ctrl-g": "menu::Cancel"
     }
   },
+  {
+    // Workaround to avoid falling back to default bindings.
+    // Unbind so Zed ignores these keys and lets emacs handle them.
+    // NOTE: must be declared before the `Editor` override.
+    // NOTE: in macos the 'ctrl-x' 'ctrl-p' and 'ctrl-n' rebindings are not needed, since they default to 'cmd'.
+    "context": "Editor",
+    "bindings": {
+      "ctrl-g": null, // currently activates `go_to_line::Toggle` when there is nothing to cancel
+      "ctrl-x": null, // currently activates `editor::Cut` if no following key is pressed for 1 second
+      "ctrl-p": null, // currently activates `file_finder::Toggle` when the cursor is on the first character of the buffer
+      "ctrl-n": null // currently activates `workspace::NewFile` when the cursor is on the last character of the buffer
+    }
+  },
   {
     "context": "Editor",
     "bindings": {
-      "alt-x": "command_palette::Toggle",
       "ctrl-g": "editor::Cancel",
-      "ctrl-x b": "tab_switcher::Toggle", // switch-to-buffer
-      "ctrl-x ctrl-b": "tab_switcher::Toggle", // list-buffers
       "alt-g g": "go_to_line::Toggle", // goto-line
       "alt-g alt-g": "go_to_line::Toggle", // goto-line
       "ctrl-space": "editor::SetMark", // set-mark
@@ -33,8 +43,8 @@
       "alt-m": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": false, "stop_at_indent": true }], // back-to-indentation
       "alt-left": "editor::MoveToPreviousWordStart", // left-word
       "alt-right": "editor::MoveToNextWordEnd", // right-word
-      "alt-f": "editor::MoveToNextSubwordEnd", // forward-word
-      "alt-b": "editor::MoveToPreviousSubwordStart", // backward-word
+      "alt-f": "editor::MoveToNextWordEnd", // forward-word
+      "alt-b": "editor::MoveToPreviousWordStart", // backward-word
       "alt-u": "editor::ConvertToUpperCase", // upcase-word
       "alt-l": "editor::ConvertToLowerCase", // downcase-word
       "alt-c": "editor::ConvertToUpperCamelCase", // capitalize-word
@@ -98,7 +108,7 @@
       "ctrl-e": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": false }],
       "alt-m": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": false, "stop_at_indent": true }],
       "alt-f": "editor::SelectToNextWordEnd",
-      "alt-b": "editor::SelectToPreviousSubwordStart",
+      "alt-b": "editor::SelectToPreviousWordStart",
       "alt-{": "editor::SelectToStartOfParagraph",
       "alt-}": "editor::SelectToEndOfParagraph",
       "ctrl-up": "editor::SelectToStartOfParagraph",
@@ -126,15 +136,28 @@
       "ctrl-n": "editor::SignatureHelpNext"
     }
   },
+  // Example setting for using emacs-style tab
+  // (i.e. indent the current line / selection or perform symbol completion depending on context)
+  // {
+  //   "context": "Editor && !showing_code_actions && !showing_completions",
+  //   "bindings": {
+  //     "tab": "editor::AutoIndent" // indent-for-tab-command
+  //   }
+  // },
   {
     "context": "Workspace",
     "bindings": {
+      "alt-x": "command_palette::Toggle", // execute-extended-command
+      "ctrl-x b": "tab_switcher::Toggle", // switch-to-buffer
+      "ctrl-x ctrl-b": "tab_switcher::Toggle", // list-buffers
+      // "ctrl-x ctrl-c": "workspace::CloseWindow" // in case you only want to exit the current Zed instance
       "ctrl-x ctrl-c": "zed::Quit", // save-buffers-kill-terminal
       "ctrl-x 5 0": "workspace::CloseWindow", // delete-frame
       "ctrl-x 5 2": "workspace::NewWindow", // make-frame-command
       "ctrl-x o": "workspace::ActivateNextPane", // other-window
       "ctrl-x k": "pane::CloseActiveItem", // kill-buffer
       "ctrl-x 0": "pane::CloseActiveItem", // delete-window
+      // "ctrl-x 1": "pane::JoinAll", // in case you prefer to delete the splits but keep the buffers open
       "ctrl-x 1": "pane::CloseOtherItems", // delete-other-windows
       "ctrl-x 2": "pane::SplitDown", // split-window-below
       "ctrl-x 3": "pane::SplitRight", // split-window-right
@@ -145,10 +168,19 @@
     }
   },
   {
-    // Workaround to enable using emacs in the Zed terminal.
+    // Workaround to enable using native emacs from the Zed terminal.
     // Unbind so Zed ignores these keys and lets emacs handle them.
+    // NOTE:
+    //  "terminal::SendKeystroke" only works for a single key stroke (e.g. ctrl-x),
+    //  so override with null for compound sequences (e.g. ctrl-x ctrl-c).
     "context": "Terminal",
     "bindings": {
+      // If you want to perfect your emacs-in-zed setup, also consider the following.
+      // You may need to enable "option_as_meta" from the Zed settings for "alt-x" to work.
+      // "alt-x": ["terminal::SendKeystroke", "alt-x"],
+      // "ctrl-x": ["terminal::SendKeystroke", "ctrl-x"],
+      // "ctrl-n": ["terminal::SendKeystroke", "ctrl-n"],
+      // ...
       "ctrl-x ctrl-c": null, // save-buffers-kill-terminal
       "ctrl-x ctrl-f": null, // find-file
       "ctrl-x ctrl-s": null, // save-buffer

assets/keymaps/macos/cursor.json 🔗

@@ -17,8 +17,8 @@
     "bindings": {
       "cmd-i": "agent::ToggleFocus",
       "cmd-shift-i": "agent::ToggleFocus",
-      "cmd-shift-l": "agent::QuoteSelection", // In cursor uses "Ask" mode
-      "cmd-l": "agent::QuoteSelection", // In cursor uses "Agent" mode
+      "cmd-shift-l": "agent::AddSelectionToThread", // In cursor uses "Ask" mode
+      "cmd-l": "agent::AddSelectionToThread", // In cursor uses "Agent" mode
       "cmd-k": "assistant::InlineAssist",
       "cmd-shift-k": "assistant::InsertIntoEditor"
     }

assets/keymaps/macos/emacs.json 🔗

@@ -9,13 +9,19 @@
       "ctrl-g": "menu::Cancel"
     }
   },
+  {
+    // Workaround to avoid falling back to default bindings.
+    // Unbind so Zed ignores these keys and lets emacs handle them.
+    // NOTE: must be declared before the `Editor` override.
+    "context": "Editor",
+    "bindings": {
+      "ctrl-g": null // currently activates `go_to_line::Toggle` when there is nothing to cancel
+    }
+  },
   {
     "context": "Editor",
     "bindings": {
-      "alt-x": "command_palette::Toggle",
       "ctrl-g": "editor::Cancel",
-      "ctrl-x b": "tab_switcher::Toggle", // switch-to-buffer
-      "ctrl-x ctrl-b": "tab_switcher::Toggle", // list-buffers
       "alt-g g": "go_to_line::Toggle", // goto-line
       "alt-g alt-g": "go_to_line::Toggle", // goto-line
       "ctrl-space": "editor::SetMark", // set-mark
@@ -34,8 +40,8 @@
       "alt-m": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": false, "stop_at_indent": true }], // back-to-indentation
       "alt-left": "editor::MoveToPreviousWordStart", // left-word
       "alt-right": "editor::MoveToNextWordEnd", // right-word
-      "alt-f": "editor::MoveToNextSubwordEnd", // forward-word
-      "alt-b": "editor::MoveToPreviousSubwordStart", // backward-word
+      "alt-f": "editor::MoveToNextWordEnd", // forward-word
+      "alt-b": "editor::MoveToPreviousWordStart", // backward-word
       "alt-u": "editor::ConvertToUpperCase", // upcase-word
       "alt-l": "editor::ConvertToLowerCase", // downcase-word
       "alt-c": "editor::ConvertToUpperCamelCase", // capitalize-word
@@ -99,7 +105,7 @@
       "ctrl-e": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": false }],
       "alt-m": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": false, "stop_at_indent": true }],
       "alt-f": "editor::SelectToNextWordEnd",
-      "alt-b": "editor::SelectToPreviousSubwordStart",
+      "alt-b": "editor::SelectToPreviousWordStart",
       "alt-{": "editor::SelectToStartOfParagraph",
       "alt-}": "editor::SelectToEndOfParagraph",
       "ctrl-up": "editor::SelectToStartOfParagraph",
@@ -127,15 +133,28 @@
       "ctrl-n": "editor::SignatureHelpNext"
     }
   },
+  // Example setting for using emacs-style tab
+  // (i.e. indent the current line / selection or perform symbol completion depending on context)
+  // {
+  //   "context": "Editor && !showing_code_actions && !showing_completions",
+  //   "bindings": {
+  //     "tab": "editor::AutoIndent" // indent-for-tab-command
+  //   }
+  // },
   {
     "context": "Workspace",
     "bindings": {
+      "alt-x": "command_palette::Toggle", // execute-extended-command
+      "ctrl-x b": "tab_switcher::Toggle", // switch-to-buffer
+      "ctrl-x ctrl-b": "tab_switcher::Toggle", // list-buffers
+      // "ctrl-x ctrl-c": "workspace::CloseWindow" // in case you only want to exit the current Zed instance
       "ctrl-x ctrl-c": "zed::Quit", // save-buffers-kill-terminal
       "ctrl-x 5 0": "workspace::CloseWindow", // delete-frame
       "ctrl-x 5 2": "workspace::NewWindow", // make-frame-command
       "ctrl-x o": "workspace::ActivateNextPane", // other-window
       "ctrl-x k": "pane::CloseActiveItem", // kill-buffer
       "ctrl-x 0": "pane::CloseActiveItem", // delete-window
+      // "ctrl-x 1": "pane::JoinAll", // in case you prefer to delete the splits but keep the buffers open
       "ctrl-x 1": "pane::CloseOtherItems", // delete-other-windows
       "ctrl-x 2": "pane::SplitDown", // split-window-below
       "ctrl-x 3": "pane::SplitRight", // split-window-right
@@ -146,10 +165,19 @@
     }
   },
   {
-    // Workaround to enable using emacs in the Zed terminal.
+    // Workaround to enable using native emacs from the Zed terminal.
     // Unbind so Zed ignores these keys and lets emacs handle them.
+    // NOTE:
+    //  "terminal::SendKeystroke" only works for a single key stroke (e.g. ctrl-x),
+    //  so override with null for compound sequences (e.g. ctrl-x ctrl-c).
     "context": "Terminal",
     "bindings": {
+      // If you want to perfect your emacs-in-zed setup, also consider the following.
+      // You may need to enable "option_as_meta" from the Zed settings for "alt-x" to work.
+      // "alt-x": ["terminal::SendKeystroke", "alt-x"],
+      // "ctrl-x": ["terminal::SendKeystroke", "ctrl-x"],
+      // "ctrl-n": ["terminal::SendKeystroke", "ctrl-n"],
+      // ...
       "ctrl-x ctrl-c": null, // save-buffers-kill-terminal
       "ctrl-x ctrl-f": null, // find-file
       "ctrl-x ctrl-s": null, // save-buffer

assets/keymaps/vim.json 🔗

@@ -422,56 +422,66 @@
   {
     "context": "(vim_mode == helix_normal || vim_mode == helix_select) && !menu",
     "bindings": {
-      ";": "vim::HelixCollapseSelection",
-      ":": "command_palette::Toggle",
-      "m": "vim::PushHelixMatch",
-      "s": "vim::HelixSelectRegex",
-      "]": ["vim::PushHelixNext", { "around": true }],
-      "[": ["vim::PushHelixPrevious", { "around": true }],
-      "left": "vim::WrappingLeft",
-      "right": "vim::WrappingRight",
+      // Movement
       "h": "vim::WrappingLeft",
+      "left": "vim::WrappingLeft",
       "l": "vim::WrappingRight",
-      "y": "vim::HelixYank",
-      "p": "vim::HelixPaste",
-      "shift-p": ["vim::HelixPaste", { "before": true }],
-      "alt-;": "vim::OtherEnd",
-      "ctrl-r": "vim::Redo",
-      "f": ["vim::PushFindForward", { "before": false, "multiline": true }],
+      "right": "vim::WrappingRight",
       "t": ["vim::PushFindForward", { "before": true, "multiline": true }],
-      "shift-f": ["vim::PushFindBackward", { "after": false, "multiline": true }],
+      "f": ["vim::PushFindForward", { "before": false, "multiline": true }],
       "shift-t": ["vim::PushFindBackward", { "after": true, "multiline": true }],
-      ">": "vim::Indent",
-      "<": "vim::Outdent",
-      "=": "vim::AutoIndent",
+      "shift-f": ["vim::PushFindBackward", { "after": false, "multiline": true }],
+      "alt-.": "vim::RepeatFind",
+      
+      // Changes
+      "shift-r": "editor::Paste",
       "`": "vim::ConvertToLowerCase",
       "alt-`": "vim::ConvertToUpperCase",
-      "g q": "vim::PushRewrap",
-      "g w": "vim::PushRewrap",
       "insert": "vim::InsertBefore",
-      "alt-.": "vim::RepeatFind",
+      "shift-u": "editor::Redo",
+      "ctrl-r": "vim::Redo",
+      "y": "vim::HelixYank",
+      "p": "vim::HelixPaste",
+      "shift-p": ["vim::HelixPaste", { "before": true }],            
+      ">": "vim::Indent",
+      "<": "vim::Outdent",
+      "=": "vim::AutoIndent",
+      "d": "vim::HelixDelete",
+      "c": "vim::HelixSubstitute",
+      "alt-c": "vim::HelixSubstituteNoYank",
+      
+      // Selection manipulation
+      "s": "vim::HelixSelectRegex",
       "alt-s": ["editor::SplitSelectionIntoLines", { "keep_selections": true }],
+      ";": "vim::HelixCollapseSelection",
+      "alt-;": "vim::OtherEnd",
+      ",": "vim::HelixKeepNewestSelection",
+      "shift-c": "vim::HelixDuplicateBelow",
+      "alt-shift-c": "vim::HelixDuplicateAbove",
+      "%": "editor::SelectAll",
+      "x": "vim::HelixSelectLine",
+      "shift-x": "editor::SelectLine",
+      "ctrl-c": "editor::ToggleComments",
+      "alt-o": "editor::SelectLargerSyntaxNode",
+      "alt-i": "editor::SelectSmallerSyntaxNode",
+      "alt-p": "editor::SelectPreviousSyntaxNode",
+      "alt-n": "editor::SelectNextSyntaxNode",
+      
       // Goto mode
-      "g n": "pane::ActivateNextItem",
-      "g p": "pane::ActivatePreviousItem",
-      // "tab": "pane::ActivateNextItem",
-      // "shift-tab": "pane::ActivatePrevItem",
-      "shift-h": "pane::ActivatePreviousItem",
-      "shift-l": "pane::ActivateNextItem",
-      "g l": "vim::EndOfLine",
+      "g e": "vim::EndOfDocument",
       "g h": "vim::StartOfLine",
+      "g l": "vim::EndOfLine",
       "g s": "vim::FirstNonWhitespace", // "g s" default behavior is "space s"
-      "g e": "vim::EndOfDocument",
-      "g .": "vim::HelixGotoLastModification", // go to last modification
-      "g r": "editor::FindAllReferences", // zed specific
       "g t": "vim::WindowTop",
       "g c": "vim::WindowMiddle",
       "g b": "vim::WindowBottom",
-
-      "shift-r": "editor::Paste",
-      "x": "vim::HelixSelectLine",
-      "shift-x": "editor::SelectLine",
-      "%": "editor::SelectAll",
+      "g r": "editor::FindAllReferences", // zed specific
+      "g n": "pane::ActivateNextItem",
+      "shift-l": "pane::ActivateNextItem",      
+      "g p": "pane::ActivatePreviousItem",
+      "shift-h": "pane::ActivatePreviousItem",
+      "g .": "vim::HelixGotoLastModification", // go to last modification
+      
       // Window mode
       "space w h": "workspace::ActivatePaneLeft",
       "space w l": "workspace::ActivatePaneRight",
@@ -482,6 +492,7 @@
       "space w r": "pane::SplitRight",
       "space w v": "pane::SplitDown",
       "space w d": "pane::SplitDown",
+
       // Space mode
       "space f": "file_finder::Toggle",
       "space k": "editor::Hover",
@@ -492,16 +503,18 @@
       "space a": "editor::ToggleCodeActions",
       "space h": "editor::SelectAllMatches",
       "space c": "editor::ToggleComments",
-      "space y": "editor::Copy",
       "space p": "editor::Paste",
-      "shift-u": "editor::Redo",
-      "ctrl-c": "editor::ToggleComments",
-      "d": "vim::HelixDelete",
-      "c": "vim::HelixSubstitute",
-      "alt-c": "vim::HelixSubstituteNoYank",
-      "shift-c": "vim::HelixDuplicateBelow",
-      "alt-shift-c": "vim::HelixDuplicateAbove",
-      ",": "vim::HelixKeepNewestSelection"
+      "space y": "editor::Copy",
+
+      // Other
+      ":": "command_palette::Toggle",
+      "m": "vim::PushHelixMatch",
+      "]": ["vim::PushHelixNext", { "around": true }],
+      "[": ["vim::PushHelixPrevious", { "around": true }],
+      "g q": "vim::PushRewrap",
+      "g w": "vim::PushRewrap",
+      // "tab": "pane::ActivateNextItem",
+      // "shift-tab": "pane::ActivatePrevItem",
     }
   },
   {
@@ -970,7 +983,9 @@
     "bindings": {
       "ctrl-h": "editor::Backspace",
       "ctrl-u": "editor::DeleteToBeginningOfLine",
-      "ctrl-w": "editor::DeleteToPreviousWordStart"
+      "ctrl-w": "editor::DeleteToPreviousWordStart",
+      "ctrl-p": "menu::SelectPrevious",
+      "ctrl-n": "menu::SelectNext"
     }
   },
   {

assets/settings/default.json 🔗

@@ -1,8 +1,8 @@
 {
   "$schema": "zed://schemas/settings",
-  /// The displayed name of this project. If not set or empty, the root directory name
+  /// The displayed name of this project. If not set or null, the root directory name
   /// will be displayed.
-  "project_name": "",
+  "project_name": null,
   // The name of the Zed theme to use for the UI.
   //
   // `mode` is one of:
@@ -311,11 +311,11 @@
   "use_on_type_format": true,
   // Whether to automatically add matching closing characters when typing
   // opening parenthesis, bracket, brace, single or double quote characters.
-  // For example, when you type (, Zed will add a closing ) at the correct position.
+  // For example, when you type '(', Zed will add a closing ) at the correct position.
   "use_autoclose": true,
   // Whether to automatically surround selected text when typing opening parenthesis,
   // bracket, brace, single or double quote characters.
-  // For example, when you select text and type (, Zed will surround the text with ().
+  // For example, when you select text and type '(', Zed will surround the text with ().
   "use_auto_surround": true,
   // Whether indentation should be adjusted based on the context whilst typing.
   "auto_indent": true,
@@ -884,8 +884,6 @@
     // Note: This setting has no effect on external agents that support permission modes, such as Claude Code.
     //       You can set `agent_servers.claude.default_mode` to `bypassPermissions` to skip all permission requests.
     "always_allow_tool_actions": false,
-    // When enabled, the agent will stream edits.
-    "stream_edits": false,
     // When enabled, agent edits will be displayed in single-file editors for review
     "single_file_review": true,
     // When enabled, show voting thumbs for feedback on agent edits.
@@ -1093,10 +1091,10 @@
     // Only the file Zed had indexed will be used, not necessary all the gitignored files.
     //
     // Can accept 3 values:
-    //   * `true`: Use all gitignored files
-    //   * `false`: Use only the files Zed had indexed
-    //   * `null`: Be smart and search for ignored when called from a gitignored worktree
-    "include_ignored": null
+    //   * "all": Use all gitignored files
+    //   * "indexed": Use only the files Zed had indexed
+    //   * "smart": Be smart and search for ignored when called from a gitignored worktree
+    "include_ignored": "smart"
   },
   // Whether or not to remove any trailing whitespace from lines of a buffer
   // before saving it.
@@ -1352,7 +1350,9 @@
     // Whether to show the active language button in the status bar.
     "active_language_button": true,
     // Whether to show the cursor position button in the status bar.
-    "cursor_position_button": true
+    "cursor_position_button": true,
+    // Whether to show active line endings button in the status bar.
+    "line_endings_button": false
   },
   // Settings specific to the terminal
   "terminal": {
@@ -1559,6 +1559,7 @@
   //
   "file_types": {
     "JSONC": ["**/.zed/**/*.json", "**/zed/**/*.json", "**/Zed/**/*.json", "**/.vscode/**/*.json", "tsconfig*.json"],
+    "Markdown": [".rules", ".cursorrules", ".windsurfrules", ".clinerules"],
     "Shell Script": [".env.*"]
   },
   // Settings for which version of Node.js and NPM to use when installing
@@ -1740,7 +1741,7 @@
       }
     },
     "Kotlin": {
-      "language_servers": ["kotlin-language-server", "!kotlin-lsp", "..."]
+      "language_servers": ["!kotlin-language-server", "kotlin-lsp", "..."]
     },
     "LaTeX": {
       "formatter": "language_server",
@@ -1776,7 +1777,8 @@
           "name": "ruff"
         }
       },
-      "debuggers": ["Debugpy"]
+      "debuggers": ["Debugpy"],
+      "language_servers": ["basedpyright", "ruff", "!ty", "!pyrefly", "!pyright", "!pylsp", "..."]
     },
     "Ruby": {
       "language_servers": ["solargraph", "!ruby-lsp", "!rubocop", "!sorbet", "!steep", "..."]
@@ -1818,10 +1820,11 @@
     },
     "SystemVerilog": {
       "format_on_save": "off",
+      "language_servers": ["!slang", "..."],
       "use_on_type_format": false
     },
     "Vue.js": {
-      "language_servers": ["vue-language-server", "..."],
+      "language_servers": ["vue-language-server", "vtsls", "..."],
       "prettier": {
         "allowed": true
       }
@@ -1925,6 +1928,11 @@
   // DAP Specific settings.
   "dap": {
     // Specify the DAP name as a key here.
+    "CodeLLDB": {
+      "env": {
+        "RUST_LOG": "info"
+      }
+    }
   },
   // Common language server settings.
   "global_lsp_settings": {

assets/themes/gruvbox/gruvbox.json 🔗

@@ -49,8 +49,9 @@
         "panel.background": "#3a3735ff",
         "panel.focused_border": "#83a598ff",
         "pane.focused_border": null,
-        "scrollbar.thumb.background": "#fbf1c74c",
-        "scrollbar.thumb.hover_background": "#494340ff",
+        "scrollbar.thumb.active_background": "#83a598ac",
+        "scrollbar.thumb.hover_background": "#fbf1c74c",
+        "scrollbar.thumb.background": "#a899844c",
         "scrollbar.thumb.border": "#494340ff",
         "scrollbar.track.background": "#00000000",
         "scrollbar.track.border": "#373432ff",
@@ -454,8 +455,9 @@
         "panel.background": "#393634ff",
         "panel.focused_border": "#83a598ff",
         "pane.focused_border": null,
-        "scrollbar.thumb.background": "#fbf1c74c",
-        "scrollbar.thumb.hover_background": "#494340ff",
+        "scrollbar.thumb.active_background": "#83a598ac",
+        "scrollbar.thumb.hover_background": "#fbf1c74c",
+        "scrollbar.thumb.background": "#a899844c",
         "scrollbar.thumb.border": "#494340ff",
         "scrollbar.track.background": "#00000000",
         "scrollbar.track.border": "#343130ff",
@@ -859,8 +861,9 @@
         "panel.background": "#3b3735ff",
         "panel.focused_border": null,
         "pane.focused_border": null,
-        "scrollbar.thumb.background": "#fbf1c74c",
-        "scrollbar.thumb.hover_background": "#494340ff",
+        "scrollbar.thumb.active_background": "#83a598ac",
+        "scrollbar.thumb.hover_background": "#fbf1c74c",
+        "scrollbar.thumb.background": "#a899844c",
         "scrollbar.thumb.border": "#494340ff",
         "scrollbar.track.background": "#00000000",
         "scrollbar.track.border": "#393634ff",
@@ -1264,8 +1267,9 @@
         "panel.background": "#ecddb4ff",
         "panel.focused_border": null,
         "pane.focused_border": null,
-        "scrollbar.thumb.background": "#2828284c",
-        "scrollbar.thumb.hover_background": "#ddcca7ff",
+        "scrollbar.thumb.active_background": "#458588ac",
+        "scrollbar.thumb.hover_background": "#2828284c",
+        "scrollbar.thumb.background": "#7c6f644c",
         "scrollbar.thumb.border": "#ddcca7ff",
         "scrollbar.track.background": "#00000000",
         "scrollbar.track.border": "#eee0b7ff",
@@ -1669,8 +1673,9 @@
         "panel.background": "#ecddb5ff",
         "panel.focused_border": null,
         "pane.focused_border": null,
-        "scrollbar.thumb.background": "#2828284c",
-        "scrollbar.thumb.hover_background": "#ddcca7ff",
+        "scrollbar.thumb.active_background": "#458588ac",
+        "scrollbar.thumb.hover_background": "#2828284c",
+        "scrollbar.thumb.background": "#7c6f644c",
         "scrollbar.thumb.border": "#ddcca7ff",
         "scrollbar.track.background": "#00000000",
         "scrollbar.track.border": "#eee1bbff",
@@ -2074,8 +2079,9 @@
         "panel.background": "#ecdcb3ff",
         "panel.focused_border": null,
         "pane.focused_border": null,
-        "scrollbar.thumb.background": "#2828284c",
-        "scrollbar.thumb.hover_background": "#ddcca7ff",
+        "scrollbar.thumb.active_background": "#458588ac",
+        "scrollbar.thumb.hover_background": "#2828284c",
+        "scrollbar.thumb.background": "#7c6f644c",
         "scrollbar.thumb.border": "#ddcca7ff",
         "scrollbar.track.background": "#00000000",
         "scrollbar.track.border": "#eddeb5ff",

clippy.toml 🔗

@@ -3,7 +3,7 @@ avoid-breaking-exported-api = false
 ignore-interior-mutability = [
     # Suppresses clippy::mutable_key_type, which is a false positive as the Eq
     # and Hash impls do not use fields with interior mutability.
-    "agent::context::AgentContextKey"
+    "agent_ui::context::AgentContextKey"
 ]
 disallowed-methods = [
     { path = "std::process::Command::spawn", reason = "Spawning `std::process::Command` can block the current thread for an unknown duration", replacement = "smol::process::Command::spawn" },

crates/acp_thread/Cargo.toml 🔗

@@ -45,7 +45,6 @@ url.workspace = true
 util.workspace = true
 uuid.workspace = true
 watch.workspace = true
-workspace-hack.workspace = true
 
 [dev-dependencies]
 env_logger.workspace = true

crates/acp_thread/src/acp_thread.rs 🔗

@@ -328,7 +328,7 @@ impl ToolCall {
         location: acp::ToolCallLocation,
         project: WeakEntity<Project>,
         cx: &mut AsyncApp,
-    ) -> Option<AgentLocation> {
+    ) -> Option<ResolvedLocation> {
         let buffer = project
             .update(cx, |project, cx| {
                 project
@@ -350,17 +350,14 @@ impl ToolCall {
             })
             .ok()?;
 
-        Some(AgentLocation {
-            buffer: buffer.downgrade(),
-            position,
-        })
+        Some(ResolvedLocation { buffer, position })
     }
 
     fn resolve_locations(
         &self,
         project: Entity<Project>,
         cx: &mut App,
-    ) -> Task<Vec<Option<AgentLocation>>> {
+    ) -> Task<Vec<Option<ResolvedLocation>>> {
         let locations = self.locations.clone();
         project.update(cx, |_, cx| {
             cx.spawn(async move |project, cx| {
@@ -374,6 +371,23 @@ impl ToolCall {
     }
 }
 
+// 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<Buffer>,
+    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
@@ -1393,35 +1407,46 @@ impl AcpThread {
         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| {
-                        if let Some(agent_location) = project.agent_location() {
-                            let should_ignore = agent_location.buffer == location.buffer
-                                && location
-                                    .buffer
-                                    .update(cx, |buffer, _| {
-                                        let snapshot = buffer.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
-                                    })
-                                    .ok()
-                                    .unwrap_or_default();
-                            if !should_ignore {
-                                project.set_agent_location(Some(location.clone()), 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::<Vec<_>>();
+
                 if tool_call.resolved_locations != resolved_locations {
                     tool_call.resolved_locations = resolved_locations;
                     cx.emit(AcpThreadEvent::EntryUpdated(ix));

crates/acp_thread/src/diff.rs 🔗

@@ -236,21 +236,21 @@ impl PendingDiff {
     fn finalize(&self, cx: &mut Context<Diff>) -> FinalizedDiff {
         let ranges = self.excerpt_ranges(cx);
         let base_text = self.base_text.clone();
-        let language_registry = self.new_buffer.read(cx).language_registry();
+        let new_buffer = self.new_buffer.read(cx);
+        let language_registry = new_buffer.language_registry();
 
-        let path = self
-            .new_buffer
-            .read(cx)
+        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(
-                0,
+                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(),

crates/acp_thread/src/terminal.rs 🔗

@@ -1,10 +1,15 @@
 use agent_client_protocol as acp;
-
+use anyhow::Result;
 use futures::{FutureExt as _, future::Shared};
-use gpui::{App, AppContext, Context, Entity, Task};
+use gpui::{App, AppContext, AsyncApp, Context, Entity, Task};
 use language::LanguageRegistry;
 use markdown::Markdown;
+use project::Project;
+use settings::{Settings as _, SettingsLocation};
 use std::{path::PathBuf, process::ExitStatus, sync::Arc, time::Instant};
+use task::Shell;
+use terminal::terminal_settings::TerminalSettings;
+use util::get_default_system_shell_preferring_bash;
 
 pub struct Terminal {
     id: acp::TerminalId,
@@ -170,3 +175,68 @@ impl Terminal {
         )
     }
 }
+
+pub async fn create_terminal_entity(
+    command: String,
+    args: &[String],
+    env_vars: Vec<(String, String)>,
+    cwd: Option<PathBuf>,
+    project: &Entity<Project>,
+    cx: &mut AsyncApp,
+) -> Result<Entity<terminal::Terminal>> {
+    let mut env = if let Some(dir) = &cwd {
+        project
+            .update(cx, |project, cx| {
+                let worktree = project.find_worktree(dir.as_path(), cx);
+                let shell = TerminalSettings::get(
+                    worktree.as_ref().map(|(worktree, path)| SettingsLocation {
+                        worktree_id: worktree.read(cx).id(),
+                        path: &path,
+                    }),
+                    cx,
+                )
+                .shell
+                .clone();
+                project.directory_environment(&shell, 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
+}

crates/acp_tools/Cargo.toml 🔗

@@ -26,5 +26,4 @@ settings.workspace = true
 theme.workspace = true
 ui.workspace = true
 util.workspace = true
-workspace-hack.workspace = true
 workspace.workspace = true

crates/acp_tools/src/acp_tools.rs 🔗

@@ -93,8 +93,8 @@ struct WatchedConnection {
     messages: Vec<WatchedConnectionMessage>,
     list_state: ListState,
     connection: Weak<acp::ClientSideConnection>,
-    incoming_request_methods: HashMap<i32, Arc<str>>,
-    outgoing_request_methods: HashMap<i32, Arc<str>>,
+    incoming_request_methods: HashMap<acp::RequestId, Arc<str>>,
+    outgoing_request_methods: HashMap<acp::RequestId, Arc<str>>,
     _task: Task<()>,
 }
 
@@ -175,7 +175,7 @@ impl AcpTools {
                     }
                 };
 
-                method_map.insert(id, method.clone());
+                method_map.insert(id.clone(), method.clone());
                 (Some(id), method.into(), MessageType::Request, Ok(params))
             }
             acp::StreamMessageContent::Response { id, result } => {
@@ -338,6 +338,7 @@ impl AcpTools {
                     .children(
                         message
                             .request_id
+                            .as_ref()
                             .map(|req_id| div().child(ui::Chip::new(req_id.to_string()))),
                     ),
             )
@@ -389,7 +390,7 @@ impl AcpTools {
 
 struct WatchedConnectionMessage {
     name: SharedString,
-    request_id: Option<i32>,
+    request_id: Option<acp::RequestId>,
     direction: acp::StreamMessageDirection,
     message_type: MessageType,
     params: Result<Option<serde_json::Value>, acp::Error>,

crates/action_log/Cargo.toml 🔗

@@ -23,7 +23,6 @@ project.workspace = true
 text.workspace = true
 util.workspace = true
 watch.workspace = true
-workspace-hack.workspace = true
 
 
 [dev-dependencies]

crates/activity_indicator/Cargo.toml 🔗

@@ -25,7 +25,6 @@ proto.workspace = true
 smallvec.workspace = true
 ui.workspace = true
 util.workspace = true
-workspace-hack.workspace = true
 workspace.workspace = true
 
 [dev-dependencies]

crates/activity_indicator/src/activity_indicator.rs 🔗

@@ -11,8 +11,7 @@ use language::{
     LanguageServerStatusUpdate, ServerHealth,
 };
 use project::{
-    EnvironmentErrorMessage, LanguageServerProgress, LspStoreEvent, Project,
-    ProjectEnvironmentEvent,
+    LanguageServerProgress, LspStoreEvent, Project, ProjectEnvironmentEvent,
     git_store::{GitStoreEvent, Repository},
 };
 use smallvec::SmallVec;
@@ -327,20 +326,20 @@ impl ActivityIndicator {
             .flatten()
     }
 
-    fn pending_environment_error<'a>(&'a self, cx: &'a App) -> Option<&'a EnvironmentErrorMessage> {
+    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<Self>) -> Option<Content> {
         // Show if any direnv calls failed
-        if let Some(error) = self.pending_environment_error(cx) {
+        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: error.0.clone(),
+                message: message.clone(),
                 on_click: Some(Arc::new(move |this, window, cx| {
                     this.project.update(cx, |project, cx| {
                         project.pop_environment_error(cx);

crates/agent/Cargo.toml 🔗

@@ -5,74 +5,101 @@ edition.workspace = true
 publish.workspace = true
 license = "GPL-3.0-or-later"
 
-[lints]
-workspace = true
-
 [lib]
 path = "src/agent.rs"
-doctest = false
 
 [features]
-test-support = [
-    "gpui/test-support",
-    "language/test-support",
-]
+test-support = ["db/test-support"]
+eval = []
+edit-agent-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_context.workspace = true
-assistant_tool.workspace = true
+assistant_text_thread.workspace = true
 chrono.workspace = true
 client.workspace = true
 cloud_llm_client.workspace = true
 collections.workspace = true
-component.workspace = true
 context_server.workspace = true
-convert_case.workspace = true
+db.workspace = true
+derive_more.workspace = true
 fs.workspace = true
 futures.workspace = true
 git.workspace = true
 gpui.workspace = true
-heed.workspace = true
+handlebars = { workspace = true, features = ["rust-embed"] }
+html_to_markdown.workspace = true
 http_client.workspace = true
-icons.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
-postage.workspace = true
 project.workspace = true
 prompt_store.workspace = true
-ref-cast.workspace = true
-rope.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
+terminal.workspace = true
 text.workspace = true
-theme.workspace = true
 thiserror.workspace = true
-time.workspace = true
+ui.workspace = true
 util.workspace = true
 uuid.workspace = true
-workspace-hack.workspace = true
+watch.workspace = true
+web_search.workspace = true
 zed_env_vars.workspace = true
 zstd.workspace = true
 
 [dev-dependencies]
-assistant_tools.workspace = true
+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
+fs = { workspace = true, "features" = ["test-support"] }
+git = { workspace = true, "features" = ["test-support"] }
 gpui = { workspace = true, "features" = ["test-support"] }
-indoc.workspace = true
+gpui_tokio.workspace = true
 language = { workspace = true, "features" = ["test-support"] }
 language_model = { workspace = true, "features" = ["test-support"] }
-parking_lot.workspace = true
+lsp = { workspace = true, "features" = ["test-support"] }
 pretty_assertions.workspace = true
-project = { workspace = true, features = ["test-support"] }
-workspace = { workspace = true, features = ["test-support"] }
+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

crates/agent/src/agent.rs 🔗

@@ -1,21 +1,1635 @@
-pub mod agent_profile;
-pub mod context;
-pub mod context_server_tool;
-pub mod context_store;
-pub mod thread;
-pub mod thread_store;
-pub mod tool_use;
-
-pub use context::{AgentContext, ContextId, ContextLoadResult};
-pub use context_store::ContextStore;
+mod db;
+mod edit_agent;
+mod history_store;
+mod legacy_thread;
+mod native_agent_server;
+pub mod outline;
+mod templates;
+mod thread;
+mod tool_schema;
+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 std::sync::Arc;
-pub use thread::{
-    LastRestoreCheckpoint, Message, MessageCrease, MessageId, MessageSegment, Thread, ThreadError,
-    ThreadEvent, ThreadFeedback, ThreadId, ThreadSummary, TokenUsageRatio,
+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,
 };
-pub use thread_store::{SerializedThread, TextThreadStore, ThreadStore};
+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<project::telemetry_snapshot::TelemetryWorktreeSnapshot>,
+    pub timestamp: DateTime<Utc>,
+}
+
+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<Thread>,
+    /// The ACP thread that handles protocol communication
+    acp_thread: WeakEntity<acp_thread::AcpThread>,
+    pending_save: Task<()>,
+    _subscriptions: Vec<Subscription>,
+}
+
+pub struct LanguageModels {
+    /// Access language model by ID
+    models: HashMap<acp::ModelId, Arc<dyn LanguageModel>>,
+    /// 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::<Vec<_>>();
+
+        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();
+                if !recommended_models.contains(&(model.provider_id(), model.id())) {
+                    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<Arc<dyn LanguageModel>> {
+        self.models.get(model_id).cloned()
+    }
+
+    fn map_language_model_to_info(
+        model: &Arc<dyn LanguageModel>,
+        provider: &Arc<dyn LanguageModelProvider>,
+    ) -> 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<dyn LanguageModel>) -> acp::ModelId {
+        acp::ModelId(format!("{}/{}", model.provider_id().0, model.id().0).into())
+    }
+
+    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::<Vec<_>>();
+
+        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<acp::SessionId, Session>,
+    history: Entity<HistoryStore>,
+    /// Shared project context for all threads
+    project_context: Entity<ProjectContext>,
+    project_context_needs_refresh: watch::Sender<()>,
+    _maintain_project_context: Task<Result<()>>,
+    context_server_registry: Entity<ContextServerRegistry>,
+    /// Shared templates for all threads
+    templates: Arc<Templates>,
+    /// Cached model information
+    models: LanguageModels,
+    project: Entity<Project>,
+    prompt_store: Option<Entity<PromptStore>>,
+    fs: Arc<dyn Fs>,
+    _subscriptions: Vec<Subscription>,
+}
+
+impl NativeAgent {
+    pub async fn new(
+        project: Entity<Project>,
+        history: Entity<HistoryStore>,
+        templates: Arc<Templates>,
+        prompt_store: Option<Entity<PromptStore>>,
+        fs: Arc<dyn Fs>,
+        cx: &mut AsyncApp,
+    ) -> Result<Entity<NativeAgent>> {
+        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<Thread>,
+        cx: &mut Context<Self>,
+    ) -> Entity<AcpThread> {
+        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<Self>,
+        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<Project>,
+        prompt_store: Option<&Entity<PromptStore>>,
+        cx: &mut App,
+    ) -> Task<ProjectContext> {
+        let worktrees = project.read(cx).visible_worktrees(cx).collect::<Vec<_>>();
+        let worktree_tasks = worktrees
+            .into_iter()
+            .map(|worktree| {
+                Self::load_worktree_info_for_system_prompt(worktree, project.clone(), cx)
+            })
+            .collect::<Vec<_>>();
+        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::<Vec<_>>();
+
+            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::<Vec<_>>();
+
+            ProjectContext::new(worktrees, default_user_rules)
+        })
+    }
+
+    fn load_worktree_info_for_system_prompt(
+        worktree: Entity<Worktree>,
+        project: Entity<Project>,
+        cx: &mut App,
+    ) -> Task<(WorktreeContext, Option<RulesLoadingError>)> {
+        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<Worktree>,
+        project: Entity<Project>,
+        cx: &mut App,
+    ) -> Option<Task<Result<RulesFileContext>>> {
+        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<Thread>,
+        _: &TitleUpdated,
+        cx: &mut Context<Self>,
+    ) {
+        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<Thread>,
+        usage: &TokenUsageUpdated,
+        cx: &mut Context<Self>,
+    ) {
+        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<Project>,
+        event: &project::Event,
+        _cx: &mut Context<Self>,
+    ) {
+        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<PromptStore>,
+        _event: &prompt_store::PromptsUpdatedEvent,
+        _cx: &mut Context<Self>,
+    ) {
+        self.project_context_needs_refresh.send(()).ok();
+    }
+
+    fn handle_models_updated_event(
+        &mut self,
+        _registry: Entity<LanguageModelRegistry>,
+        _event: &language_model::Event,
+        cx: &mut Context<Self>,
+    ) {
+        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<Self>,
+    ) -> Task<Result<Entity<Thread>>> {
+        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<Self>,
+    ) -> Task<Result<Entity<AcpThread>>> {
+        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<Self>,
+    ) -> Task<Result<SharedString>> {
+        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<Thread>, cx: &mut Context<Self>) {
+        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<NativeAgent>);
+
+impl NativeAgentConnection {
+    pub fn thread(&self, session_id: &acp::SessionId, cx: &App) -> Option<Entity<Thread>> {
+        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<Result<Entity<Thread>>> {
+        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<Thread>, &mut App) -> Result<mpsc::UnboundedReceiver<Result<ThreadEvent>>>,
+    ) -> Task<Result<acp::PromptResponse>> {
+        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<Result<ThreadEvent>>,
+        acp_thread: WeakEntity<AcpThread>,
+        cx: &App,
+    ) -> Task<Result<acp::PromptResponse>> {
+        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(
+                                        acp::ContentBlock::Text(acp::TextContent {
+                                            text,
+                                            annotations: None,
+                                            meta: None,
+                                        }),
+                                        false,
+                                        cx,
+                                    )
+                                })?;
+                            }
+                            ThreadEvent::AgentThinking(text) => {
+                                acp_thread.update(cx, |thread, cx| {
+                                    thread.push_assistant_content_block(
+                                        acp::ContentBlock::Text(acp::TextContent {
+                                            text,
+                                            annotations: None,
+                                            meta: None,
+                                        }),
+                                        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 { 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 {
+                                    stop_reason,
+                                    meta: None,
+                                });
+                            }
+                        }
+                    }
+                    Err(e) => {
+                        log::error!("Error in model response stream: {:?}", e);
+                        return Err(e);
+                    }
+                }
+            }
+
+            log::debug!("Response stream completed");
+            anyhow::Ok(acp::PromptResponse {
+                stop_reason: acp::StopReason::EndTurn,
+                meta: None,
+            })
+        })
+    }
+}
+
+struct NativeAgentModelSelector {
+    session_id: acp::SessionId,
+    connection: NativeAgentConnection,
+}
+
+impl acp_thread::AgentModelSelector for NativeAgentModelSelector {
+    fn list_models(&self, cx: &mut App) -> Task<Result<acp_thread::AgentModelList>> {
+        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<Result<()>> {
+        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<Result<acp_thread::AgentModelInfo>> {
+        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<watch::Receiver<()>> {
+        Some(self.connection.0.read(cx).models.watch())
+    }
+}
+
+impl acp_thread::AgentConnection for NativeAgentConnection {
+    fn new_thread(
+        self: Rc<Self>,
+        project: Entity<Project>,
+        cwd: &Path,
+        cx: &mut App,
+    ) -> Task<Result<Entity<acp_thread::AcpThread>>> {
+        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<NativeAgent>| -> 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<Result<()>> {
+        Task::ready(Ok(()))
+    }
+
+    fn model_selector(&self, session_id: &acp::SessionId) -> Option<Rc<dyn AgentModelSelector>> {
+        Some(Rc::new(NativeAgentModelSelector {
+            session_id: session_id.clone(),
+            connection: self.clone(),
+        }) as Rc<dyn AgentModelSelector>)
+    }
+
+    fn prompt(
+        &self,
+        id: Option<acp_thread::UserMessageId>,
+        params: acp::PromptRequest,
+        cx: &mut App,
+    ) -> Task<Result<acp::PromptResponse>> {
+        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());
+
+        self.run_turn(session_id, cx, |thread, cx| {
+            let content: Vec<UserMessageContent> = params
+                .prompt
+                .into_iter()
+                .map(Into::into)
+                .collect::<Vec<_>>();
+            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<Rc<dyn acp_thread::AgentSessionResume>> {
+        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<Rc<dyn acp_thread::AgentSessionTruncate>> {
+        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<Rc<dyn acp_thread::AgentSessionSetTitle>> {
+        Some(Rc::new(NativeAgentSessionSetTitle {
+            connection: self.clone(),
+            session_id: session_id.clone(),
+        }) as _)
+    }
+
+    fn telemetry(&self) -> Option<Rc<dyn acp_thread::AgentTelemetry>> {
+        Some(Rc::new(self.clone()) as Rc<dyn acp_thread::AgentTelemetry>)
+    }
+
+    fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
+        self
+    }
+}
+
+impl acp_thread::AgentTelemetry for NativeAgentConnection {
+    fn agent_name(&self) -> String {
+        "Zed".into()
+    }
+
+    fn thread_data(
+        &self,
+        session_id: &acp::SessionId,
+        cx: &mut App,
+    ) -> Task<Result<serde_json::Value>> {
+        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<Thread>,
+    acp_thread: WeakEntity<AcpThread>,
+}
+
+impl acp_thread::AgentSessionTruncate for NativeAgentSessionTruncate {
+    fn run(&self, message_id: acp_thread::UserMessageId, cx: &mut App) -> Task<Result<()>> {
+        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<Result<acp::PromptResponse>> {
+        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<Result<()>> {
+        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<AcpThread>,
+}
+
+impl ThreadEnvironment for AcpThreadEnvironment {
+    fn create_terminal(
+        &self,
+        command: String,
+        cwd: Option<PathBuf>,
+        output_byte_limit: Option<u64>,
+        cx: &mut AsyncApp,
+    ) -> Task<Result<Rc<dyn TerminalHandle>>> {
+        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<acp_thread::Terminal>,
+    _drop_tx: Option<oneshot::Sender<()>>,
+}
+
+impl TerminalHandle for AcpTerminalHandle {
+    fn id(&self, cx: &AsyncApp) -> Result<acp::TerminalId> {
+        self.terminal.read_with(cx, |term, _cx| term.id().clone())
+    }
+
+    fn wait_for_exit(&self, cx: &AsyncApp) -> Result<Shared<Task<acp::TerminalExitStatus>>> {
+        self.terminal
+            .read_with(cx, |term, _cx| term.wait_for_exit())
+    }
+
+    fn current_output(&self, cx: &AsyncApp) -> Result<acp::TerminalOutputResponse> {
+        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("fake/fake".into()),
+                    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("fake/fake".into());
+        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 {
+                        name: "b.md".into(),
+                        uri: MentionUri::File {
+                            abs_path: path!("/a/b.md").into(),
+                        }
+                        .to_uri()
+                        .to_string(),
+                        annotations: None,
+                        description: None,
+                        mime_type: None,
+                        size: None,
+                        title: None,
+                        meta: None,
+                    }),
+                    " mean?".into(),
+                ],
+                cx,
+            )
+        });
+        let send = cx.foreground_executor().spawn(send);
+        cx.run_until_parked();
+
+        model.send_last_completion_stream_text_chunk("Lorem.");
+        model.end_last_completion_stream();
+        cx.run_until_parked();
+        summary_model
+            .send_last_completion_stream_text_chunk(&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::<Vec<_>>(), []);
+        });
+
+        // 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<HistoryStore>,
+        cx: &mut TestAppContext,
+    ) -> Vec<(HistoryEntryId, String)> {
+        history.read_with(cx, |history, _| {
+            history
+                .entries()
+                .map(|e| (e.id(), e.title().to_string()))
+                .collect::<Vec<_>>()
+        })
+    }
 
-pub fn init(fs: Arc<dyn Fs>, cx: &mut gpui::App) {
-    thread_store::init(fs, cx);
+    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);
+            Project::init_settings(cx);
+            agent_settings::init(cx);
+            language::init(cx);
+            LanguageModelRegistry::test(cx);
+        });
+    }
 }

crates/agent/src/agent_profile.rs 🔗

@@ -1,341 +0,0 @@
-use std::sync::Arc;
-
-use agent_settings::{AgentProfileId, AgentProfileSettings, AgentSettings};
-use assistant_tool::{Tool, ToolSource, ToolWorkingSet, UniqueToolName};
-use collections::IndexMap;
-use convert_case::{Case, Casing};
-use fs::Fs;
-use gpui::{App, Entity, SharedString};
-use settings::{Settings, update_settings_file};
-use util::ResultExt;
-
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub struct AgentProfile {
-    id: AgentProfileId,
-    tool_set: Entity<ToolWorkingSet>,
-}
-
-pub type AvailableProfiles = IndexMap<AgentProfileId, SharedString>;
-
-impl AgentProfile {
-    pub fn new(id: AgentProfileId, tool_set: Entity<ToolWorkingSet>) -> Self {
-        Self { id, tool_set }
-    }
-
-    /// Saves a new profile to the settings.
-    pub fn create(
-        name: String,
-        base_profile_id: Option<AgentProfileId>,
-        fs: Arc<dyn Fs>,
-        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());
-
-        let profile_settings = AgentProfileSettings {
-            name: name.into(),
-            tools: base_profile
-                .as_ref()
-                .map(|profile| profile.tools.clone())
-                .unwrap_or_default(),
-            enable_all_context_servers: base_profile
-                .as_ref()
-                .map(|profile| profile.enable_all_context_servers)
-                .unwrap_or_default(),
-            context_servers: base_profile
-                .map(|profile| profile.context_servers)
-                .unwrap_or_default(),
-        };
-
-        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
-    }
-
-    pub fn id(&self) -> &AgentProfileId {
-        &self.id
-    }
-
-    pub fn enabled_tools(&self, cx: &App) -> Vec<(UniqueToolName, Arc<dyn Tool>)> {
-        let Some(settings) = AgentSettings::get_global(cx).profiles.get(&self.id) else {
-            return Vec::new();
-        };
-
-        self.tool_set
-            .read(cx)
-            .tools(cx)
-            .into_iter()
-            .filter(|(_, tool)| Self::is_enabled(settings, tool.source(), tool.name()))
-            .collect()
-    }
-
-    pub fn is_tool_enabled(&self, source: ToolSource, tool_name: String, cx: &App) -> bool {
-        let Some(settings) = AgentSettings::get_global(cx).profiles.get(&self.id) else {
-            return false;
-        };
-
-        Self::is_enabled(settings, source, tool_name)
-    }
-
-    fn is_enabled(settings: &AgentProfileSettings, source: ToolSource, name: String) -> bool {
-        match source {
-            ToolSource::Native => *settings.tools.get(name.as_str()).unwrap_or(&false),
-            ToolSource::ContextServer { id } => settings
-                .context_servers
-                .get(id.as_ref())
-                .and_then(|preset| preset.tools.get(name.as_str()).copied())
-                .unwrap_or(settings.enable_all_context_servers),
-        }
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use agent_settings::ContextServerPreset;
-    use assistant_tool::ToolRegistry;
-    use collections::IndexMap;
-    use gpui::SharedString;
-    use gpui::{AppContext, TestAppContext};
-    use http_client::FakeHttpClient;
-    use project::Project;
-    use settings::{Settings, SettingsStore};
-
-    use super::*;
-
-    #[gpui::test]
-    async fn test_enabled_built_in_tools_for_profile(cx: &mut TestAppContext) {
-        init_test_settings(cx);
-
-        let id = AgentProfileId::default();
-        let profile_settings = cx.read(|cx| {
-            AgentSettings::get_global(cx)
-                .profiles
-                .get(&id)
-                .unwrap()
-                .clone()
-        });
-        let tool_set = default_tool_set(cx);
-
-        let profile = AgentProfile::new(id, tool_set);
-
-        let mut enabled_tools = cx
-            .read(|cx| profile.enabled_tools(cx))
-            .into_iter()
-            .map(|(_, tool)| tool.name())
-            .collect::<Vec<_>>();
-        enabled_tools.sort();
-
-        let mut expected_tools = profile_settings
-            .tools
-            .into_iter()
-            .filter_map(|(tool, enabled)| enabled.then_some(tool.to_string()))
-            // Provider dependent
-            .filter(|tool| tool != "web_search")
-            .collect::<Vec<_>>();
-        // Plus all registered MCP tools
-        expected_tools.extend(["enabled_mcp_tool".into(), "disabled_mcp_tool".into()]);
-        expected_tools.sort();
-
-        assert_eq!(enabled_tools, expected_tools);
-    }
-
-    #[gpui::test]
-    async fn test_custom_mcp_settings(cx: &mut TestAppContext) {
-        init_test_settings(cx);
-
-        let id = AgentProfileId("custom_mcp".into());
-        let profile_settings = cx.read(|cx| {
-            AgentSettings::get_global(cx)
-                .profiles
-                .get(&id)
-                .unwrap()
-                .clone()
-        });
-        let tool_set = default_tool_set(cx);
-
-        let profile = AgentProfile::new(id, tool_set);
-
-        let mut enabled_tools = cx
-            .read(|cx| profile.enabled_tools(cx))
-            .into_iter()
-            .map(|(_, tool)| tool.name())
-            .collect::<Vec<_>>();
-        enabled_tools.sort();
-
-        let mut expected_tools = profile_settings.context_servers["mcp"]
-            .tools
-            .iter()
-            .filter_map(|(key, enabled)| enabled.then(|| key.to_string()))
-            .collect::<Vec<_>>();
-        expected_tools.sort();
-
-        assert_eq!(enabled_tools, expected_tools);
-    }
-
-    #[gpui::test]
-    async fn test_only_built_in(cx: &mut TestAppContext) {
-        init_test_settings(cx);
-
-        let id = AgentProfileId("write_minus_mcp".into());
-        let profile_settings = cx.read(|cx| {
-            AgentSettings::get_global(cx)
-                .profiles
-                .get(&id)
-                .unwrap()
-                .clone()
-        });
-        let tool_set = default_tool_set(cx);
-
-        let profile = AgentProfile::new(id, tool_set);
-
-        let mut enabled_tools = cx
-            .read(|cx| profile.enabled_tools(cx))
-            .into_iter()
-            .map(|(_, tool)| tool.name())
-            .collect::<Vec<_>>();
-        enabled_tools.sort();
-
-        let mut expected_tools = profile_settings
-            .tools
-            .into_iter()
-            .filter_map(|(tool, enabled)| enabled.then_some(tool.to_string()))
-            // Provider dependent
-            .filter(|tool| tool != "web_search")
-            .collect::<Vec<_>>();
-        expected_tools.sort();
-
-        assert_eq!(enabled_tools, expected_tools);
-    }
-
-    fn init_test_settings(cx: &mut TestAppContext) {
-        cx.update(|cx| {
-            let settings_store = SettingsStore::test(cx);
-            cx.set_global(settings_store);
-            Project::init_settings(cx);
-            AgentSettings::register(cx);
-            language_model::init_settings(cx);
-            ToolRegistry::default_global(cx);
-            assistant_tools::init(FakeHttpClient::with_404_response(), cx);
-        });
-
-        cx.update(|cx| {
-            let mut agent_settings = AgentSettings::get_global(cx).clone();
-            agent_settings.profiles.insert(
-                AgentProfileId("write_minus_mcp".into()),
-                AgentProfileSettings {
-                    name: "write_minus_mcp".into(),
-                    enable_all_context_servers: false,
-                    ..agent_settings.profiles[&AgentProfileId::default()].clone()
-                },
-            );
-            agent_settings.profiles.insert(
-                AgentProfileId("custom_mcp".into()),
-                AgentProfileSettings {
-                    name: "mcp".into(),
-                    tools: IndexMap::default(),
-                    enable_all_context_servers: false,
-                    context_servers: IndexMap::from_iter([("mcp".into(), context_server_preset())]),
-                },
-            );
-            AgentSettings::override_global(agent_settings, cx);
-        })
-    }
-
-    fn context_server_preset() -> ContextServerPreset {
-        ContextServerPreset {
-            tools: IndexMap::from_iter([
-                ("enabled_mcp_tool".into(), true),
-                ("disabled_mcp_tool".into(), false),
-            ]),
-        }
-    }
-
-    fn default_tool_set(cx: &mut TestAppContext) -> Entity<ToolWorkingSet> {
-        cx.new(|cx| {
-            let mut tool_set = ToolWorkingSet::default();
-            tool_set.insert(Arc::new(FakeTool::new("enabled_mcp_tool", "mcp")), cx);
-            tool_set.insert(Arc::new(FakeTool::new("disabled_mcp_tool", "mcp")), cx);
-            tool_set
-        })
-    }
-
-    struct FakeTool {
-        name: String,
-        source: SharedString,
-    }
-
-    impl FakeTool {
-        fn new(name: impl Into<String>, source: impl Into<SharedString>) -> Self {
-            Self {
-                name: name.into(),
-                source: source.into(),
-            }
-        }
-    }
-
-    impl Tool for FakeTool {
-        fn name(&self) -> String {
-            self.name.clone()
-        }
-
-        fn source(&self) -> ToolSource {
-            ToolSource::ContextServer {
-                id: self.source.clone(),
-            }
-        }
-
-        fn description(&self) -> String {
-            unimplemented!()
-        }
-
-        fn icon(&self) -> icons::IconName {
-            unimplemented!()
-        }
-
-        fn needs_confirmation(
-            &self,
-            _input: &serde_json::Value,
-            _project: &Entity<Project>,
-            _cx: &App,
-        ) -> bool {
-            unimplemented!()
-        }
-
-        fn ui_text(&self, _input: &serde_json::Value) -> String {
-            unimplemented!()
-        }
-
-        fn run(
-            self: Arc<Self>,
-            _input: serde_json::Value,
-            _request: Arc<language_model::LanguageModelRequest>,
-            _project: Entity<Project>,
-            _action_log: Entity<action_log::ActionLog>,
-            _model: Arc<dyn language_model::LanguageModel>,
-            _window: Option<gpui::AnyWindowHandle>,
-            _cx: &mut App,
-        ) -> assistant_tool::ToolResult {
-            unimplemented!()
-        }
-
-        fn may_perform_edits(&self) -> bool {
-            unimplemented!()
-        }
-    }
-}

crates/agent/src/context_server_tool.rs 🔗

@@ -1,140 +0,0 @@
-use std::sync::Arc;
-
-use action_log::ActionLog;
-use anyhow::{Result, anyhow, bail};
-use assistant_tool::{Tool, ToolResult, ToolSource};
-use context_server::{ContextServerId, types};
-use gpui::{AnyWindowHandle, App, Entity, Task};
-use icons::IconName;
-use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
-use project::{Project, context_server_store::ContextServerStore};
-
-pub struct ContextServerTool {
-    store: Entity<ContextServerStore>,
-    server_id: ContextServerId,
-    tool: types::Tool,
-}
-
-impl ContextServerTool {
-    pub fn new(
-        store: Entity<ContextServerStore>,
-        server_id: ContextServerId,
-        tool: types::Tool,
-    ) -> Self {
-        Self {
-            store,
-            server_id,
-            tool,
-        }
-    }
-}
-
-impl Tool for ContextServerTool {
-    fn name(&self) -> String {
-        self.tool.name.clone()
-    }
-
-    fn description(&self) -> String {
-        self.tool.description.clone().unwrap_or_default()
-    }
-
-    fn icon(&self) -> IconName {
-        IconName::ToolHammer
-    }
-
-    fn source(&self) -> ToolSource {
-        ToolSource::ContextServer {
-            id: self.server_id.clone().0.into(),
-        }
-    }
-
-    fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
-        true
-    }
-
-    fn may_perform_edits(&self) -> bool {
-        true
-    }
-
-    fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
-        let mut schema = self.tool.input_schema.clone();
-        assistant_tool::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 ui_text(&self, _input: &serde_json::Value) -> String {
-        format!("Run MCP tool `{}`", self.tool.name)
-    }
-
-    fn run(
-        self: Arc<Self>,
-        input: serde_json::Value,
-        _request: Arc<LanguageModelRequest>,
-        _project: Entity<Project>,
-        _action_log: Entity<ActionLog>,
-        _model: Arc<dyn LanguageModel>,
-        _window: Option<AnyWindowHandle>,
-        cx: &mut App,
-    ) -> ToolResult {
-        if let Some(server) = self.store.read(cx).get_running_server(&self.server_id) {
-            let tool_name = self.tool.name.clone();
-
-            cx.spawn(async move |_cx| {
-                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::requests::CallTool>(
-                        context_server::types::CallToolParams {
-                            name: tool_name,
-                            arguments,
-                            meta: None,
-                        },
-                    )
-                    .await?;
-
-                let mut result = String::new();
-                for content in response.content {
-                    match content {
-                        types::ToolResponseContent::Text { text } => {
-                            result.push_str(&text);
-                        }
-                        types::ToolResponseContent::Image { .. } => {
-                            log::warn!("Ignoring image content from tool response");
-                        }
-                        types::ToolResponseContent::Audio { .. } => {
-                            log::warn!("Ignoring audio content from tool response");
-                        }
-                        types::ToolResponseContent::Resource { .. } => {
-                            log::warn!("Ignoring resource content from tool response");
-                        }
-                    }
-                }
-                Ok(result.into())
-            })
-            .into()
-        } else {
-            Task::ready(Err(anyhow!("Context server not found"))).into()
-        }
-    }
-}

crates/agent2/src/db.rs → crates/agent/src/db.rs 🔗

@@ -1,6 +1,5 @@
 use crate::{AgentMessage, AgentMessageContent, UserMessage, UserMessageContent};
 use acp_thread::UserMessageId;
-use agent::{thread::DetailedSummaryState, thread_store};
 use agent_client_protocol as acp;
 use agent_settings::{AgentProfileId, CompletionMode};
 use anyhow::{Result, anyhow};
@@ -21,8 +20,8 @@ use ui::{App, SharedString};
 use zed_env_vars::ZED_STATELESS;
 
 pub type DbMessage = crate::Message;
-pub type DbSummary = DetailedSummaryState;
-pub type DbLanguageModel = thread_store::SerializedLanguageModel;
+pub type DbSummary = crate::legacy_thread::DetailedSummaryState;
+pub type DbLanguageModel = crate::legacy_thread::SerializedLanguageModel;
 
 #[derive(Debug, Clone, Serialize, Deserialize)]
 pub struct DbThreadMetadata {
@@ -40,7 +39,7 @@ pub struct DbThread {
     #[serde(default)]
     pub detailed_summary: Option<SharedString>,
     #[serde(default)]
-    pub initial_project_snapshot: Option<Arc<agent::thread::ProjectSnapshot>>,
+    pub initial_project_snapshot: Option<Arc<crate::ProjectSnapshot>>,
     #[serde(default)]
     pub cumulative_token_usage: language_model::TokenUsage,
     #[serde(default)]
@@ -61,13 +60,17 @@ impl DbThread {
         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(agent::SerializedThread::from_json(json)?),
+                _ => Self::upgrade_from_agent_1(crate::legacy_thread::SerializedThread::from_json(
+                    json,
+                )?),
             },
-            _ => Self::upgrade_from_agent_1(agent::SerializedThread::from_json(json)?),
+            _ => {
+                Self::upgrade_from_agent_1(crate::legacy_thread::SerializedThread::from_json(json)?)
+            }
         }
     }
 
-    fn upgrade_from_agent_1(thread: agent::SerializedThread) -> Result<Self> {
+    fn upgrade_from_agent_1(thread: crate::legacy_thread::SerializedThread) -> Result<Self> {
         let mut messages = Vec::new();
         let mut request_token_usage = HashMap::default();
 
@@ -80,14 +83,19 @@ impl DbThread {
                     // Convert segments to content
                     for segment in msg.segments {
                         match segment {
-                            thread_store::SerializedMessageSegment::Text { text } => {
+                            crate::legacy_thread::SerializedMessageSegment::Text { text } => {
                                 content.push(UserMessageContent::Text(text));
                             }
-                            thread_store::SerializedMessageSegment::Thinking { text, .. } => {
+                            crate::legacy_thread::SerializedMessageSegment::Thinking {
+                                text,
+                                ..
+                            } => {
                                 // User messages don't have thinking segments, but handle gracefully
                                 content.push(UserMessageContent::Text(text));
                             }
-                            thread_store::SerializedMessageSegment::RedactedThinking { .. } => {
+                            crate::legacy_thread::SerializedMessageSegment::RedactedThinking {
+                                ..
+                            } => {
                                 // User messages don't have redacted thinking, skip.
                             }
                         }
@@ -113,16 +121,18 @@ impl DbThread {
                     // Convert segments to content
                     for segment in msg.segments {
                         match segment {
-                            thread_store::SerializedMessageSegment::Text { text } => {
+                            crate::legacy_thread::SerializedMessageSegment::Text { text } => {
                                 content.push(AgentMessageContent::Text(text));
                             }
-                            thread_store::SerializedMessageSegment::Thinking {
+                            crate::legacy_thread::SerializedMessageSegment::Thinking {
                                 text,
                                 signature,
                             } => {
                                 content.push(AgentMessageContent::Thinking { text, signature });
                             }
-                            thread_store::SerializedMessageSegment::RedactedThinking { data } => {
+                            crate::legacy_thread::SerializedMessageSegment::RedactedThinking {
+                                data,
+                            } => {
                                 content.push(AgentMessageContent::RedactedThinking(data));
                             }
                         }
@@ -187,10 +197,9 @@ impl DbThread {
             messages,
             updated_at: thread.updated_at,
             detailed_summary: match thread.detailed_summary_state {
-                DetailedSummaryState::NotGenerated | DetailedSummaryState::Generating { .. } => {
-                    None
-                }
-                DetailedSummaryState::Generated { text, .. } => Some(text),
+                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,
@@ -414,84 +423,3 @@ impl ThreadsDatabase {
         })
     }
 }
-
-#[cfg(test)]
-mod tests {
-
-    use super::*;
-    use agent::MessageSegment;
-    use agent::context::LoadedContext;
-    use client::Client;
-    use fs::{FakeFs, Fs};
-    use gpui::AppContext;
-    use gpui::TestAppContext;
-    use http_client::FakeHttpClient;
-    use language_model::Role;
-    use project::Project;
-    use settings::SettingsStore;
-
-    fn init_test(fs: Arc<dyn Fs>, cx: &mut TestAppContext) {
-        env_logger::try_init().ok();
-        cx.update(|cx| {
-            let settings_store = SettingsStore::test(cx);
-            cx.set_global(settings_store);
-            Project::init_settings(cx);
-            language::init(cx);
-
-            let http_client = FakeHttpClient::with_404_response();
-            let clock = Arc::new(clock::FakeSystemClock::new());
-            let client = Client::new(clock, http_client, cx);
-            agent::init(fs, cx);
-            agent_settings::init(cx);
-            language_model::init(client, cx);
-        });
-    }
-
-    #[gpui::test]
-    async fn test_retrieving_old_thread(cx: &mut TestAppContext) {
-        let fs = FakeFs::new(cx.executor());
-        init_test(fs.clone(), cx);
-        let project = Project::test(fs, [], cx).await;
-
-        // Save a thread using the old agent.
-        let thread_store = cx.new(|cx| agent::ThreadStore::fake(project, cx));
-        let thread = thread_store.update(cx, |thread_store, cx| thread_store.create_thread(cx));
-        thread.update(cx, |thread, cx| {
-            thread.insert_message(
-                Role::User,
-                vec![MessageSegment::Text("Hey!".into())],
-                LoadedContext::default(),
-                vec![],
-                false,
-                cx,
-            );
-            thread.insert_message(
-                Role::Assistant,
-                vec![MessageSegment::Text("How're you doing?".into())],
-                LoadedContext::default(),
-                vec![],
-                false,
-                cx,
-            )
-        });
-        thread_store
-            .update(cx, |thread_store, cx| thread_store.save_thread(&thread, cx))
-            .await
-            .unwrap();
-
-        // Open that same thread using the new agent.
-        let db = cx.update(ThreadsDatabase::connect).await.unwrap();
-        let threads = db.list_threads().await.unwrap();
-        assert_eq!(threads.len(), 1);
-        let thread = db
-            .load_thread(threads[0].id.clone())
-            .await
-            .unwrap()
-            .unwrap();
-        assert_eq!(thread.messages[0].to_markdown(), "## User\n\nHey!\n");
-        assert_eq!(
-            thread.messages[1].to_markdown(),
-            "## Assistant\n\nHow're you doing?\n"
-        );
-    }
-}

crates/assistant_tools/src/edit_agent/evals.rs → crates/agent/src/edit_agent/evals.rs 🔗

@@ -1,12 +1,8 @@
 use super::*;
 use crate::{
-    ReadFileToolInput,
-    edit_file_tool::{EditFileMode, EditFileToolInput},
-    grep_tool::GrepToolInput,
-    list_directory_tool::ListDirectoryToolInput,
+    EditFileMode, EditFileToolInput, GrepToolInput, ListDirectoryToolInput, ReadFileToolInput,
 };
 use Role::*;
-use assistant_tool::ToolRegistry;
 use client::{Client, UserStore};
 use collections::HashMap;
 use fs::FakeFs;
@@ -15,11 +11,11 @@ use gpui::{AppContext, TestAppContext, Timer};
 use http_client::StatusCode;
 use indoc::{formatdoc, indoc};
 use language_model::{
-    LanguageModelRegistry, LanguageModelRequestTool, LanguageModelToolResult,
-    LanguageModelToolResultContent, LanguageModelToolUse, LanguageModelToolUseId, SelectedModel,
+    LanguageModelRegistry, LanguageModelToolResult, LanguageModelToolResultContent,
+    LanguageModelToolUse, LanguageModelToolUseId, SelectedModel,
 };
 use project::Project;
-use prompt_store::{ModelContext, ProjectContext, PromptBuilder, WorktreeContext};
+use prompt_store::{ProjectContext, WorktreeContext};
 use rand::prelude::*;
 use reqwest_client::ReqwestClient;
 use serde_json::json;
@@ -35,7 +31,7 @@ use std::{
 use util::path;
 
 #[test]
-#[cfg_attr(not(feature = "eval"), ignore)]
+#[cfg_attr(not(feature = "edit-agent-eval"), ignore)]
 fn eval_extract_handle_command_output() {
     // Test how well agent generates multiple edit hunks.
     //
@@ -112,7 +108,7 @@ fn eval_extract_handle_command_output() {
 }
 
 #[test]
-#[cfg_attr(not(feature = "eval"), ignore)]
+#[cfg_attr(not(feature = "edit-agent-eval"), ignore)]
 fn eval_delete_run_git_blame() {
     // Model                       | Pass rate
     // ----------------------------|----------
@@ -121,6 +117,7 @@ fn eval_delete_run_git_blame() {
     // 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");
@@ -174,7 +171,7 @@ fn eval_delete_run_git_blame() {
 }
 
 #[test]
-#[cfg_attr(not(feature = "eval"), ignore)]
+#[cfg_attr(not(feature = "edit-agent-eval"), ignore)]
 fn eval_translate_doc_comments() {
     //  Model                          | Pass rate
     // ============================================
@@ -184,6 +181,7 @@ fn eval_translate_doc_comments() {
     //  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";
@@ -236,7 +234,7 @@ fn eval_translate_doc_comments() {
 }
 
 #[test]
-#[cfg_attr(not(feature = "eval"), ignore)]
+#[cfg_attr(not(feature = "edit-agent-eval"), ignore)]
 fn eval_use_wasi_sdk_in_compile_parser_to_wasm() {
     //  Model                          | Pass rate
     // ============================================
@@ -246,6 +244,7 @@ fn eval_use_wasi_sdk_in_compile_parser_to_wasm() {
     //  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");
@@ -361,7 +360,7 @@ fn eval_use_wasi_sdk_in_compile_parser_to_wasm() {
 }
 
 #[test]
-#[cfg_attr(not(feature = "eval"), ignore)]
+#[cfg_attr(not(feature = "edit-agent-eval"), ignore)]
 fn eval_disable_cursor_blinking() {
     //  Model                          | Pass rate
     // ============================================
@@ -371,6 +370,7 @@ fn eval_disable_cursor_blinking() {
     //  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`";
@@ -446,7 +446,7 @@ fn eval_disable_cursor_blinking() {
 }
 
 #[test]
-#[cfg_attr(not(feature = "eval"), ignore)]
+#[cfg_attr(not(feature = "edit-agent-eval"), ignore)]
 fn eval_from_pixels_constructor() {
     // Results for 2025-06-13
     //
@@ -463,6 +463,7 @@ fn eval_from_pixels_constructor() {
     //  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.";
@@ -655,7 +656,7 @@ fn eval_from_pixels_constructor() {
 }
 
 #[test]
-#[cfg_attr(not(feature = "eval"), ignore)]
+#[cfg_attr(not(feature = "edit-agent-eval"), ignore)]
 fn eval_zode() {
     //  Model                          | Pass rate
     // ============================================
@@ -665,6 +666,7 @@ fn eval_zode() {
     //  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";
@@ -761,7 +763,7 @@ fn eval_zode() {
 }
 
 #[test]
-#[cfg_attr(not(feature = "eval"), ignore)]
+#[cfg_attr(not(feature = "edit-agent-eval"), ignore)]
 fn eval_add_overwrite_test() {
     //  Model                          | Pass rate
     // ============================================
@@ -771,6 +773,7 @@ fn eval_add_overwrite_test() {
     //  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";
@@ -992,7 +995,7 @@ fn eval_add_overwrite_test() {
 }
 
 #[test]
-#[cfg_attr(not(feature = "eval"), ignore)]
+#[cfg_attr(not(feature = "edit-agent-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
@@ -1010,7 +1013,7 @@ fn eval_create_empty_file() {
     //
     // 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(
@@ -1475,24 +1478,32 @@ impl EditAgentTest {
             language::init(cx);
             language_model::init(client.clone(), cx);
             language_models::init(user_store, client.clone(), cx);
-            crate::init(client.http_client(), 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-3-7-sonnet-latest".into()),
+            &std::env::var("ZED_AGENT_MODEL").unwrap_or("anthropic/claude-4-sonnet-latest".into()),
         )
         .unwrap();
         let judge_model = SelectedModel::from_str(
-            &std::env::var("ZED_JUDGE_MODEL")
-                .unwrap_or("anthropic/claude-3-7-sonnet-latest".into()),
+            &std::env::var("ZED_JUDGE_MODEL").unwrap_or("anthropic/claude-4-sonnet-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::<Vec<_>>()
+            })
+        });
         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())
@@ -1553,39 +1564,27 @@ impl EditAgentTest {
             .update(cx, |project, cx| project.open_buffer(path, cx))
             .await
             .unwrap();
-        let tools = cx.update(|cx| {
-            ToolRegistry::default_global(cx)
-                .tools()
-                .into_iter()
-                .filter_map(|tool| {
-                    let input_schema = tool
-                        .input_schema(self.agent.model.tool_input_format())
-                        .ok()?;
-                    Some(LanguageModelRequestTool {
-                        name: tool.name(),
-                        description: tool.description(),
-                        input_schema,
-                    })
-                })
-                .collect::<Vec<_>>()
-        });
-        let tool_names = tools
-            .iter()
-            .map(|tool| tool.name.clone())
-            .collect::<Vec<_>>();
-        let worktrees = vec![WorktreeContext {
-            root_name: "root".to_string(),
-            abs_path: Path::new("/path/to/root").into(),
-            rules_file: None,
-        }];
-        let prompt_builder = PromptBuilder::new(None)?;
-        let project_context = ProjectContext::new(worktrees, Vec::default());
-        let system_prompt = prompt_builder.generate_assistant_system_prompt(
-            &project_context,
-            &ModelContext {
+
+        let tools = crate::built_in_tools().collect::<Vec<_>>();
+
+        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::<Vec<_>>();
+            let template = crate::SystemPromptTemplate {
+                project: &project_context,
                 available_tools: tool_names,
-            },
-        )?;
+            };
+            let templates = Templates::new();
+            template.render(&templates).unwrap()
+        };
 
         let has_system_prompt = eval
             .conversation

crates/assistant_tools/src/edit_agent/streaming_fuzzy_matcher.rs → crates/agent/src/edit_agent/streaming_fuzzy_matcher.rs 🔗

@@ -308,12 +308,13 @@ mod tests {
     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(
-            0,
+            ReplicaId::LOCAL,
             BufferId::new(1).unwrap(),
             "Hello world\nThis is a test\nFoo bar baz",
         );
@@ -327,7 +328,7 @@ mod tests {
     #[test]
     fn test_streaming_exact_match() {
         let buffer = TextBuffer::new(
-            0,
+            ReplicaId::LOCAL,
             BufferId::new(1).unwrap(),
             "Hello world\nThis is a test\nFoo bar baz",
         );
@@ -351,7 +352,7 @@ mod tests {
     #[test]
     fn test_streaming_fuzzy_match() {
         let buffer = TextBuffer::new(
-            0,
+            ReplicaId::LOCAL,
             BufferId::new(1).unwrap(),
             indoc! {"
                 function foo(a, b) {
@@ -385,7 +386,7 @@ mod tests {
     #[test]
     fn test_incremental_improvement() {
         let buffer = TextBuffer::new(
-            0,
+            ReplicaId::LOCAL,
             BufferId::new(1).unwrap(),
             "Line 1\nLine 2\nLine 3\nLine 4\nLine 5",
         );
@@ -410,7 +411,7 @@ mod tests {
     #[test]
     fn test_incomplete_lines_buffering() {
         let buffer = TextBuffer::new(
-            0,
+            ReplicaId::LOCAL,
             BufferId::new(1).unwrap(),
             indoc! {"
                 The quick brown fox
@@ -437,7 +438,7 @@ mod tests {
     #[test]
     fn test_multiline_fuzzy_match() {
         let buffer = TextBuffer::new(
-            0,
+            ReplicaId::LOCAL,
             BufferId::new(1).unwrap(),
             indoc! {r#"
                 impl Display for User {
@@ -691,7 +692,11 @@ mod tests {
             }
         "#};
 
-        let buffer = TextBuffer::new(0, BufferId::new(1).unwrap(), text.to_string());
+        let buffer = TextBuffer::new(
+            ReplicaId::LOCAL,
+            BufferId::new(1).unwrap(),
+            text.to_string(),
+        );
         let snapshot = buffer.snapshot();
         let mut matcher = StreamingFuzzyMatcher::new(snapshot.clone());
 
@@ -724,7 +729,7 @@ mod tests {
     #[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(0, BufferId::new(1).unwrap(), text.clone());
+        let buffer = TextBuffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), text.clone());
         let snapshot = buffer.snapshot();
 
         let mut matcher = StreamingFuzzyMatcher::new(snapshot);

crates/agent2/src/history_store.rs → crates/agent/src/history_store.rs 🔗

@@ -1,15 +1,16 @@
-use crate::{DbThreadMetadata, ThreadsDatabase};
+use crate::{DbThread, DbThreadMetadata, ThreadsDatabase};
 use acp_thread::MentionUri;
 use agent_client_protocol as acp;
 use anyhow::{Context as _, Result, anyhow};
-use assistant_context::{AssistantContext, SavedContextMetadata};
+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::contexts_dir;
+use paths::text_threads_dir;
+use project::Project;
 use serde::{Deserialize, Serialize};
-use std::{collections::VecDeque, path::Path, sync::Arc, time::Duration};
+use std::{collections::VecDeque, path::Path, rc::Rc, sync::Arc, time::Duration};
 use ui::ElementId;
 use util::ResultExt as _;
 
@@ -19,24 +20,53 @@ 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<HistoryStore>,
+    project: Entity<Project>,
+    cx: &mut App,
+) -> Task<Result<Entity<crate::Thread>>> {
+    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::<crate::NativeAgentConnection>().unwrap();
+        cx.update(|cx| agent.load_thread(session_id, cx))?.await
+    })
+}
+
 #[derive(Clone, Debug)]
 pub enum HistoryEntry {
     AcpThread(DbThreadMetadata),
-    TextThread(SavedContextMetadata),
+    TextThread(SavedTextThreadMetadata),
 }
 
 impl HistoryEntry {
     pub fn updated_at(&self) -> DateTime<Utc> {
         match self {
             HistoryEntry::AcpThread(thread) => thread.updated_at,
-            HistoryEntry::TextThread(context) => context.mtime.to_utc(),
+            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(context) => HistoryEntryId::TextThread(context.path.clone()),
+            HistoryEntry::TextThread(text_thread) => {
+                HistoryEntryId::TextThread(text_thread.path.clone())
+            }
         }
     }
 
@@ -46,18 +76,23 @@ impl HistoryEntry {
                 id: thread.id.clone(),
                 name: thread.title.to_string(),
             },
-            HistoryEntry::TextThread(context) => MentionUri::TextThread {
-                path: context.path.as_ref().to_owned(),
-                name: context.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,
-            HistoryEntry::AcpThread(thread) => &thread.title,
-            HistoryEntry::TextThread(context) => &context.title,
+            HistoryEntry::AcpThread(thread) => {
+                if thread.title.is_empty() {
+                    DEFAULT_TITLE
+                } else {
+                    &thread.title
+                }
+            }
+            HistoryEntry::TextThread(text_thread) => &text_thread.title,
         }
     }
 }
@@ -87,7 +122,7 @@ enum SerializedRecentOpen {
 pub struct HistoryStore {
     threads: Vec<DbThreadMetadata>,
     entries: Vec<HistoryEntry>,
-    context_store: Entity<assistant_context::ContextStore>,
+    text_thread_store: Entity<assistant_text_thread::TextThreadStore>,
     recently_opened_entries: VecDeque<HistoryEntryId>,
     _subscriptions: Vec<gpui::Subscription>,
     _save_recently_opened_entries_task: Task<()>,
@@ -95,10 +130,11 @@ pub struct HistoryStore {
 
 impl HistoryStore {
     pub fn new(
-        context_store: Entity<assistant_context::ContextStore>,
+        text_thread_store: Entity<assistant_text_thread::TextThreadStore>,
         cx: &mut Context<Self>,
     ) -> Self {
-        let subscriptions = vec![cx.observe(&context_store, |this, _, cx| this.update_entries(cx))];
+        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;
@@ -114,7 +150,7 @@ impl HistoryStore {
         .detach();
 
         Self {
-            context_store,
+            text_thread_store,
             recently_opened_entries: VecDeque::default(),
             threads: Vec::default(),
             entries: Vec::default(),
@@ -127,6 +163,18 @@ impl HistoryStore {
         self.threads.iter().find(|thread| &thread.id == session_id)
     }
 
+    pub fn load_thread(
+        &mut self,
+        id: acp::SessionId,
+        cx: &mut Context<Self>,
+    ) -> Task<Result<Option<DbThread>>> {
+        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,
@@ -145,19 +193,17 @@ impl HistoryStore {
         path: Arc<Path>,
         cx: &mut Context<Self>,
     ) -> Task<Result<()>> {
-        self.context_store.update(cx, |context_store, cx| {
-            context_store.delete_local_context(path, cx)
-        })
+        self.text_thread_store
+            .update(cx, |store, cx| store.delete_local(path, cx))
     }
 
     pub fn load_text_thread(
         &self,
         path: Arc<Path>,
         cx: &mut Context<Self>,
-    ) -> Task<Result<Entity<AssistantContext>>> {
-        self.context_store.update(cx, |context_store, cx| {
-            context_store.open_local_context(path, cx)
-        })
+    ) -> Task<Result<Entity<TextThread>>> {
+        self.text_thread_store
+            .update(cx, |store, cx| store.open_local(path, cx))
     }
 
     pub fn reload(&self, cx: &mut Context<Self>) {
@@ -197,9 +243,9 @@ impl HistoryStore {
         let mut history_entries = Vec::new();
         history_entries.extend(self.threads.iter().cloned().map(HistoryEntry::AcpThread));
         history_entries.extend(
-            self.context_store
+            self.text_thread_store
                 .read(cx)
-                .unordered_contexts()
+                .unordered_text_threads()
                 .cloned()
                 .map(HistoryEntry::TextThread),
         );
@@ -231,21 +277,21 @@ impl HistoryStore {
                 })
         });
 
-        let context_entries =
-            self.context_store
-                .read(cx)
-                .unordered_contexts()
-                .flat_map(|context| {
-                    self.recently_opened_entries
-                        .iter()
-                        .enumerate()
-                        .flat_map(|(index, entry)| match entry {
-                            HistoryEntryId::TextThread(path) if &context.path == path => {
-                                Some((index, HistoryEntry::TextThread(context.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)
@@ -303,7 +349,7 @@ impl HistoryStore {
                         acp::SessionId(id.as_str().into()),
                     )),
                     SerializedRecentOpen::TextThread(file_name) => Some(
-                        HistoryEntryId::TextThread(contexts_dir().join(file_name).into()),
+                        HistoryEntryId::TextThread(text_threads_dir().join(file_name).into()),
                     ),
                 })
                 .collect();

crates/agent/src/legacy_thread.rs 🔗

@@ -0,0 +1,402 @@
+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<Utc>,
+    pub messages: Vec<SerializedMessage>,
+    #[serde(default)]
+    pub initial_project_snapshot: Option<Arc<ProjectSnapshot>>,
+    #[serde(default)]
+    pub cumulative_token_usage: TokenUsage,
+    #[serde(default)]
+    pub request_token_usage: Vec<TokenUsage>,
+    #[serde(default)]
+    pub detailed_summary_state: DetailedSummaryState,
+    #[serde(default)]
+    pub model: Option<SerializedLanguageModel>,
+    #[serde(default)]
+    pub completion_mode: Option<CompletionMode>,
+    #[serde(default)]
+    pub tool_use_limit_reached: bool,
+    #[serde(default)]
+    pub profile: Option<AgentProfileId>,
+}
+
+#[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<Self> {
+        let saved_thread_json = serde_json::from_slice::<serde_json::Value>(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::<SerializedThreadV0_1_0>(saved_thread_json)?;
+                    Ok(saved_thread.upgrade())
+                }
+                SerializedThread::VERSION => Ok(serde_json::from_value::<SerializedThread>(
+                    saved_thread_json,
+                )?),
+                _ => anyhow::bail!("unrecognized serialized thread version: {version:?}"),
+            },
+            None => {
+                let saved_thread =
+                    serde_json::from_value::<LegacySerializedThread>(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<SerializedMessage> = 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<SerializedMessageSegment>,
+    #[serde(default)]
+    pub tool_uses: Vec<SerializedToolUse>,
+    #[serde(default)]
+    pub tool_results: Vec<SerializedToolResult>,
+    #[serde(default)]
+    pub context: String,
+    #[serde(default)]
+    pub creases: Vec<SerializedCrease>,
+    #[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<String>,
+    },
+    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<serde_json::Value>,
+}
+
+#[derive(Serialize, Deserialize)]
+struct LegacySerializedThread {
+    pub summary: SharedString,
+    pub updated_at: DateTime<Utc>,
+    pub messages: Vec<LegacySerializedMessage>,
+    #[serde(default)]
+    pub initial_project_snapshot: Option<Arc<ProjectSnapshot>>,
+}
+
+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<SerializedToolUse>,
+    #[serde(default)]
+    pub tool_results: Vec<SerializedToolResult>,
+}
+
+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
+            }
+        )
+    }
+}

crates/agent2/src/native_agent_server.rs → crates/agent/src/native_agent_server.rs 🔗

@@ -81,7 +81,7 @@ impl AgentServer for NativeAgentServer {
 mod tests {
     use super::*;
 
-    use assistant_context::ContextStore;
+    use assistant_text_thread::TextThreadStore;
     use gpui::AppContext;
 
     agent_servers::e2e_tests::common_e2e_tests!(
@@ -116,8 +116,9 @@ mod tests {
             });
 
             let history = cx.update(|cx| {
-                let context_store = cx.new(move |cx| ContextStore::fake(project.clone(), cx));
-                cx.new(move |cx| HistoryStore::new(context_store, cx))
+                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)

crates/assistant_tool/src/outline.rs → crates/agent/src/outline.rs 🔗

@@ -1,8 +1,6 @@
-use action_log::ActionLog;
-use anyhow::{Context as _, Result};
+use anyhow::Result;
 use gpui::{AsyncApp, Entity};
 use language::{Buffer, OutlineItem, ParseStatus};
-use project::Project;
 use regex::Regex;
 use std::fmt::Write;
 use text::Point;
@@ -11,51 +9,66 @@ use text::Point;
 /// we automatically provide the file's symbol outline instead, with line numbers.
 pub const AUTO_OUTLINE_SIZE: usize = 16384;
 
-pub async fn file_outline(
-    project: Entity<Project>,
-    path: String,
-    action_log: Entity<ActionLog>,
-    regex: Option<Regex>,
-    cx: &mut AsyncApp,
-) -> anyhow::Result<String> {
-    let buffer = {
-        let project_path = project.read_with(cx, |project, cx| {
-            project
-                .find_project_path(&path, cx)
-                .with_context(|| format!("Path {path} not found in project"))
-        })??;
-
-        project
-            .update(cx, |project, cx| project.open_buffer(project_path, cx))?
-            .await?
-    };
+/// 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,
+}
 
-    action_log.update(cx, |action_log, cx| {
-        action_log.buffer_read(buffer.clone(), cx);
-    })?;
+/// 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<Buffer>,
+    path: Option<&str>,
+    cx: &AsyncApp,
+) -> Result<BufferContent> {
+    let file_size = buffer.read_with(cx, |buffer, _| buffer.text().len())?;
 
-    // Wait until the buffer has been fully parsed, so that we can read its outline.
-    let mut parse_status = buffer.read_with(cx, |buffer, _| buffer.parse_status())?;
-    while *parse_status.borrow() != ParseStatus::Idle {
-        parse_status.changed().await?;
-    }
+    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
+        let mut parse_status = buffer.read_with(cx, |buffer, _| buffer.parse_status())?;
+        while *parse_status.borrow() != ParseStatus::Idle {
+            parse_status.changed().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::<Vec<_>>()
+        })?;
 
-    let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot())?;
-    let outline = snapshot.outline(None);
-
-    render_outline(
-        outline
-            .items
-            .into_iter()
-            .map(|item| item.to_point(&snapshot)),
-        regex,
-        0,
-        usize::MAX,
-    )
-    .await
+        let outline_text = render_outline(outline_items, None, 0, usize::MAX).await?;
+
+        let text = if let Some(path) = path {
+            format!(
+                "# File outline for {path} (file too large to show full content)\n\n{outline_text}",
+            )
+        } else {
+            format!("# File outline (file too large to show full content)\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,
+        })
+    }
 }
 
-pub async fn render_outline(
+async fn render_outline(
     items: impl IntoIterator<Item = OutlineItem<Point>>,
     regex: Option<Regex>,
     offset: usize,
@@ -128,62 +141,3 @@ fn render_entries(
 
     entries_rendered
 }
-
-/// 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<Buffer>,
-    path: Option<&str>,
-    cx: &AsyncApp,
-) -> Result<BufferContent> {
-    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
-        let mut parse_status = buffer.read_with(cx, |buffer, _| buffer.parse_status())?;
-        while *parse_status.borrow() != ParseStatus::Idle {
-            parse_status.changed().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::<Vec<_>>()
-        })?;
-
-        let outline_text = render_outline(outline_items, None, 0, usize::MAX).await?;
-
-        let text = if let Some(path) = path {
-            format!(
-                "# File outline for {path} (file too large to show full content)\n\n{outline_text}",
-            )
-        } else {
-            format!("# File outline (file too large to show full content)\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,
-        })
-    }
-}

crates/agent2/src/tests/mod.rs → crates/agent/src/tests/mod.rs 🔗

@@ -975,9 +975,9 @@ async fn test_mcp_tools(cx: &mut TestAppContext) {
         vec![context_server::types::Tool {
             name: "echo".into(),
             description: None,
-            input_schema: serde_json::to_value(
-                EchoTool.input_schema(LanguageModelToolSchemaFormat::JsonSchema),
-            )
+            input_schema: serde_json::to_value(EchoTool::input_schema(
+                LanguageModelToolSchemaFormat::JsonSchema,
+            ))
             .unwrap(),
             output_schema: None,
             annotations: None,
@@ -1149,9 +1149,9 @@ async fn test_mcp_tool_truncation(cx: &mut TestAppContext) {
             context_server::types::Tool {
                 name: "echo".into(), // Conflicts with native EchoTool
                 description: None,
-                input_schema: serde_json::to_value(
-                    EchoTool.input_schema(LanguageModelToolSchemaFormat::JsonSchema),
-                )
+                input_schema: serde_json::to_value(EchoTool::input_schema(
+                    LanguageModelToolSchemaFormat::JsonSchema,
+                ))
                 .unwrap(),
                 output_schema: None,
                 annotations: None,
@@ -1174,9 +1174,9 @@ async fn test_mcp_tool_truncation(cx: &mut TestAppContext) {
             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),
-                )
+                input_schema: serde_json::to_value(EchoTool::input_schema(
+                    LanguageModelToolSchemaFormat::JsonSchema,
+                ))
                 .unwrap(),
                 output_schema: None,
                 annotations: None,
@@ -1834,8 +1834,9 @@ async fn test_agent_connection(cx: &mut TestAppContext) {
     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 context_store = cx.new(|cx| assistant_context::ContextStore::fake(project.clone(), cx));
-    let history_store = cx.new(|cx| HistoryStore::new(context_store, cx));
+    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(
@@ -1864,7 +1865,7 @@ async fn test_agent_connection(cx: &mut TestAppContext) {
     let selector_opt = connection.model_selector(&session_id);
     assert!(
         selector_opt.is_some(),
-        "agent2 should always support ModelSelector"
+        "agent should always support ModelSelector"
     );
     let selector = selector_opt.unwrap();
 
@@ -1995,7 +1996,7 @@ async fn test_tool_updates_to_completion(cx: &mut TestAppContext) {
             locations: vec![],
             raw_input: Some(json!({})),
             raw_output: None,
-            meta: None,
+            meta: Some(json!({ "tool_name": "thinking" })),
         }
     );
     let update = expect_tool_call_update_fields(&mut events).await;

crates/agent/src/thread.rs 🔗

@@ -1,95 +1,60 @@
 use crate::{
-    agent_profile::AgentProfile,
-    context::{AgentContext, AgentContextHandle, ContextLoadResult, LoadedContext},
-    thread_store::{
-        SerializedCrease, SerializedLanguageModel, SerializedMessage, SerializedMessageSegment,
-        SerializedThread, SerializedToolResult, SerializedToolUse, SharedProjectContext,
-        ThreadStore,
-    },
-    tool_use::{PendingToolUse, ToolUse, ToolUseMetadata, ToolUseState},
+    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, AgentSettings, CompletionMode, SUMMARIZE_THREAD_DETAILED_PROMPT,
-    SUMMARIZE_THREAD_PROMPT,
+    AgentProfileId, AgentProfileSettings, AgentSettings, CompletionMode,
+    SUMMARIZE_THREAD_DETAILED_PROMPT, SUMMARIZE_THREAD_PROMPT,
 };
-use anyhow::{Result, anyhow};
-use assistant_tool::{AnyToolCard, Tool, ToolWorkingSet};
+use anyhow::{Context as _, Result, anyhow};
 use chrono::{DateTime, Utc};
-use client::{ModelRequestUsage, RequestUsage};
+use client::{ModelRequestUsage, RequestUsage, UserStore};
 use cloud_llm_client::{CompletionIntent, CompletionRequestStatus, Plan, UsageLimit};
-use collections::HashMap;
-use futures::{FutureExt, StreamExt as _, future::Shared};
-use git::repository::DiffType;
+use collections::{HashMap, HashSet, IndexMap};
+use fs::Fs;
+use futures::stream;
+use futures::{
+    FutureExt,
+    channel::{mpsc, oneshot},
+    future::Shared,
+    stream::FuturesUnordered,
+};
 use gpui::{
-    AnyWindowHandle, App, AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task,
-    WeakEntity, Window,
+    App, AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task, WeakEntity,
 };
-use http_client::StatusCode;
 use language_model::{
-    ConfiguredModel, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent,
-    LanguageModelExt as _, LanguageModelId, LanguageModelRegistry, LanguageModelRequest,
+    LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelExt,
+    LanguageModelImage, LanguageModelProviderId, LanguageModelRegistry, LanguageModelRequest,
     LanguageModelRequestMessage, LanguageModelRequestTool, LanguageModelToolResult,
-    LanguageModelToolResultContent, LanguageModelToolUse, LanguageModelToolUseId, MessageContent,
-    ModelRequestLimitReachedError, PaymentRequiredError, Role, SelectedModel, StopReason,
-    TokenUsage,
-};
-use postage::stream::Stream as _;
-use project::{
-    Project,
-    git_store::{GitStore, GitStoreCheckpoint, RepositoryState},
+    LanguageModelToolResultContent, LanguageModelToolSchemaFormat, LanguageModelToolUse,
+    LanguageModelToolUseId, Role, SelectedModel, StopReason, TokenUsage, ZED_CLOUD_PROVIDER_ID,
 };
-use prompt_store::{ModelContext, PromptBuilder};
-use schemars::JsonSchema;
+use project::Project;
+use prompt_store::ProjectContext;
+use schemars::{JsonSchema, Schema};
 use serde::{Deserialize, Serialize};
-use settings::Settings;
+use settings::{Settings, update_settings_file};
+use smol::stream::StreamExt;
 use std::{
-    io::Write,
-    ops::Range,
+    collections::BTreeMap,
+    ops::RangeInclusive,
+    path::Path,
+    rc::Rc,
     sync::Arc,
     time::{Duration, Instant},
 };
-use thiserror::Error;
-use util::{ResultExt as _, post_inc};
+use std::{fmt::Write, path::PathBuf};
+use util::{ResultExt, debug_panic, markdown::MarkdownCodeBlock};
 use uuid::Uuid;
 
-const MAX_RETRY_ATTEMPTS: u8 = 4;
-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, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Serialize, Deserialize, JsonSchema,
-)]
-pub struct ThreadId(Arc<str>);
-
-impl ThreadId {
-    pub fn new() -> Self {
-        Self(Uuid::new_v4().to_string().into())
-    }
-}
-
-impl std::fmt::Display for ThreadId {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        write!(f, "{}", self.0)
-    }
-}
-
-impl From<&str> for ThreadId {
-    fn from(value: &str) -> Self {
-        Self(value.into())
-    }
-}
+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.
 ///
@@ -109,2014 +74,1898 @@ impl std::fmt::Display for PromptId {
     }
 }
 
-#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize)]
-pub struct MessageId(pub usize);
-
-impl MessageId {
-    fn post_inc(&mut self) -> Self {
-        Self(post_inc(&mut self.0))
-    }
-
-    pub fn as_usize(&self) -> usize {
-        self.0
-    }
-}
+pub(crate) const MAX_RETRY_ATTEMPTS: u8 = 4;
+pub(crate) const BASE_RETRY_DELAY: Duration = Duration::from_secs(5);
 
-/// Stored information that can be used to resurrect a context crease when creating an editor for a past message.
-#[derive(Clone, Debug)]
-pub struct MessageCrease {
-    pub range: Range<usize>,
-    pub icon_path: SharedString,
-    pub label: SharedString,
-    /// None for a deserialized message, Some otherwise.
-    pub context: Option<AgentContextHandle>,
+#[derive(Debug, Clone)]
+enum RetryStrategy {
+    ExponentialBackoff {
+        initial_delay: Duration,
+        max_attempts: u8,
+    },
+    Fixed {
+        delay: Duration,
+        max_attempts: u8,
+    },
 }
 
-/// A message in a [`Thread`].
-#[derive(Debug, Clone)]
-pub struct Message {
-    pub id: MessageId,
-    pub role: Role,
-    pub segments: Vec<MessageSegment>,
-    pub loaded_context: LoadedContext,
-    pub creases: Vec<MessageCrease>,
-    pub is_hidden: bool,
-    pub ui_only: bool,
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub enum Message {
+    User(UserMessage),
+    Agent(AgentMessage),
+    Resume,
 }
 
 impl Message {
-    /// Returns whether the message contains any meaningful text that should be displayed
-    /// The model sometimes runs tool without producing any text or just a marker ([`USING_TOOL_MARKER`])
-    pub fn should_display_content(&self) -> bool {
-        self.segments.iter().all(|segment| segment.should_display())
+    pub fn as_agent_message(&self) -> Option<&AgentMessage> {
+        match self {
+            Message::Agent(agent_message) => Some(agent_message),
+            _ => None,
+        }
     }
 
-    pub fn push_thinking(&mut self, text: &str, signature: Option<String>) {
-        if let Some(MessageSegment::Thinking {
-            text: segment,
-            signature: current_signature,
-        }) = self.segments.last_mut()
-        {
-            if let Some(signature) = signature {
-                *current_signature = Some(signature);
-            }
-            segment.push_str(text);
-        } else {
-            self.segments.push(MessageSegment::Thinking {
-                text: text.to_string(),
-                signature,
-            });
+    pub fn to_request(&self) -> Vec<LanguageModelRequestMessage> {
+        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,
+            }],
         }
     }
 
-    pub fn push_redacted_thinking(&mut self, data: String) {
-        self.segments.push(MessageSegment::RedactedThinking(data));
+    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 push_text(&mut self, text: &str) {
-        if let Some(MessageSegment::Text(segment)) = self.segments.last_mut() {
-            segment.push_str(text);
-        } else {
-            self.segments.push(MessageSegment::Text(text.to_string()));
+    pub fn role(&self) -> Role {
+        match self {
+            Message::User(_) | Message::Resume => Role::User,
+            Message::Agent(_) => Role::Assistant,
         }
     }
+}
 
-    pub fn to_message_content(&self) -> String {
-        let mut result = String::new();
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct UserMessage {
+    pub id: UserMessageId,
+    pub content: Vec<UserMessageContent>,
+}
 
-        if !self.loaded_context.text.is_empty() {
-            result.push_str(&self.loaded_context.text);
-        }
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub enum UserMessageContent {
+    Text(String),
+    Mention { uri: MentionUri, content: String },
+    Image(LanguageModelImage),
+}
 
-        for segment in &self.segments {
-            match segment {
-                MessageSegment::Text(text) => result.push_str(text),
-                MessageSegment::Thinking { text, .. } => {
-                    result.push_str("<think>\n");
-                    result.push_str(text);
-                    result.push_str("\n</think>");
+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("<image />\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());
+                    }
                 }
-                MessageSegment::RedactedThinking(_) => {}
             }
         }
 
-        result
+        markdown
     }
-}
 
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub enum MessageSegment {
-    Text(String),
-    Thinking {
-        text: String,
-        signature: Option<String>,
-    },
-    RedactedThinking(String),
-}
+    fn to_request(&self) -> LanguageModelRequestMessage {
+        let mut message = LanguageModelRequestMessage {
+            role: Role::User,
+            content: Vec::with_capacity(self.content.len()),
+            cache: false,
+        };
 
-impl MessageSegment {
-    pub fn should_display(&self) -> bool {
-        match self {
-            Self::Text(text) => text.is_empty(),
-            Self::Thinking { text, .. } => text.is_empty(),
-            Self::RedactedThinking(_) => false,
+        const OPEN_CONTEXT: &str = "<context>\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 = "<files>";
+        const OPEN_DIRECTORIES_TAG: &str = "<directories>";
+        const OPEN_SYMBOLS_TAG: &str = "<symbols>";
+        const OPEN_SELECTIONS_TAG: &str = "<selections>";
+        const OPEN_THREADS_TAG: &str = "<threads>";
+        const OPEN_FETCH_TAG: &str = "<fetched_urls>";
+        const OPEN_RULES_TAG: &str =
+            "<rules>\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);
         }
-    }
 
-    pub fn text(&self) -> Option<&str> {
-        match self {
-            MessageSegment::Text(text) => Some(text),
-            _ => None,
+        let len_before_context = message.content.len();
+
+        if file_context.len() > OPEN_FILES_TAG.len() {
+            file_context.push_str("</files>\n");
+            message
+                .content
+                .push(language_model::MessageContent::Text(file_context));
         }
-    }
-}
 
-#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
-pub struct ProjectSnapshot {
-    pub worktree_snapshots: Vec<WorktreeSnapshot>,
-    pub timestamp: DateTime<Utc>,
-}
+        if directory_context.len() > OPEN_DIRECTORIES_TAG.len() {
+            directory_context.push_str("</directories>\n");
+            message
+                .content
+                .push(language_model::MessageContent::Text(directory_context));
+        }
 
-#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
-pub struct WorktreeSnapshot {
-    pub worktree_path: String,
-    pub git_state: Option<GitState>,
-}
+        if symbol_context.len() > OPEN_SYMBOLS_TAG.len() {
+            symbol_context.push_str("</symbols>\n");
+            message
+                .content
+                .push(language_model::MessageContent::Text(symbol_context));
+        }
 
-#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
-pub struct GitState {
-    pub remote_url: Option<String>,
-    pub head_sha: Option<String>,
-    pub current_branch: Option<String>,
-    pub diff: Option<String>,
-}
+        if selection_context.len() > OPEN_SELECTIONS_TAG.len() {
+            selection_context.push_str("</selections>\n");
+            message
+                .content
+                .push(language_model::MessageContent::Text(selection_context));
+        }
 
-#[derive(Clone, Debug)]
-pub struct ThreadCheckpoint {
-    message_id: MessageId,
-    git_checkpoint: GitStoreCheckpoint,
-}
+        if thread_context.len() > OPEN_THREADS_TAG.len() {
+            thread_context.push_str("</threads>\n");
+            message
+                .content
+                .push(language_model::MessageContent::Text(thread_context));
+        }
 
-#[derive(Copy, Clone, Debug, PartialEq, Eq)]
-pub enum ThreadFeedback {
-    Positive,
-    Negative,
-}
+        if fetch_context.len() > OPEN_FETCH_TAG.len() {
+            fetch_context.push_str("</fetched_urls>\n");
+            message
+                .content
+                .push(language_model::MessageContent::Text(fetch_context));
+        }
 
-pub enum LastRestoreCheckpoint {
-    Pending {
-        message_id: MessageId,
-    },
-    Error {
-        message_id: MessageId,
-        error: String,
-    },
-}
+        if rules_context.len() > OPEN_RULES_TAG.len() {
+            rules_context.push_str("</user_rules>\n");
+            message
+                .content
+                .push(language_model::MessageContent::Text(rules_context));
+        }
 
-impl LastRestoreCheckpoint {
-    pub fn message_id(&self) -> MessageId {
-        match self {
-            LastRestoreCheckpoint::Pending { message_id } => *message_id,
-            LastRestoreCheckpoint::Error { message_id, .. } => *message_id,
+        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("</context>".into()));
         }
+
+        message
     }
 }
 
-#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
-pub enum DetailedSummaryState {
-    #[default]
-    NotGenerated,
-    Generating {
-        message_id: MessageId,
-    },
-    Generated {
-        text: SharedString,
-        message_id: MessageId,
-    },
-}
+fn codeblock_tag(full_path: &Path, line_range: Option<&RangeInclusive<u32>>) -> 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());
 
-impl DetailedSummaryState {
-    fn text(&self) -> Option<SharedString> {
-        if let Self::Generated { text, .. } = self {
-            Some(text.clone())
+    if let Some(range) = line_range {
+        if range.start() == range.end() {
+            let _ = write!(result, ":{}", range.start() + 1);
         } else {
-            None
+            let _ = write!(result, ":{}-{}", range.start() + 1, range.end() + 1);
         }
     }
-}
 
-#[derive(Default, Debug)]
-pub struct TotalTokenUsage {
-    pub total: u64,
-    pub max: u64,
+    result
 }
 
-impl TotalTokenUsage {
-    pub fn ratio(&self) -> TokenUsageRatio {
-        #[cfg(debug_assertions)]
-        let warning_threshold: f32 = std::env::var("ZED_THREAD_WARNING_THRESHOLD")
-            .unwrap_or("0.8".to_string())
-            .parse()
-            .unwrap();
-        #[cfg(not(debug_assertions))]
-        let warning_threshold: f32 = 0.8;
-
-        // When the maximum is unknown because there is no selected model,
-        // avoid showing the token limit warning.
-        if self.max == 0 {
-            TokenUsageRatio::Normal
-        } else if self.total >= self.max {
-            TokenUsageRatio::Exceeded
-        } else if self.total as f32 / self.max as f32 >= warning_threshold {
-            TokenUsageRatio::Warning
-        } else {
-            TokenUsageRatio::Normal
+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("<think>");
+                    markdown.push_str(text);
+                    markdown.push_str("</think>\n");
+                }
+                AgentMessageContent::RedactedThinking(_) => {
+                    markdown.push_str("<redacted_thinking />\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, "<image />\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 add(&self, tokens: u64) -> TotalTokenUsage {
-        TotalTokenUsage {
-            total: self.total + tokens,
-            max: self.max,
+    pub fn to_request(&self) -> Vec<LanguageModelRequestMessage> {
+        let mut assistant_message = LanguageModelRequestMessage {
+            role: Role::Assistant,
+            content: Vec::with_capacity(self.content.len()),
+            cache: false,
+        };
+        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,
+        };
+
+        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 = "<Tool returned an empty string>".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(Debug, Default, PartialEq, Eq)]
-pub enum TokenUsageRatio {
-    #[default]
-    Normal,
-    Warning,
-    Exceeded,
+#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct AgentMessage {
+    pub content: Vec<AgentMessageContent>,
+    pub tool_results: IndexMap<LanguageModelToolUseId, LanguageModelToolResult>,
 }
 
-#[derive(Debug, Clone, Copy)]
-pub enum QueueState {
-    Sending,
-    Queued { position: usize },
-    Started,
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub enum AgentMessageContent {
+    Text(String),
+    Thinking {
+        text: String,
+        signature: Option<String>,
+    },
+    RedactedThinking(String),
+    ToolUse(LanguageModelToolUse),
 }
 
-/// A thread of conversation with the LLM.
-pub struct Thread {
-    id: ThreadId,
-    updated_at: DateTime<Utc>,
-    summary: ThreadSummary,
-    pending_summary: Task<Option<()>>,
-    detailed_summary_task: Task<Option<()>>,
-    detailed_summary_tx: postage::watch::Sender<DetailedSummaryState>,
-    detailed_summary_rx: postage::watch::Receiver<DetailedSummaryState>,
-    completion_mode: agent_settings::CompletionMode,
-    messages: Vec<Message>,
-    next_message_id: MessageId,
-    last_prompt_id: PromptId,
-    project_context: SharedProjectContext,
-    checkpoints_by_message: HashMap<MessageId, ThreadCheckpoint>,
-    completion_count: usize,
-    pending_completions: Vec<PendingCompletion>,
-    project: Entity<Project>,
-    prompt_builder: Arc<PromptBuilder>,
-    tools: Entity<ToolWorkingSet>,
-    tool_use: ToolUseState,
-    action_log: Entity<ActionLog>,
-    last_restore_checkpoint: Option<LastRestoreCheckpoint>,
-    pending_checkpoint: Option<ThreadCheckpoint>,
-    initial_project_snapshot: Shared<Task<Option<Arc<ProjectSnapshot>>>>,
-    request_token_usage: Vec<TokenUsage>,
-    cumulative_token_usage: TokenUsage,
-    exceeded_window_error: Option<ExceededWindowError>,
-    tool_use_limit_reached: bool,
-    retry_state: Option<RetryState>,
-    message_feedback: HashMap<MessageId, ThreadFeedback>,
-    last_received_chunk_at: Option<Instant>,
-    request_callback: Option<
-        Box<dyn FnMut(&LanguageModelRequest, &[Result<LanguageModelCompletionEvent, String>])>,
-    >,
-    remaining_turns: u32,
-    configured_model: Option<ConfiguredModel>,
-    profile: AgentProfile,
-    last_error_context: Option<(Arc<dyn LanguageModel>, CompletionIntent)>,
+pub trait TerminalHandle {
+    fn id(&self, cx: &AsyncApp) -> Result<acp::TerminalId>;
+    fn current_output(&self, cx: &AsyncApp) -> Result<acp::TerminalOutputResponse>;
+    fn wait_for_exit(&self, cx: &AsyncApp) -> Result<Shared<Task<acp::TerminalExitStatus>>>;
 }
 
-#[derive(Clone, Debug)]
-struct RetryState {
-    attempt: u8,
-    max_attempts: u8,
-    intent: CompletionIntent,
+pub trait ThreadEnvironment {
+    fn create_terminal(
+        &self,
+        command: String,
+        cwd: Option<PathBuf>,
+        output_byte_limit: Option<u64>,
+        cx: &mut AsyncApp,
+    ) -> Task<Result<Rc<dyn TerminalHandle>>>;
 }
 
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub enum ThreadSummary {
-    Pending,
-    Generating,
-    Ready(SharedString),
-    Error,
+#[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),
 }
 
-impl ThreadSummary {
-    pub const DEFAULT: SharedString = SharedString::new_static("New Thread");
-
-    pub fn or_default(&self) -> SharedString {
-        self.unwrap_or(Self::DEFAULT)
-    }
+#[derive(Debug)]
+pub struct NewTerminal {
+    pub command: String,
+    pub output_byte_limit: Option<u64>,
+    pub cwd: Option<PathBuf>,
+    pub response: oneshot::Sender<Result<Entity<acp_thread::Terminal>>>,
+}
 
-    pub fn unwrap_or(&self, message: impl Into<SharedString>) -> SharedString {
-        self.ready().unwrap_or_else(|| message.into())
-    }
+#[derive(Debug)]
+pub struct ToolCallAuthorization {
+    pub tool_call: acp::ToolCallUpdate,
+    pub options: Vec<acp::PermissionOption>,
+    pub response: oneshot::Sender<acp::PermissionOptionId>,
+}
 
-    pub fn ready(&self) -> Option<SharedString> {
-        match self {
-            ThreadSummary::Ready(summary) => Some(summary.clone()),
-            ThreadSummary::Pending | ThreadSummary::Generating | ThreadSummary::Error => None,
-        }
-    }
+#[derive(Debug, thiserror::Error)]
+enum CompletionError {
+    #[error("max tokens")]
+    MaxTokens,
+    #[error("refusal")]
+    Refusal,
+    #[error(transparent)]
+    Other(#[from] anyhow::Error),
 }
 
-#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
-pub struct ExceededWindowError {
-    /// Model used when last message exceeded context window
-    model_id: LanguageModelId,
-    /// Token count including last message
-    token_count: u64,
+pub struct Thread {
+    id: acp::SessionId,
+    prompt_id: PromptId,
+    updated_at: DateTime<Utc>,
+    title: Option<SharedString>,
+    pending_title_generation: Option<Task<()>>,
+    pending_summary_generation: Option<Shared<Task<Option<SharedString>>>>,
+    summary: Option<SharedString>,
+    messages: Vec<Message>,
+    user_store: Entity<UserStore>,
+    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<RunningTurn>,
+    pending_message: Option<AgentMessage>,
+    tools: BTreeMap<SharedString, Arc<dyn AnyAgentTool>>,
+    tool_use_limit_reached: bool,
+    request_token_usage: HashMap<UserMessageId, language_model::TokenUsage>,
+    #[allow(unused)]
+    cumulative_token_usage: TokenUsage,
+    #[allow(unused)]
+    initial_project_snapshot: Shared<Task<Option<Arc<ProjectSnapshot>>>>,
+    context_server_registry: Entity<ContextServerRegistry>,
+    profile_id: AgentProfileId,
+    project_context: Entity<ProjectContext>,
+    templates: Arc<Templates>,
+    model: Option<Arc<dyn LanguageModel>>,
+    summarization_model: Option<Arc<dyn LanguageModel>>,
+    prompt_capabilities_tx: watch::Sender<acp::PromptCapabilities>,
+    pub(crate) prompt_capabilities_rx: watch::Receiver<acp::PromptCapabilities>,
+    pub(crate) project: Entity<Project>,
+    pub(crate) action_log: Entity<ActionLog>,
 }
 
 impl Thread {
+    fn prompt_capabilities(model: Option<&dyn LanguageModel>) -> acp::PromptCapabilities {
+        let image = model.map_or(true, |model| model.supports_images());
+        acp::PromptCapabilities {
+            meta: None,
+            image,
+            audio: false,
+            embedded_context: true,
+        }
+    }
+
     pub fn new(
         project: Entity<Project>,
-        tools: Entity<ToolWorkingSet>,
-        prompt_builder: Arc<PromptBuilder>,
-        system_prompt: SharedProjectContext,
+        project_context: Entity<ProjectContext>,
+        context_server_registry: Entity<ContextServerRegistry>,
+        templates: Arc<Templates>,
+        model: Option<Arc<dyn LanguageModel>>,
         cx: &mut Context<Self>,
     ) -> Self {
-        let (detailed_summary_tx, detailed_summary_rx) = postage::watch::channel();
-        let configured_model = LanguageModelRegistry::read_global(cx).default_model();
         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: ThreadId::new(),
+            id: acp::SessionId(uuid::Uuid::new_v4().to_string().into()),
+            prompt_id: PromptId::new(),
             updated_at: Utc::now(),
-            summary: ThreadSummary::Pending,
-            pending_summary: Task::ready(None),
-            detailed_summary_task: Task::ready(None),
-            detailed_summary_tx,
-            detailed_summary_rx,
-            completion_mode: AgentSettings::get_global(cx).preferred_completion_mode,
+            title: None,
+            pending_title_generation: None,
+            pending_summary_generation: None,
+            summary: None,
             messages: Vec::new(),
-            next_message_id: MessageId(0),
-            last_prompt_id: PromptId::new(),
-            project_context: system_prompt,
-            checkpoints_by_message: HashMap::default(),
-            completion_count: 0,
-            pending_completions: Vec::new(),
-            project: project.clone(),
-            prompt_builder,
-            tools: tools.clone(),
-            last_restore_checkpoint: None,
-            pending_checkpoint: None,
-            tool_use: ToolUseState::new(tools.clone()),
-            action_log: cx.new(|_| ActionLog::new(project.clone())),
+            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, cx);
+                let project_snapshot = Self::project_snapshot(project.clone(), cx);
                 cx.foreground_executor()
                     .spawn(async move { Some(project_snapshot.await) })
                     .shared()
             },
-            request_token_usage: Vec::new(),
-            cumulative_token_usage: TokenUsage::default(),
-            exceeded_window_error: None,
-            tool_use_limit_reached: false,
-            retry_state: None,
-            message_feedback: HashMap::default(),
-            last_error_context: None,
-            last_received_chunk_at: None,
-            request_callback: None,
-            remaining_turns: u32::MAX,
-            configured_model,
-            profile: AgentProfile::new(profile_id, tools),
+            context_server_registry,
+            profile_id,
+            project_context,
+            templates,
+            model,
+            summarization_model: None,
+            prompt_capabilities_tx,
+            prompt_capabilities_rx,
+            project,
+            action_log,
         }
     }
 
-    pub fn deserialize(
-        id: ThreadId,
-        serialized: SerializedThread,
-        project: Entity<Project>,
-        tools: Entity<ToolWorkingSet>,
-        prompt_builder: Arc<PromptBuilder>,
-        project_context: SharedProjectContext,
-        window: Option<&mut Window>, // None in headless mode
-        cx: &mut Context<Self>,
-    ) -> Self {
-        let next_message_id = MessageId(
-            serialized
-                .messages
-                .last()
-                .map(|message| message.id.0 + 1)
-                .unwrap_or(0),
-        );
-        let tool_use = ToolUseState::from_serialized_messages(
-            tools.clone(),
-            &serialized.messages,
-            project.clone(),
-            window,
-            cx,
-        );
-        let (detailed_summary_tx, detailed_summary_rx) =
-            postage::watch::channel_with(serialized.detailed_summary_state);
-
-        let configured_model = LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
-            serialized
-                .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())
-        });
-
-        let completion_mode = serialized
-            .completion_mode
-            .unwrap_or_else(|| AgentSettings::get_global(cx).preferred_completion_mode);
-        let profile_id = serialized
-            .profile
-            .unwrap_or_else(|| AgentSettings::get_global(cx).default_profile.clone());
+    pub fn id(&self) -> &acp::SessionId {
+        &self.id
+    }
 
-        Self {
-            id,
-            updated_at: serialized.updated_at,
-            summary: ThreadSummary::Ready(serialized.summary),
-            pending_summary: Task::ready(None),
-            detailed_summary_task: Task::ready(None),
-            detailed_summary_tx,
-            detailed_summary_rx,
-            completion_mode,
-            retry_state: None,
-            messages: serialized
-                .messages
-                .into_iter()
-                .map(|message| Message {
-                    id: message.id,
-                    role: message.role,
-                    segments: message
-                        .segments
-                        .into_iter()
-                        .map(|segment| match segment {
-                            SerializedMessageSegment::Text { text } => MessageSegment::Text(text),
-                            SerializedMessageSegment::Thinking { text, signature } => {
-                                MessageSegment::Thinking { text, signature }
+    pub fn replay(
+        &mut self,
+        cx: &mut Context<Self>,
+    ) -> mpsc::UnboundedReceiver<Result<ThreadEvent>> {
+        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)
                             }
-                            SerializedMessageSegment::RedactedThinking { data } => {
-                                MessageSegment::RedactedThinking(data)
+                            AgentMessageContent::RedactedThinking(_) => {}
+                            AgentMessageContent::ToolUse(tool_use) => {
+                                self.replay_tool_call(
+                                    tool_use,
+                                    assistant_message.tool_results.get(&tool_use.id),
+                                    &stream,
+                                    cx,
+                                );
                             }
-                        })
-                        .collect(),
-                    loaded_context: LoadedContext {
-                        contexts: Vec::new(),
-                        text: message.context,
-                        images: Vec::new(),
-                    },
-                    creases: message
-                        .creases
-                        .into_iter()
-                        .map(|crease| MessageCrease {
-                            range: crease.start..crease.end,
-                            icon_path: crease.icon_path,
-                            label: crease.label,
-                            context: None,
-                        })
-                        .collect(),
-                    is_hidden: message.is_hidden,
-                    ui_only: false, // UI-only messages are not persisted
-                })
-                .collect(),
-            next_message_id,
-            last_prompt_id: PromptId::new(),
-            project_context,
-            checkpoints_by_message: HashMap::default(),
-            completion_count: 0,
-            pending_completions: Vec::new(),
-            last_restore_checkpoint: None,
-            pending_checkpoint: None,
-            project: project.clone(),
-            prompt_builder,
-            tools: tools.clone(),
-            tool_use,
-            action_log: cx.new(|_| ActionLog::new(project)),
-            initial_project_snapshot: Task::ready(serialized.initial_project_snapshot).shared(),
-            request_token_usage: serialized.request_token_usage,
-            cumulative_token_usage: serialized.cumulative_token_usage,
-            exceeded_window_error: None,
-            tool_use_limit_reached: serialized.tool_use_limit_reached,
-            message_feedback: HashMap::default(),
-            last_error_context: None,
-            last_received_chunk_at: None,
-            request_callback: None,
-            remaining_turns: u32::MAX,
-            configured_model,
-            profile: AgentProfile::new(profile_id, tools),
+                        }
+                    }
+                }
+                Message::Resume => {}
+            }
         }
+        rx
     }
 
-    pub fn set_request_callback(
-        &mut self,
-        callback: impl 'static
-        + FnMut(&LanguageModelRequest, &[Result<LanguageModelCompletionEvent, String>]),
+    fn replay_tool_call(
+        &self,
+        tool_use: &LanguageModelToolUse,
+        tool_result: Option<&LanguageModelToolResult>,
+        stream: &ThreadEventStream,
+        cx: &mut Context<Self>,
     ) {
-        self.request_callback = Some(Box::new(callback));
-    }
+        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
+                    }
+                })
+        });
 
-    pub fn id(&self) -> &ThreadId {
-        &self.id
-    }
+        let Some(tool) = tool else {
+            stream
+                .0
+                .unbounded_send(Ok(ThreadEvent::ToolCall(acp::ToolCall {
+                    meta: None,
+                    id: acp::ToolCallId(tool_use.id.to_string().into()),
+                    title: tool_use.name.to_string(),
+                    kind: acp::ToolKind::Other,
+                    status: acp::ToolCallStatus::Failed,
+                    content: Vec::new(),
+                    locations: Vec::new(),
+                    raw_input: Some(tool_use.input.clone()),
+                    raw_output: None,
+                })))
+                .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(),
+        );
 
-    pub fn profile(&self) -> &AgentProfile {
-        &self.profile
+        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 {
+                status: Some(
+                    tool_result
+                        .as_ref()
+                        .map_or(acp::ToolCallStatus::Failed, |result| {
+                            if result.is_error {
+                                acp::ToolCallStatus::Failed
+                            } else {
+                                acp::ToolCallStatus::Completed
+                            }
+                        }),
+                ),
+                raw_output: output,
+                ..Default::default()
+            },
+        );
     }
 
-    pub fn set_profile(&mut self, id: AgentProfileId, cx: &mut Context<Self>) {
-        if &id != self.profile.id() {
-            self.profile = AgentProfile::new(id, self.tools.clone());
-            cx.emit(ThreadEvent::ProfileChanged);
+    pub fn from_db(
+        id: acp::SessionId,
+        db_thread: DbThread,
+        project: Entity<Project>,
+        project_context: Entity<ProjectContext>,
+        context_server_registry: Entity<ContextServerRegistry>,
+        templates: Arc<Templates>,
+        cx: &mut Context<Self>,
+    ) -> Self {
+        let profile_id = db_thread
+            .profile
+            .unwrap_or_else(|| AgentSettings::get_global(cx).default_profile.clone());
+        let 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)
+        });
+        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,
         }
     }
 
-    pub fn is_empty(&self) -> bool {
-        self.messages.is_empty()
-    }
+    pub fn to_db(&self, cx: &App) -> Task<DbThread> {
+        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()),
+        };
 
-    pub fn updated_at(&self) -> DateTime<Utc> {
-        self.updated_at
+        cx.background_spawn(async move {
+            let initial_project_snapshot = initial_project_snapshot.await;
+            thread.initial_project_snapshot = initial_project_snapshot;
+            thread
+        })
     }
 
-    pub fn touch_updated_at(&mut self) {
-        self.updated_at = Utc::now();
-    }
+    /// Create a snapshot of the current project state including git information and unsaved buffers.
+    fn project_snapshot(
+        project: Entity<Project>,
+        cx: &mut Context<Self>,
+    ) -> Task<Arc<ProjectSnapshot>> {
+        let task = project::telemetry_snapshot::TelemetrySnapshot::new(&project, cx);
+        cx.spawn(async move |_, _| {
+            let snapshot = task.await;
 
-    pub fn advance_prompt_id(&mut self) {
-        self.last_prompt_id = PromptId::new();
+            Arc::new(ProjectSnapshot {
+                worktree_snapshots: snapshot.worktree_snapshots,
+                timestamp: Utc::now(),
+            })
+        })
     }
 
-    pub fn project_context(&self) -> SharedProjectContext {
-        self.project_context.clone()
+    pub fn project_context(&self) -> &Entity<ProjectContext> {
+        &self.project_context
     }
 
-    pub fn get_or_init_configured_model(&mut self, cx: &App) -> Option<ConfiguredModel> {
-        if self.configured_model.is_none() {
-            self.configured_model = LanguageModelRegistry::read_global(cx).default_model();
-        }
-        self.configured_model.clone()
+    pub fn project(&self) -> &Entity<Project> {
+        &self.project
     }
 
-    pub fn configured_model(&self) -> Option<ConfiguredModel> {
-        self.configured_model.clone()
+    pub fn action_log(&self) -> &Entity<ActionLog> {
+        &self.action_log
     }
 
-    pub fn set_configured_model(&mut self, model: Option<ConfiguredModel>, cx: &mut Context<Self>) {
-        self.configured_model = model;
-        cx.notify();
+    pub fn is_empty(&self) -> bool {
+        self.messages.is_empty() && self.title.is_none()
     }
 
-    pub fn summary(&self) -> &ThreadSummary {
-        &self.summary
+    pub fn model(&self) -> Option<&Arc<dyn LanguageModel>> {
+        self.model.as_ref()
     }
 
-    pub fn set_summary(&mut self, new_summary: impl Into<SharedString>, cx: &mut Context<Self>) {
-        let current_summary = match &self.summary {
-            ThreadSummary::Pending | ThreadSummary::Generating => return,
-            ThreadSummary::Ready(summary) => summary,
-            ThreadSummary::Error => &ThreadSummary::DEFAULT,
-        };
-
-        let mut new_summary = new_summary.into();
-
-        if new_summary.is_empty() {
-            new_summary = ThreadSummary::DEFAULT;
+    pub fn set_model(&mut self, model: Arc<dyn LanguageModel>, cx: &mut Context<Self>) {
+        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()
+    }
 
-        if current_summary != &new_summary {
-            self.summary = ThreadSummary::Ready(new_summary);
-            cx.emit(ThreadEvent::SummaryChanged);
-        }
+    pub fn summarization_model(&self) -> Option<&Arc<dyn LanguageModel>> {
+        self.summarization_model.as_ref()
+    }
+
+    pub fn set_summarization_model(
+        &mut self,
+        model: Option<Arc<dyn LanguageModel>>,
+        cx: &mut Context<Self>,
+    ) {
+        self.summarization_model = model;
+        cx.notify()
     }
 
     pub fn completion_mode(&self) -> CompletionMode {
         self.completion_mode
     }
 
-    pub fn set_completion_mode(&mut self, mode: CompletionMode) {
+    pub fn set_completion_mode(&mut self, mode: CompletionMode, cx: &mut Context<Self>) {
+        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()
     }
 
-    pub fn message(&self, id: MessageId) -> Option<&Message> {
-        let index = self
-            .messages
-            .binary_search_by(|message| message.id.cmp(&id))
-            .ok()?;
-
-        self.messages.get(index)
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn last_message(&self) -> Option<Message> {
+        if let Some(message) = self.pending_message.clone() {
+            Some(Message::Agent(message))
+        } else {
+            self.messages.last().cloned()
+        }
     }
 
-    pub fn messages(&self) -> impl ExactSizeIterator<Item = &Message> {
-        self.messages.iter()
+    pub fn add_default_tools(
+        &mut self,
+        environment: Rc<dyn ThreadEnvironment>,
+        cx: &mut Context<Self>,
+    ) {
+        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(
+            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 is_generating(&self) -> bool {
-        !self.pending_completions.is_empty() || !self.all_tools_finished()
+    pub fn add_tool<T: AgentTool>(&mut self, tool: T) {
+        self.tools.insert(T::name().into(), tool.erase());
     }
 
-    /// Indicates whether streaming of language model events is stale.
-    /// When `is_generating()` is false, this method returns `None`.
-    pub fn is_generation_stale(&self) -> Option<bool> {
-        const STALE_THRESHOLD: u128 = 250;
-
-        self.last_received_chunk_at
-            .map(|instant| instant.elapsed().as_millis() > STALE_THRESHOLD)
+    pub fn remove_tool(&mut self, name: &str) -> bool {
+        self.tools.remove(name).is_some()
     }
 
-    fn received_chunk(&mut self) {
-        self.last_received_chunk_at = Some(Instant::now());
+    pub fn profile(&self) -> &AgentProfileId {
+        &self.profile_id
     }
 
-    pub fn queue_state(&self) -> Option<QueueState> {
-        self.pending_completions
-            .first()
-            .map(|pending_completion| pending_completion.queue_state)
+    pub fn set_profile(&mut self, profile_id: AgentProfileId) {
+        self.profile_id = profile_id;
     }
 
-    pub fn tools(&self) -> &Entity<ToolWorkingSet> {
-        &self.tools
+    pub fn cancel(&mut self, cx: &mut Context<Self>) {
+        if let Some(running_turn) = self.running_turn.take() {
+            running_turn.cancel();
+        }
+        self.flush_pending_message(cx);
     }
 
-    pub fn pending_tool(&self, id: &LanguageModelToolUseId) -> Option<&PendingToolUse> {
-        self.tool_use
-            .pending_tool_uses()
-            .into_iter()
-            .find(|tool_use| &tool_use.id == id)
+    fn update_token_usage(&mut self, update: language_model::TokenUsage, cx: &mut Context<Self>) {
+        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 tools_needing_confirmation(&self) -> impl Iterator<Item = &PendingToolUse> {
-        self.tool_use
-            .pending_tool_uses()
-            .into_iter()
-            .filter(|tool_use| tool_use.status.needs_confirmation())
+    pub fn truncate(&mut self, message_id: UserMessageId, cx: &mut Context<Self>) -> 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 has_pending_tool_uses(&self) -> bool {
-        !self.tool_use.pending_tool_uses().is_empty()
+    pub fn latest_request_token_usage(&self) -> Option<language_model::TokenUsage> {
+        let last_user_message = self.last_user_message()?;
+        let tokens = self.request_token_usage.get(&last_user_message.id)?;
+        Some(*tokens)
     }
 
-    pub fn checkpoint_for_message(&self, id: MessageId) -> Option<ThreadCheckpoint> {
-        self.checkpoints_by_message.get(&id).cloned()
+    pub fn latest_token_usage(&self) -> Option<acp_thread::TokenUsage> {
+        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(),
+        })
     }
 
-    pub fn restore_checkpoint(
+    pub fn resume(
         &mut self,
-        checkpoint: ThreadCheckpoint,
         cx: &mut Context<Self>,
-    ) -> Task<Result<()>> {
-        self.last_restore_checkpoint = Some(LastRestoreCheckpoint::Pending {
-            message_id: checkpoint.message_id,
-        });
-        cx.emit(ThreadEvent::CheckpointChanged);
+    ) -> Result<mpsc::UnboundedReceiver<Result<ThreadEvent>>> {
+        self.messages.push(Message::Resume);
         cx.notify();
 
-        let git_store = self.project().read(cx).git_store().clone();
-        let restore = git_store.update(cx, |git_store, cx| {
-            git_store.restore_checkpoint(checkpoint.git_checkpoint.clone(), cx)
-        });
-
-        cx.spawn(async move |this, cx| {
-            let result = restore.await;
-            this.update(cx, |this, cx| {
-                if let Err(err) = result.as_ref() {
-                    this.last_restore_checkpoint = Some(LastRestoreCheckpoint::Error {
-                        message_id: checkpoint.message_id,
-                        error: err.to_string(),
-                    });
-                } else {
-                    this.truncate(checkpoint.message_id, cx);
-                    this.last_restore_checkpoint = None;
-                }
-                this.pending_checkpoint = None;
-                cx.emit(ThreadEvent::CheckpointChanged);
-                cx.notify();
-            })?;
-            result
-        })
+        log::debug!("Total messages in thread: {}", self.messages.len());
+        self.run_turn(cx)
     }
 
-    fn finalize_pending_checkpoint(&mut self, cx: &mut Context<Self>) {
-        let pending_checkpoint = if self.is_generating() {
-            return;
-        } else if let Some(checkpoint) = self.pending_checkpoint.take() {
-            checkpoint
-        } else {
-            return;
-        };
+    /// 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<T>(
+        &mut self,
+        id: UserMessageId,
+        content: impl IntoIterator<Item = T>,
+        cx: &mut Context<Self>,
+    ) -> Result<mpsc::UnboundedReceiver<Result<ThreadEvent>>>
+    where
+        T: Into<UserMessageContent>,
+    {
+        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::<Vec<_>>();
+        log::debug!("Thread::send content: {:?}", content);
 
-        self.finalize_checkpoint(pending_checkpoint, cx);
+        self.messages
+            .push(Message::User(UserMessage { id, content }));
+        cx.notify();
+
+        log::debug!("Total messages in thread: {}", self.messages.len());
+        self.run_turn(cx)
     }
 
-    fn finalize_checkpoint(
+    #[cfg(feature = "eval")]
+    pub fn proceed(
         &mut self,
-        pending_checkpoint: ThreadCheckpoint,
         cx: &mut Context<Self>,
-    ) {
-        let git_store = self.project.read(cx).git_store().clone();
-        let final_checkpoint = git_store.update(cx, |git_store, cx| git_store.checkpoint(cx));
-        cx.spawn(async move |this, cx| match final_checkpoint.await {
-            Ok(final_checkpoint) => {
-                let equal = git_store
-                    .update(cx, |store, cx| {
-                        store.compare_checkpoints(
-                            pending_checkpoint.git_checkpoint.clone(),
-                            final_checkpoint.clone(),
-                            cx,
-                        )
-                    })?
-                    .await
-                    .unwrap_or(false);
+    ) -> Result<mpsc::UnboundedReceiver<Result<ThreadEvent>>> {
+        self.run_turn(cx)
+    }
 
-                this.update(cx, |this, cx| {
-                    this.pending_checkpoint = if equal {
-                        Some(pending_checkpoint)
-                    } else {
-                        this.insert_checkpoint(pending_checkpoint, cx);
-                        Some(ThreadCheckpoint {
-                            message_id: this.next_message_id,
-                            git_checkpoint: final_checkpoint,
-                        })
+    fn run_turn(
+        &mut self,
+        cx: &mut Context<Self>,
+    ) -> Result<mpsc::UnboundedReceiver<Result<ThreadEvent>>> {
+        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::<Result<ThreadEvent>>();
+        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::<CompletionError>() {
+                            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);
+                            }
+                        }
+                    }
+                }
 
-                Ok(())
-            }
-            Err(_) => this.update(cx, |this, cx| {
-                this.insert_checkpoint(pending_checkpoint, cx)
+                _ = this.update(cx, |this, _| this.running_turn.take());
             }),
-        })
-        .detach();
+        });
+        Ok(events_rx)
     }
 
-    fn insert_checkpoint(&mut self, checkpoint: ThreadCheckpoint, cx: &mut Context<Self>) {
-        self.checkpoints_by_message
-            .insert(checkpoint.message_id, checkpoint);
-        cx.emit(ThreadEvent::CheckpointChanged);
-        cx.notify();
-    }
+    async fn run_turn_internal(
+        this: &WeakEntity<Self>,
+        model: Arc<dyn LanguageModel>,
+        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))??;
 
-    pub fn last_restore_checkpoint(&self) -> Option<&LastRestoreCheckpoint> {
-        self.last_restore_checkpoint.as_ref()
-    }
+            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
+            );
 
-    pub fn truncate(&mut self, message_id: MessageId, cx: &mut Context<Self>) {
-        let Some(message_ix) = self
-            .messages
-            .iter()
-            .rposition(|message| message.id == message_id)
-        else {
-            return;
-        };
-        for deleted_message in self.messages.drain(message_ix..) {
-            self.checkpoints_by_message.remove(&deleted_message.id);
-        }
-        cx.notify();
-    }
+            log::debug!("Calling model.stream_completion, attempt {}", attempt);
 
-    pub fn context_for_message(&self, id: MessageId) -> impl Iterator<Item = &AgentContext> {
-        self.messages
-            .iter()
-            .find(|message| message.id == id)
-            .into_iter()
-            .flat_map(|message| message.loaded_context.contexts.iter())
-    }
+            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;
+                    }
+                }
+            }
 
-    pub fn is_turn_end(&self, ix: usize) -> bool {
-        if self.messages.is_empty() {
-            return false;
-        }
+            let end_turn = tool_results.is_empty();
+            while let Some(tool_result) = tool_results.next().await {
+                log::debug!("Tool finished {:?}", tool_result);
 
-        if !self.is_generating() && ix == self.messages.len() - 1 {
-            return true;
-        }
+                event_stream.update_tool_call_fields(
+                    &tool_result.tool_use_id,
+                    acp::ToolCallUpdateFields {
+                        status: Some(if tool_result.is_error {
+                            acp::ToolCallStatus::Failed
+                        } else {
+                            acp::ToolCallStatus::Completed
+                        }),
+                        raw_output: tool_result.output.clone(),
+                        ..Default::default()
+                    },
+                );
+                this.update(cx, |this, _cx| {
+                    this.pending_message()
+                        .tool_results
+                        .insert(tool_result.tool_use_id.clone(), tool_result);
+                })?;
+            }
 
-        let Some(message) = self.messages.get(ix) else {
-            return false;
-        };
+            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 message.role != Role::Assistant {
-            return false;
+            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;
+            }
         }
-
-        self.messages
-            .get(ix + 1)
-            .and_then(|message| {
-                self.message(message.id)
-                    .map(|next_message| next_message.role == Role::User && !next_message.is_hidden)
-            })
-            .unwrap_or(false)
     }
 
-    pub fn tool_use_limit_reached(&self) -> bool {
-        self.tool_use_limit_reached
-    }
+    fn handle_completion_error(
+        &mut self,
+        error: LanguageModelCompletionError,
+        attempt: u8,
+        plan: Option<Plan>,
+    ) -> Result<acp_thread::RetryStatus> {
+        let Some(model) = self.model.as_ref() else {
+            return Err(anyhow!(error));
+        };
 
-    /// Returns whether all of the tool uses have finished running.
-    pub fn all_tools_finished(&self) -> bool {
-        // If the only pending tool uses left are the ones with errors, then
-        // that means that we've finished running all of the pending tools.
-        self.tool_use
-            .pending_tool_uses()
-            .iter()
-            .all(|pending_tool_use| pending_tool_use.status.is_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
+        };
 
-    /// Returns whether any pending tool uses may perform edits
-    pub fn has_pending_edit_tool_uses(&self) -> bool {
-        self.tool_use
-            .pending_tool_uses()
-            .iter()
-            .filter(|pending_tool_use| !pending_tool_use.status.is_error())
-            .any(|pending_tool_use| pending_tool_use.may_perform_edits)
-    }
+        if !auto_retry {
+            return Err(anyhow!(error));
+        }
 
-    pub fn tool_uses_for_message(&self, id: MessageId, cx: &App) -> Vec<ToolUse> {
-        self.tool_use.tool_uses_for_message(id, &self.project, cx)
-    }
+        let Some(strategy) = Self::retry_strategy_for(&error) else {
+            return Err(anyhow!(error));
+        };
 
-    pub fn tool_results_for_message(
-        &self,
-        assistant_message_id: MessageId,
-    ) -> Vec<&LanguageModelToolResult> {
-        self.tool_use.tool_results_for_message(assistant_message_id)
-    }
+        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));
+        }
 
-    pub fn tool_result(&self, id: &LanguageModelToolUseId) -> Option<&LanguageModelToolResult> {
-        self.tool_use.tool_result(id)
+        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,
+        })
     }
 
-    pub fn output_for_tool(&self, id: &LanguageModelToolUseId) -> Option<&Arc<str>> {
-        match &self.tool_use.tool_result(id)?.content {
-            LanguageModelToolResultContent::Text(text) => Some(text),
-            LanguageModelToolResultContent::Image(_) => {
-                // TODO: We should display image
-                None
+    /// 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<Self>,
+    ) -> Result<Option<Task<LanguageModelToolResult>>> {
+        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),
+            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);
+            }
+            StatusUpdate(CompletionRequestStatus::UsageUpdated { amount, limit }) => {
+                self.update_model_request_usage(amount, limit, cx);
+            }
+            StatusUpdate(
+                CompletionRequestStatus::Started
+                | CompletionRequestStatus::Queued { .. }
+                | CompletionRequestStatus::Failed { .. },
+            ) => {}
+            StatusUpdate(CompletionRequestStatus::ToolUseLimitReached) => {
+                self.tool_use_limit_reached = true;
+            }
+            Stop(StopReason::Refusal) => return Err(CompletionError::Refusal.into()),
+            Stop(StopReason::MaxTokens) => return Err(CompletionError::MaxTokens.into()),
+            Stop(StopReason::ToolUse | StopReason::EndTurn) => {}
         }
-    }
 
-    pub fn card_for_tool(&self, id: &LanguageModelToolUseId) -> Option<AnyToolCard> {
-        self.tool_use.tool_result_card(id).cloned()
+        Ok(None)
     }
 
-    /// Return tools that are both enabled and supported by the model
-    pub fn available_tools(
-        &self,
-        cx: &App,
-        model: Arc<dyn LanguageModel>,
-    ) -> Vec<LanguageModelRequestTool> {
-        if model.supports_tools() {
-            self.profile
-                .enabled_tools(cx)
-                .into_iter()
-                .filter_map(|(name, tool)| {
-                    // Skip tools that cannot be supported
-                    let input_schema = tool.input_schema(model.tool_input_format()).ok()?;
-                    Some(LanguageModelRequestTool {
-                        name: name.into(),
-                        description: tool.description(),
-                        input_schema,
-                    })
-                })
-                .collect()
+    fn handle_text_event(
+        &mut self,
+        new_text: String,
+        event_stream: &ThreadEventStream,
+        cx: &mut Context<Self>,
+    ) {
+        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 {
-            Vec::default()
+            last_message
+                .content
+                .push(AgentMessageContent::Text(new_text));
         }
+
+        cx.notify();
     }
 
-    pub fn insert_user_message(
+    fn handle_thinking_event(
         &mut self,
-        text: impl Into<String>,
-        loaded_context: ContextLoadResult,
-        git_checkpoint: Option<GitStoreCheckpoint>,
-        creases: Vec<MessageCrease>,
+        new_text: String,
+        new_signature: Option<String>,
+        event_stream: &ThreadEventStream,
         cx: &mut Context<Self>,
-    ) -> MessageId {
-        if !loaded_context.referenced_buffers.is_empty() {
-            self.action_log.update(cx, |log, cx| {
-                for buffer in loaded_context.referenced_buffers {
-                    log.buffer_read(buffer, cx);
-                }
+    ) {
+        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,
             });
         }
 
-        let message_id = self.insert_message(
-            Role::User,
-            vec![MessageSegment::Text(text.into())],
-            loaded_context.loaded_context,
-            creases,
-            false,
-            cx,
-        );
-
-        if let Some(git_checkpoint) = git_checkpoint {
-            self.pending_checkpoint = Some(ThreadCheckpoint {
-                message_id,
-                git_checkpoint,
-            });
-        }
-
-        message_id
+        cx.notify();
     }
 
-    pub fn insert_invisible_continue_message(&mut self, cx: &mut Context<Self>) -> MessageId {
-        let id = self.insert_message(
-            Role::User,
-            vec![MessageSegment::Text("Continue where you left off".into())],
-            LoadedContext::default(),
-            vec![],
-            true,
-            cx,
-        );
-        self.pending_checkpoint = None;
-
-        id
+    fn handle_redacted_thinking_event(&mut self, data: String, cx: &mut Context<Self>) {
+        let last_message = self.pending_message();
+        last_message
+            .content
+            .push(AgentMessageContent::RedactedThinking(data));
+        cx.notify();
     }
 
-    pub fn insert_assistant_message(
+    fn handle_tool_use_event(
         &mut self,
-        segments: Vec<MessageSegment>,
+        tool_use: LanguageModelToolUse,
+        event_stream: &ThreadEventStream,
         cx: &mut Context<Self>,
-    ) -> MessageId {
-        self.insert_message(
-            Role::Assistant,
-            segments,
-            LoadedContext::default(),
-            Vec::new(),
-            false,
-            cx,
-        )
-    }
+    ) -> Option<Task<LanguageModelToolResult>> {
+        cx.notify();
 
-    pub fn insert_message(
-        &mut self,
-        role: Role,
-        segments: Vec<MessageSegment>,
-        loaded_context: LoadedContext,
-        creases: Vec<MessageCrease>,
-        is_hidden: bool,
-        cx: &mut Context<Self>,
-    ) -> MessageId {
-        let id = self.next_message_id.post_inc();
-        self.messages.push(Message {
-            id,
-            role,
-            segments,
-            loaded_context,
-            creases,
-            is_hidden,
-            ui_only: false,
+        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
+            }
         });
-        self.touch_updated_at();
-        cx.emit(ThreadEvent::MessageAdded(id));
-        id
-    }
 
-    pub fn edit_message(
-        &mut self,
-        id: MessageId,
-        new_role: Role,
-        new_segments: Vec<MessageSegment>,
-        creases: Vec<MessageCrease>,
-        loaded_context: Option<LoadedContext>,
-        checkpoint: Option<GitStoreCheckpoint>,
-        cx: &mut Context<Self>,
-    ) -> bool {
-        let Some(message) = self.messages.iter_mut().find(|message| message.id == id) else {
-            return false;
-        };
-        message.role = new_role;
-        message.segments = new_segments;
-        message.creases = creases;
-        if let Some(context) = loaded_context {
-            message.loaded_context = context;
-        }
-        if let Some(git_checkpoint) = checkpoint {
-            self.checkpoints_by_message.insert(
-                id,
-                ThreadCheckpoint {
-                    message_id: id,
-                    git_checkpoint,
+        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 {
+                    title: Some(title.into()),
+                    kind: Some(kind),
+                    raw_input: Some(tool_use.input.clone()),
+                    ..Default::default()
                 },
             );
         }
-        self.touch_updated_at();
-        cx.emit(ThreadEvent::MessageEdited(id));
-        true
-    }
 
-    pub fn delete_message(&mut self, id: MessageId, cx: &mut Context<Self>) -> bool {
-        let Some(index) = self.messages.iter().position(|message| message.id == id) else {
-            return false;
-        };
-        self.messages.remove(index);
-        self.touch_updated_at();
-        cx.emit(ThreadEvent::MessageDeleted(id));
-        true
-    }
+        if !tool_use.is_input_complete {
+            return None;
+        }
 
-    /// Returns the representation of this [`Thread`] in a textual form.
-    ///
-    /// This is the representation we use when attaching a thread as context to another thread.
-    pub fn text(&self) -> String {
-        let mut text = String::new();
+        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,
+            }));
+        };
 
-        for message in &self.messages {
-            text.push_str(match message.role {
-                language_model::Role::User => "User:",
-                language_model::Role::Assistant => "Agent:",
-                language_model::Role::System => "System:",
+        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 {
+            status: Some(acp::ToolCallStatus::InProgress),
+            ..Default::default()
+        });
+        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)
             });
-            text.push('\n');
 
-            for segment in &message.segments {
-                match segment {
-                    MessageSegment::Text(content) => text.push_str(content),
-                    MessageSegment::Thinking { text: content, .. } => {
-                        text.push_str(&format!("<think>{}</think>", content))
-                    }
-                    MessageSegment::RedactedThinking(_) => {}
-                }
+            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()),
+                },
             }
-            text.push('\n');
-        }
+        }))
+    }
 
-        text
+    fn handle_tool_use_json_parse_error_event(
+        &mut self,
+        tool_use_id: LanguageModelToolUseId,
+        tool_name: Arc<str>,
+        raw_input: Arc<str>,
+        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())),
+        }
     }
 
-    /// Serializes this thread into a format for storage or telemetry.
-    pub fn serialize(&self, cx: &mut Context<Self>) -> Task<Result<SerializedThread>> {
-        let initial_project_snapshot = self.initial_project_snapshot.clone();
-        cx.spawn(async move |this, cx| {
-            let initial_project_snapshot = initial_project_snapshot.await;
-            this.read_with(cx, |this, cx| SerializedThread {
-                version: SerializedThread::VERSION.to_string(),
-                summary: this.summary().or_default(),
-                updated_at: this.updated_at(),
-                messages: this
-                    .messages()
-                    .filter(|message| !message.ui_only)
-                    .map(|message| SerializedMessage {
-                        id: message.id,
-                        role: message.role,
-                        segments: message
-                            .segments
-                            .iter()
-                            .map(|segment| match segment {
-                                MessageSegment::Text(text) => {
-                                    SerializedMessageSegment::Text { text: text.clone() }
-                                }
-                                MessageSegment::Thinking { text, signature } => {
-                                    SerializedMessageSegment::Thinking {
-                                        text: text.clone(),
-                                        signature: signature.clone(),
-                                    }
-                                }
-                                MessageSegment::RedactedThinking(data) => {
-                                    SerializedMessageSegment::RedactedThinking {
-                                        data: data.clone(),
-                                    }
-                                }
-                            })
-                            .collect(),
-                        tool_uses: this
-                            .tool_uses_for_message(message.id, cx)
-                            .into_iter()
-                            .map(|tool_use| SerializedToolUse {
-                                id: tool_use.id,
-                                name: tool_use.name,
-                                input: tool_use.input,
-                            })
-                            .collect(),
-                        tool_results: this
-                            .tool_results_for_message(message.id)
-                            .into_iter()
-                            .map(|tool_result| SerializedToolResult {
-                                tool_use_id: tool_result.tool_use_id.clone(),
-                                is_error: tool_result.is_error,
-                                content: tool_result.content.clone(),
-                                output: tool_result.output.clone(),
-                            })
-                            .collect(),
-                        context: message.loaded_context.text.clone(),
-                        creases: message
-                            .creases
-                            .iter()
-                            .map(|crease| SerializedCrease {
-                                start: crease.range.start,
-                                end: crease.range.end,
-                                icon_path: crease.icon_path.clone(),
-                                label: crease.label.clone(),
-                            })
-                            .collect(),
-                        is_hidden: message.is_hidden,
-                    })
-                    .collect(),
-                initial_project_snapshot,
-                cumulative_token_usage: this.cumulative_token_usage,
-                request_token_usage: this.request_token_usage.clone(),
-                detailed_summary_state: this.detailed_summary_rx.borrow().clone(),
-                exceeded_window_error: this.exceeded_window_error.clone(),
-                model: this
-                    .configured_model
-                    .as_ref()
-                    .map(|model| SerializedLanguageModel {
-                        provider: model.provider.id().0.to_string(),
-                        model: model.model.id().0.to_string(),
+    fn update_model_request_usage(&self, amount: usize, limit: UsageLimit, cx: &mut Context<Self>) {
+        self.project
+            .read(cx)
+            .user_store()
+            .update(cx, |user_store, cx| {
+                user_store.update_model_request_usage(
+                    ModelRequestUsage(RequestUsage {
+                        amount: amount as i32,
+                        limit,
                     }),
-                completion_mode: Some(this.completion_mode),
-                tool_use_limit_reached: this.tool_use_limit_reached,
-                profile: Some(this.profile.id().clone()),
-            })
-        })
+                    cx,
+                )
+            });
     }
 
-    pub fn remaining_turns(&self) -> u32 {
-        self.remaining_turns
+    pub fn title(&self) -> SharedString {
+        self.title.clone().unwrap_or("New Thread".into())
     }
 
-    pub fn set_remaining_turns(&mut self, remaining_turns: u32) {
-        self.remaining_turns = remaining_turns;
+    pub fn is_generating_summary(&self) -> bool {
+        self.pending_summary_generation.is_some()
     }
 
-    pub fn send_to_model(
-        &mut self,
-        model: Arc<dyn LanguageModel>,
-        intent: CompletionIntent,
-        window: Option<AnyWindowHandle>,
-        cx: &mut Context<Self>,
-    ) {
-        if self.remaining_turns == 0 {
-            return;
+    pub fn summary(&mut self, cx: &mut Context<Self>) -> Shared<Task<Option<SharedString>>> {
+        if let Some(summary) = self.summary.as_ref() {
+            return Task::ready(Some(summary.clone())).shared();
         }
-
-        self.remaining_turns -= 1;
-
-        self.flush_notifications(model.clone(), intent, cx);
-
-        let _checkpoint = self.finalize_pending_checkpoint(cx);
-        self.stream_completion(
-            self.to_completion_request(model.clone(), intent, cx),
-            model,
-            intent,
-            window,
-            cx,
-        );
-    }
-
-    pub fn to_completion_request(
-        &self,
-        model: Arc<dyn LanguageModel>,
-        intent: CompletionIntent,
-        cx: &mut Context<Self>,
-    ) -> LanguageModelRequest {
+        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 {
-            thread_id: Some(self.id.to_string()),
-            prompt_id: Some(self.last_prompt_id.to_string()),
-            intent: Some(intent),
-            mode: None,
-            messages: vec![],
-            tools: Vec::new(),
-            tool_choice: None,
-            stop: Vec::new(),
+            intent: Some(CompletionIntent::ThreadContextSummarization),
             temperature: AgentSettings::temperature_for_model(&model, cx),
-            thinking_allowed: true,
-        };
-
-        let available_tools = self.available_tools(cx, model.clone());
-        let available_tool_names = available_tools
-            .iter()
-            .map(|tool| tool.name.clone())
-            .collect();
-
-        let model_context = &ModelContext {
-            available_tools: available_tool_names,
+            ..Default::default()
         };
 
-        if let Some(project_context) = self.project_context.borrow().as_ref() {
-            match self
-                .prompt_builder
-                .generate_assistant_system_prompt(project_context, model_context)
-            {
-                Err(err) => {
-                    let message = format!("{err:?}").into();
-                    log::error!("{message}");
-                    cx.emit(ThreadEvent::ShowError(ThreadError::Message {
-                        header: "Error generating system prompt".into(),
-                        message,
-                    }));
-                }
-                Ok(system_prompt) => {
-                    request.messages.push(LanguageModelRequestMessage {
-                        role: Role::System,
-                        content: vec![MessageContent::Text(system_prompt)],
-                        cache: true,
-                    });
-                }
-            }
-        } else {
-            let message = "Context for system prompt unexpectedly not ready.".into();
-            log::error!("{message}");
-            cx.emit(ThreadEvent::ShowError(ThreadError::Message {
-                header: "Error generating system prompt".into(),
-                message,
-            }));
-        }
-
-        let mut message_ix_to_cache = None;
         for message in &self.messages {
-            // ui_only messages are for the UI only, not for the model
-            if message.ui_only {
-                continue;
-            }
+            request.messages.extend(message.to_request());
+        }
 
-            let mut request_message = LanguageModelRequestMessage {
-                role: message.role,
-                content: Vec::new(),
-                cache: false,
-            };
+        request.messages.push(LanguageModelRequestMessage {
+            role: Role::User,
+            content: vec![SUMMARIZE_THREAD_DETAILED_PROMPT.into()],
+            cache: false,
+        });
 
-            message
-                .loaded_context
-                .add_to_request_message(&mut request_message);
-
-            for segment in &message.segments {
-                match segment {
-                    MessageSegment::Text(text) => {
-                        let text = text.trim_end();
-                        if !text.is_empty() {
-                            request_message
-                                .content
-                                .push(MessageContent::Text(text.into()));
-                        }
-                    }
-                    MessageSegment::Thinking { text, signature } => {
-                        if !text.is_empty() {
-                            request_message.content.push(MessageContent::Thinking {
-                                text: text.into(),
-                                signature: signature.clone(),
-                            });
+        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::StatusUpdate(
+                            CompletionRequestStatus::UsageUpdated { amount, limit },
+                        ) => {
+                            this.update(cx, |thread, cx| {
+                                thread.update_model_request_usage(amount, limit, cx);
+                            })
+                            .ok()?;
+                            continue;
                         }
-                    }
-                    MessageSegment::RedactedThinking(data) => {
-                        request_message
-                            .content
-                            .push(MessageContent::RedactedThinking(data.clone()));
-                    }
-                };
-            }
+                        _ => continue,
+                    };
 
-            let mut cache_message = true;
-            let mut tool_results_message = LanguageModelRequestMessage {
-                role: Role::User,
-                content: Vec::new(),
-                cache: false,
-            };
-            for (tool_use, tool_result) in self.tool_use.tool_results(message.id) {
-                if let Some(tool_result) = tool_result {
-                    request_message
-                        .content
-                        .push(MessageContent::ToolUse(tool_use.clone()));
-                    tool_results_message
-                        .content
-                        .push(MessageContent::ToolResult(LanguageModelToolResult {
-                            tool_use_id: tool_use.id.clone(),
-                            tool_name: tool_result.tool_name.clone(),
-                            is_error: tool_result.is_error,
-                            content: if tool_result.content.is_empty() {
-                                // Surprisingly, the API fails if we return an empty string here.
-                                // It thinks we are sending a tool use without a tool result.
-                                "<Tool returned an empty string>".into()
-                            } else {
-                                tool_result.content.clone()
-                            },
-                            output: None,
-                        }));
-                } else {
-                    cache_message = false;
-                    log::debug!(
-                        "skipped tool use {:?} because it is still pending",
-                        tool_use
-                    );
+                    let mut lines = text.lines();
+                    summary.extend(lines.next());
                 }
-            }
 
-            if cache_message {
-                message_ix_to_cache = Some(request.messages.len());
-            }
-            request.messages.push(request_message);
+                log::debug!("Setting summary: {}", summary);
+                let summary = SharedString::from(summary);
 
-            if !tool_results_message.content.is_empty() {
-                if cache_message {
-                    message_ix_to_cache = Some(request.messages.len());
-                }
-                request.messages.push(tool_results_message);
-            }
-        }
+                this.update(cx, |this, cx| {
+                    this.summary = Some(summary.clone());
+                    this.pending_summary_generation = None;
+                    cx.notify()
+                })
+                .ok()?;
 
-        // https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching
-        if let Some(message_ix_to_cache) = message_ix_to_cache {
-            request.messages[message_ix_to_cache].cache = true;
-        }
+                Some(summary)
+            })
+            .shared();
+        self.pending_summary_generation = Some(task.clone());
+        task
+    }
 
-        request.tools = available_tools;
-        request.mode = if model.supports_burn_mode() {
-            Some(self.completion_mode.into())
-        } else {
-            Some(CompletionMode::Normal.into())
+    fn generate_title(&mut self, cx: &mut Context<Self>) {
+        let Some(model) = self.summarization_model.clone() else {
+            return;
         };
 
-        request
-    }
-
-    fn to_summarize_request(
-        &self,
-        model: &Arc<dyn LanguageModel>,
-        intent: CompletionIntent,
-        added_user_message: String,
-        cx: &App,
-    ) -> LanguageModelRequest {
+        log::debug!(
+            "Generating title with model: {:?}",
+            self.summarization_model.as_ref().map(|model| model.name())
+        );
         let mut request = LanguageModelRequest {
-            thread_id: None,
-            prompt_id: None,
-            intent: Some(intent),
-            mode: None,
-            messages: vec![],
-            tools: Vec::new(),
-            tool_choice: None,
-            stop: Vec::new(),
-            temperature: AgentSettings::temperature_for_model(model, cx),
-            thinking_allowed: false,
+            intent: Some(CompletionIntent::ThreadSummarization),
+            temperature: AgentSettings::temperature_for_model(&model, cx),
+            ..Default::default()
         };
 
         for message in &self.messages {
-            let mut request_message = LanguageModelRequestMessage {
-                role: message.role,
-                content: Vec::new(),
-                cache: false,
-            };
-
-            for segment in &message.segments {
-                match segment {
-                    MessageSegment::Text(text) => request_message
-                        .content
-                        .push(MessageContent::Text(text.clone())),
-                    MessageSegment::Thinking { .. } => {}
-                    MessageSegment::RedactedThinking(_) => {}
-                }
-            }
-
-            if request_message.content.is_empty() {
-                continue;
-            }
-
-            request.messages.push(request_message);
+            request.messages.extend(message.to_request());
         }
 
         request.messages.push(LanguageModelRequestMessage {
             role: Role::User,
-            content: vec![MessageContent::Text(added_user_message)],
+            content: vec![SUMMARIZE_THREAD_PROMPT.into()],
             cache: false,
         });
+        self.pending_title_generation = Some(cx.spawn(async move |this, cx| {
+            let mut title = String::new();
 
-        request
-    }
+            let generate = async {
+                let mut messages = model.stream_completion(request, cx).await?;
+                while let Some(event) = messages.next().await {
+                    let event = event?;
+                    let text = match event {
+                        LanguageModelCompletionEvent::Text(text) => text,
+                        LanguageModelCompletionEvent::StatusUpdate(
+                            CompletionRequestStatus::UsageUpdated { amount, limit },
+                        ) => {
+                            this.update(cx, |thread, cx| {
+                                thread.update_model_request_usage(amount, limit, cx);
+                            })?;
+                            continue;
+                        }
+                        _ => continue,
+                    };
 
-    /// Insert auto-generated notifications (if any) to the thread
-    fn flush_notifications(
-        &mut self,
-        model: Arc<dyn LanguageModel>,
-        intent: CompletionIntent,
-        cx: &mut Context<Self>,
-    ) {
-        match intent {
-            CompletionIntent::UserPrompt | CompletionIntent::ToolResults => {
-                if let Some(pending_tool_use) = self.attach_tracked_files_state(model, cx) {
-                    cx.emit(ThreadEvent::ToolFinished {
-                        tool_use_id: pending_tool_use.id.clone(),
-                        pending_tool_use: Some(pending_tool_use),
-                    });
+                    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));
             }
-            CompletionIntent::ThreadSummarization
-            | CompletionIntent::ThreadContextSummarization
-            | CompletionIntent::CreateFile
-            | CompletionIntent::EditFile
-            | CompletionIntent::InlineAssist
-            | CompletionIntent::TerminalInlineAssist
-            | CompletionIntent::GenerateGitCommitMessage => {}
-        };
+            _ = this.update(cx, |this, _| this.pending_title_generation = None);
+        }));
     }
 
-    fn attach_tracked_files_state(
-        &mut self,
-        model: Arc<dyn LanguageModel>,
-        cx: &mut App,
-    ) -> Option<PendingToolUse> {
-        // Represent notification as a simulated `project_notifications` tool call
-        let tool_name = Arc::from("project_notifications");
-        let tool = self.tools.read(cx).tool(&tool_name, cx)?;
-
-        if !self.profile.is_tool_enabled(tool.source(), tool.name(), cx) {
-            return None;
+    pub fn set_title(&mut self, title: SharedString, cx: &mut Context<Self>) {
+        self.pending_title_generation = None;
+        if Some(&title) != self.title.as_ref() {
+            self.title = Some(title);
+            cx.emit(TitleUpdated);
+            cx.notify();
         }
+    }
 
-        if self
-            .action_log
-            .update(cx, |log, cx| log.unnotified_user_edits(cx).is_none())
-        {
-            return None;
-        }
+    fn clear_summary(&mut self) {
+        self.summary = None;
+        self.pending_summary_generation = None;
+    }
 
-        let input = serde_json::json!({});
-        let request = Arc::new(LanguageModelRequest::default()); // unused
-        let window = None;
-        let tool_result = tool.run(
-            input,
-            request,
-            self.project.clone(),
-            self.action_log.clone(),
-            model.clone(),
-            window,
-            cx,
-        );
+    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,
+            })
+    }
 
-        let tool_use_id =
-            LanguageModelToolUseId::from(format!("project_notifications_{}", self.messages.len()));
+    fn pending_message(&mut self) -> &mut AgentMessage {
+        self.pending_message.get_or_insert_default()
+    }
 
-        let tool_use = LanguageModelToolUse {
-            id: tool_use_id.clone(),
-            name: tool_name.clone(),
-            raw_input: "{}".to_string(),
-            input: serde_json::json!({}),
-            is_input_complete: true,
+    fn flush_pending_message(&mut self, cx: &mut Context<Self>) {
+        let Some(mut message) = self.pending_message.take() else {
+            return;
         };
 
-        let tool_output = cx.background_executor().block(tool_result.output);
+        if message.content.is_empty() {
+            return;
+        }
 
-        // Attach a project_notification tool call to the latest existing
-        // Assistant message. We cannot create a new Assistant message
-        // because thinking models require a `thinking` block that we
-        // cannot mock. We cannot send a notification as a normal
-        // (non-tool-use) User message because this distracts Agent
-        // too much.
-        let tool_message_id = self
-            .messages
-            .iter()
-            .enumerate()
-            .rfind(|(_, message)| message.role == Role::Assistant)
-            .map(|(_, message)| message.id)?;
-
-        let tool_use_metadata = ToolUseMetadata {
-            model: model.clone(),
-            thread_id: self.id.clone(),
-            prompt_id: self.last_prompt_id.clone(),
-        };
+        for content in &message.content {
+            let AgentMessageContent::ToolUse(tool_use) = content else {
+                continue;
+            };
 
-        self.tool_use
-            .request_tool_use(tool_message_id, tool_use, tool_use_metadata, cx);
+            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.tool_use.insert_tool_output(
-            tool_use_id,
-            tool_name,
-            tool_output,
-            self.configured_model.as_ref(),
-            self.completion_mode,
-        )
+        self.messages.push(Message::Agent(message));
+        self.updated_at = Utc::now();
+        self.clear_summary();
+        cx.notify()
     }
 
-    pub fn stream_completion(
-        &mut self,
-        request: LanguageModelRequest,
-        model: Arc<dyn LanguageModel>,
-        intent: CompletionIntent,
-        window: Option<AnyWindowHandle>,
-        cx: &mut Context<Self>,
-    ) {
-        self.tool_use_limit_reached = false;
-
-        let pending_completion_id = post_inc(&mut self.completion_count);
-        let mut request_callback_parameters = if self.request_callback.is_some() {
-            Some((request.clone(), Vec::new()))
+    pub(crate) fn build_completion_request(
+        &self,
+        completion_intent: CompletionIntent,
+        cx: &App,
+    ) -> Result<LanguageModelRequest> {
+        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::<Vec<_>>()
         } else {
-            None
-        };
-        let prompt_id = self.last_prompt_id.clone();
-        let tool_use_metadata = ToolUseMetadata {
-            model: model.clone(),
-            thread_id: self.id.clone(),
-            prompt_id: prompt_id.clone(),
+            Vec::new()
         };
 
-        let completion_mode = request
-            .mode
-            .unwrap_or(cloud_llm_client::CompletionMode::Normal);
-
-        self.last_received_chunk_at = Some(Instant::now());
-
-        let task = cx.spawn(async move |thread, cx| {
-            let stream_completion_future = model.stream_completion(request, cx);
-            let initial_token_usage =
-                thread.read_with(cx, |thread, _cx| thread.cumulative_token_usage);
-            let stream_completion = async {
-                let mut events = stream_completion_future.await?;
+        log::debug!("Building completion request");
+        log::debug!("Completion intent: {:?}", completion_intent);
+        log::debug!("Completion mode: {:?}", self.completion_mode);
 
-                let mut stop_reason = StopReason::EndTurn;
-                let mut current_token_usage = TokenUsage::default();
+        let messages = self.build_request_messages(cx);
+        log::debug!("Request will include {} messages", messages.len());
+        log::debug!("Request includes {} tools", tools.len());
 
-                thread
-                    .update(cx, |_thread, cx| {
-                        cx.emit(ThreadEvent::NewRequest);
-                    })
-                    .ok();
+        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,
+        };
 
-                let mut request_assistant_message_id = None;
+        log::debug!("Completion request built successfully");
+        Ok(request)
+    }
 
-                while let Some(event) = events.next().await {
-                    if let Some((_, response_events)) = request_callback_parameters.as_mut() {
-                        response_events
-                            .push(event.as_ref().map_err(|error| error.to_string()).cloned());
-                    }
-
-                    thread.update(cx, |thread, cx| {
-                        match event? {
-                            LanguageModelCompletionEvent::StartMessage { .. } => {
-                                request_assistant_message_id =
-                                    Some(thread.insert_assistant_message(
-                                        vec![MessageSegment::Text(String::new())],
-                                        cx,
-                                    ));
-                            }
-                            LanguageModelCompletionEvent::Stop(reason) => {
-                                stop_reason = reason;
-                            }
-                            LanguageModelCompletionEvent::UsageUpdate(token_usage) => {
-                                thread.update_token_usage_at_last_message(token_usage);
-                                thread.cumulative_token_usage = thread.cumulative_token_usage
-                                    + token_usage
-                                    - current_token_usage;
-                                current_token_usage = token_usage;
-                            }
-                            LanguageModelCompletionEvent::Text(chunk) => {
-                                thread.received_chunk();
-
-                                cx.emit(ThreadEvent::ReceivedTextChunk);
-                                if let Some(last_message) = thread.messages.last_mut() {
-                                    if last_message.role == Role::Assistant
-                                        && !thread.tool_use.has_tool_results(last_message.id)
-                                    {
-                                        last_message.push_text(&chunk);
-                                        cx.emit(ThreadEvent::StreamedAssistantText(
-                                            last_message.id,
-                                            chunk,
-                                        ));
-                                    } else {
-                                        // If we won't have an Assistant message yet, assume this chunk marks the beginning
-                                        // of a new Assistant response.
-                                        //
-                                        // Importantly: We do *not* want to emit a `StreamedAssistantText` event here, as it
-                                        // will result in duplicating the text of the chunk in the rendered Markdown.
-                                        request_assistant_message_id =
-                                            Some(thread.insert_assistant_message(
-                                                vec![MessageSegment::Text(chunk.to_string())],
-                                                cx,
-                                            ));
-                                    };
-                                }
-                            }
-                            LanguageModelCompletionEvent::Thinking {
-                                text: chunk,
-                                signature,
-                            } => {
-                                thread.received_chunk();
-
-                                if let Some(last_message) = thread.messages.last_mut() {
-                                    if last_message.role == Role::Assistant
-                                        && !thread.tool_use.has_tool_results(last_message.id)
-                                    {
-                                        last_message.push_thinking(&chunk, signature);
-                                        cx.emit(ThreadEvent::StreamedAssistantThinking(
-                                            last_message.id,
-                                            chunk,
-                                        ));
-                                    } else {
-                                        // If we won't have an Assistant message yet, assume this chunk marks the beginning
-                                        // of a new Assistant response.
-                                        //
-                                        // Importantly: We do *not* want to emit a `StreamedAssistantText` event here, as it
-                                        // will result in duplicating the text of the chunk in the rendered Markdown.
-                                        request_assistant_message_id =
-                                            Some(thread.insert_assistant_message(
-                                                vec![MessageSegment::Thinking {
-                                                    text: chunk.to_string(),
-                                                    signature,
-                                                }],
-                                                cx,
-                                            ));
-                                    };
-                                }
-                            }
-                            LanguageModelCompletionEvent::RedactedThinking { data } => {
-                                thread.received_chunk();
-
-                                if let Some(last_message) = thread.messages.last_mut() {
-                                    if last_message.role == Role::Assistant
-                                        && !thread.tool_use.has_tool_results(last_message.id)
-                                    {
-                                        last_message.push_redacted_thinking(data);
-                                    } else {
-                                        request_assistant_message_id =
-                                            Some(thread.insert_assistant_message(
-                                                vec![MessageSegment::RedactedThinking(data)],
-                                                cx,
-                                            ));
-                                    };
-                                }
-                            }
-                            LanguageModelCompletionEvent::ToolUse(tool_use) => {
-                                let last_assistant_message_id = request_assistant_message_id
-                                    .unwrap_or_else(|| {
-                                        let new_assistant_message_id =
-                                            thread.insert_assistant_message(vec![], cx);
-                                        request_assistant_message_id =
-                                            Some(new_assistant_message_id);
-                                        new_assistant_message_id
-                                    });
-
-                                let tool_use_id = tool_use.id.clone();
-                                let streamed_input = if tool_use.is_input_complete {
-                                    None
-                                } else {
-                                    Some(tool_use.input.clone())
-                                };
-
-                                let ui_text = thread.tool_use.request_tool_use(
-                                    last_assistant_message_id,
-                                    tool_use,
-                                    tool_use_metadata.clone(),
-                                    cx,
-                                );
-
-                                if let Some(input) = streamed_input {
-                                    cx.emit(ThreadEvent::StreamedToolUse {
-                                        tool_use_id,
-                                        ui_text,
-                                        input,
-                                    });
-                                }
-                            }
-                            LanguageModelCompletionEvent::ToolUseJsonParseError {
-                                id,
-                                tool_name,
-                                raw_input: invalid_input_json,
-                                json_parse_error,
-                            } => {
-                                thread.receive_invalid_tool_json(
-                                    id,
-                                    tool_name,
-                                    invalid_input_json,
-                                    json_parse_error,
-                                    window,
-                                    cx,
-                                );
-                            }
-                            LanguageModelCompletionEvent::StatusUpdate(status_update) => {
-                                if let Some(completion) = thread
-                                    .pending_completions
-                                    .iter_mut()
-                                    .find(|completion| completion.id == pending_completion_id)
-                                {
-                                    match status_update {
-                                        CompletionRequestStatus::Queued { position } => {
-                                            completion.queue_state =
-                                                QueueState::Queued { position };
-                                        }
-                                        CompletionRequestStatus::Started => {
-                                            completion.queue_state = QueueState::Started;
-                                        }
-                                        CompletionRequestStatus::Failed {
-                                            code,
-                                            message,
-                                            request_id: _,
-                                            retry_after,
-                                        } => {
-                                            return Err(
-                                                LanguageModelCompletionError::from_cloud_failure(
-                                                    model.upstream_provider_name(),
-                                                    code,
-                                                    message,
-                                                    retry_after.map(Duration::from_secs_f64),
-                                                ),
-                                            );
-                                        }
-                                        CompletionRequestStatus::UsageUpdated { amount, limit } => {
-                                            thread.update_model_request_usage(
-                                                amount as u32,
-                                                limit,
-                                                cx,
-                                            );
-                                        }
-                                        CompletionRequestStatus::ToolUseLimitReached => {
-                                            thread.tool_use_limit_reached = true;
-                                            cx.emit(ThreadEvent::ToolUseLimitReached);
-                                        }
-                                    }
-                                }
-                            }
-                        }
-
-                        thread.touch_updated_at();
-                        cx.emit(ThreadEvent::StreamedCompletion);
-                        cx.notify();
-
-                        Ok(())
-                    })??;
+    fn enabled_tools(
+        &self,
+        profile: &AgentProfileSettings,
+        model: &Arc<dyn LanguageModel>,
+        cx: &App,
+    ) -> BTreeMap<SharedString, Arc<dyn AnyAgentTool>> {
+        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()
+            }
+        }
 
-                    smol::future::yield_now().await;
+        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
                 }
-
-                thread.update(cx, |thread, cx| {
-                    thread.last_received_chunk_at = None;
-                    thread
-                        .pending_completions
-                        .retain(|completion| completion.id != pending_completion_id);
-
-                    // If there is a response without tool use, summarize the message. Otherwise,
-                    // allow two tool uses before summarizing.
-                    if matches!(thread.summary, ThreadSummary::Pending)
-                        && thread.messages.len() >= 2
-                        && (!thread.has_pending_tool_uses() || thread.messages.len() >= 6)
-                    {
-                        thread.summarize(cx);
-                    }
-                })?;
-
-                anyhow::Ok(stop_reason)
-            };
-
-            let result = stream_completion.await;
-            let mut retry_scheduled = false;
-
-            thread
-                .update(cx, |thread, cx| {
-                    thread.finalize_pending_checkpoint(cx);
-                    match result.as_ref() {
-                        Ok(stop_reason) => {
-                            match stop_reason {
-                                StopReason::ToolUse => {
-                                    let tool_uses =
-                                        thread.use_pending_tools(window, model.clone(), cx);
-                                    cx.emit(ThreadEvent::UsePendingTools { tool_uses });
-                                }
-                                StopReason::EndTurn | StopReason::MaxTokens => {
-                                    thread.project.update(cx, |project, cx| {
-                                        project.set_agent_location(None, cx);
-                                    });
-                                }
-                                StopReason::Refusal => {
-                                    thread.project.update(cx, |project, cx| {
-                                        project.set_agent_location(None, cx);
-                                    });
-
-                                    // Remove the turn that was refused.
-                                    //
-                                    // https://docs.anthropic.com/en/docs/test-and-evaluate/strengthen-guardrails/handle-streaming-refusals#reset-context-after-refusal
-                                    {
-                                        let mut messages_to_remove = Vec::new();
-
-                                        for (ix, message) in
-                                            thread.messages.iter().enumerate().rev()
-                                        {
-                                            messages_to_remove.push(message.id);
-
-                                            if message.role == Role::User {
-                                                if ix == 0 {
-                                                    break;
-                                                }
-
-                                                if let Some(prev_message) =
-                                                    thread.messages.get(ix - 1)
-                                                    && prev_message.role == Role::Assistant {
-                                                        break;
-                                                    }
-                                            }
-                                        }
-
-                                        for message_id in messages_to_remove {
-                                            thread.delete_message(message_id, cx);
-                                        }
-                                    }
-
-                                    cx.emit(ThreadEvent::ShowError(ThreadError::Message {
-                                        header: "Language model refusal".into(),
-                                        message:
-                                            "Model refused to generate content for safety reasons."
-                                                .into(),
-                                    }));
-                                }
-                            }
-
-                            // We successfully completed, so cancel any remaining retries.
-                            thread.retry_state = None;
-                        }
-                        Err(error) => {
-                            thread.project.update(cx, |project, cx| {
-                                project.set_agent_location(None, cx);
-                            });
-
-                            if error.is::<PaymentRequiredError>() {
-                                cx.emit(ThreadEvent::ShowError(ThreadError::PaymentRequired));
-                            } else if let Some(error) =
-                                error.downcast_ref::<ModelRequestLimitReachedError>()
-                            {
-                                cx.emit(ThreadEvent::ShowError(
-                                    ThreadError::ModelRequestLimitReached { plan: error.plan },
-                                ));
-                            } else if let Some(completion_error) =
-                                error.downcast_ref::<LanguageModelCompletionError>()
-                            {
-                                match &completion_error {
-                                    LanguageModelCompletionError::PromptTooLarge {
-                                        tokens, ..
-                                    } => {
-                                        let tokens = tokens.unwrap_or_else(|| {
-                                            // We didn't get an exact token count from the API, so fall back on our estimate.
-                                            thread
-                                                .total_token_usage()
-                                                .map(|usage| usage.total)
-                                                .unwrap_or(0)
-                                                // We know the context window was exceeded in practice, so if our estimate was
-                                                // lower than max tokens, the estimate was wrong; return that we exceeded by 1.
-                                                .max(
-                                                    model
-                                                        .max_token_count_for_mode(completion_mode)
-                                                        .saturating_add(1),
-                                                )
-                                        });
-                                        thread.exceeded_window_error = Some(ExceededWindowError {
-                                            model_id: model.id(),
-                                            token_count: tokens,
-                                        });
-                                        cx.notify();
-                                    }
-                                    _ => {
-                                        if let Some(retry_strategy) =
-                                            Thread::get_retry_strategy(completion_error)
-                                        {
-                                            log::info!(
-                                                "Retrying with {:?} for language model completion error {:?}",
-                                                retry_strategy,
-                                                completion_error
-                                            );
-
-                                            retry_scheduled = thread
-                                                .handle_retryable_error_with_delay(
-                                                    completion_error,
-                                                    Some(retry_strategy),
-                                                    model.clone(),
-                                                    intent,
-                                                    window,
-                                                    cx,
-                                                );
-                                        }
-                                    }
-                                }
-                            }
-
-                            if !retry_scheduled {
-                                thread.cancel_last_completion(window, cx);
-                            }
-                        }
-                    }
-
-                    if !retry_scheduled {
-                        cx.emit(ThreadEvent::Stopped(result.map_err(Arc::new)));
-                    }
-
-                    if let Some((request_callback, (request, response_events))) = thread
-                        .request_callback
-                        .as_mut()
-                        .zip(request_callback_parameters.as_ref())
-                    {
-                        request_callback(request, response_events);
+            })
+            .collect::<BTreeMap<_, _>>();
+
+        let mut context_server_tools = Vec::new();
+        let mut seen_tools = tools.keys().cloned().collect::<HashSet<_>>();
+        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()));
+                }
+            }
+        }
 
-                    if let Ok(initial_usage) = initial_token_usage {
-                        let usage = thread.cumulative_token_usage - initial_usage;
-
-                        telemetry::event!(
-                            "Assistant Thread Completion",
-                            thread_id = thread.id().to_string(),
-                            prompt_id = prompt_id,
-                            model = model.telemetry_id(),
-                            model_provider = model.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,
-                        );
-                    }
-                })
-                .ok();
-        });
+        // 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());
+            }
+        }
 
-        self.pending_completions.push(PendingCompletion {
-            id: pending_completion_id,
-            queue_state: QueueState::Sending,
-            _task: task,
-        });
+        tools
     }
 
-    pub fn summarize(&mut self, cx: &mut Context<Self>) {
-        let Some(model) = LanguageModelRegistry::read_global(cx).thread_summary_model() else {
-            println!("No thread summary model");
-            return;
-        };
-
-        if !model.provider.is_authenticated(cx) {
-            return;
-        }
+    fn tool(&self, name: &str) -> Option<Arc<dyn AnyAgentTool>> {
+        self.running_turn.as_ref()?.tools.get(name).cloned()
+    }
 
-        let request = self.to_summarize_request(
-            &model.model,
-            CompletionIntent::ThreadSummarization,
-            SUMMARIZE_THREAD_PROMPT.into(),
-            cx,
+    fn build_request_messages(&self, cx: &App) -> Vec<LanguageModelRequestMessage> {
+        log::trace!(
+            "Building request messages from {} thread messages",
+            self.messages.len()
         );
 
-        self.summary = ThreadSummary::Generating;
-
-        self.pending_summary = cx.spawn(async move |this, cx| {
-            let result = async {
-                let mut messages = model.model.stream_completion(request, cx).await?;
+        let system_prompt = SystemPromptTemplate {
+            project: self.project_context.read(cx),
+            available_tools: self.tools.keys().cloned().collect(),
+        }
+        .render(&self.templates)
+        .context("failed to build system prompt")
+        .expect("Invalid template");
+        let mut messages = vec![LanguageModelRequestMessage {
+            role: Role::System,
+            content: vec![system_prompt.into()],
+            cache: false,
+        }];
+        for message in &self.messages {
+            messages.extend(message.to_request());
+        }
 
-                let mut new_summary = String::new();
-                while let Some(event) = messages.next().await {
-                    let Ok(event) = event else {
-                        continue;
-                    };
-                    let text = match event {
-                        LanguageModelCompletionEvent::Text(text) => text,
-                        LanguageModelCompletionEvent::StatusUpdate(
-                            CompletionRequestStatus::UsageUpdated { amount, limit },
-                        ) => {
-                            this.update(cx, |thread, cx| {
-                                thread.update_model_request_usage(amount as u32, limit, cx);
-                            })?;
-                            continue;
-                        }
-                        _ => continue,
-                    };
+        if let Some(last_message) = messages.last_mut() {
+            last_message.cache = true;
+        }
 
-                    let mut lines = text.lines();
-                    new_summary.extend(lines.next());
+        if let Some(message) = self.pending_message.as_ref() {
+            messages.extend(message.to_request());
+        }
 
-                    // Stop if the LLM generated multiple lines.
-                    if lines.next().is_some() {
-                        break;
-                    }
-                }
+        messages
+    }
 
-                anyhow::Ok(new_summary)
+    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');
             }
-            .await;
+            markdown.push_str(&message.to_markdown());
+        }
 
-            this.update(cx, |this, cx| {
-                match result {
-                    Ok(new_summary) => {
-                        if new_summary.is_empty() {
-                            this.summary = ThreadSummary::Error;
-                        } else {
-                            this.summary = ThreadSummary::Ready(new_summary.into());
-                        }
-                    }
-                    Err(err) => {
-                        this.summary = ThreadSummary::Error;
-                        log::error!("Failed to generate thread summary: {}", err);
-                    }
-                }
-                cx.emit(ThreadEvent::SummaryGenerated);
-            })
-            .log_err()?;
+        if let Some(message) = self.pending_message.as_ref() {
+            markdown.push('\n');
+            markdown.push_str(&message.to_markdown());
+        }
 
-            Some(())
-        });
+        markdown
+    }
+
+    fn advance_prompt_id(&mut self) {
+        self.prompt_id = PromptId::new();
     }
 
-    fn get_retry_strategy(error: &LanguageModelCompletionError) -> Option<RetryStrategy> {
+    fn retry_strategy_for(error: &LanguageModelCompletionError) -> Option<RetryStrategy> {
         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.

crates/agent/src/thread_store.rs 🔗

@@ -1,1287 +0,0 @@
-use crate::{
-    context_server_tool::ContextServerTool,
-    thread::{
-        DetailedSummaryState, ExceededWindowError, MessageId, ProjectSnapshot, Thread, ThreadId,
-    },
-};
-use agent_settings::{AgentProfileId, CompletionMode};
-use anyhow::{Context as _, Result, anyhow};
-use assistant_tool::{Tool, ToolId, ToolWorkingSet};
-use chrono::{DateTime, Utc};
-use collections::HashMap;
-use context_server::ContextServerId;
-use fs::{Fs, RemoveOptions};
-use futures::{
-    FutureExt as _, StreamExt as _,
-    channel::{mpsc, oneshot},
-    future::{self, BoxFuture, Shared},
-};
-use gpui::{
-    App, BackgroundExecutor, Context, Entity, EventEmitter, Global, ReadGlobal, SharedString,
-    Subscription, Task, Window, prelude::*,
-};
-use indoc::indoc;
-use language_model::{LanguageModelToolResultContent, LanguageModelToolUseId, Role, TokenUsage};
-use project::context_server_store::{ContextServerStatus, ContextServerStore};
-use project::{Project, ProjectItem, ProjectPath, Worktree};
-use prompt_store::{
-    ProjectContext, PromptBuilder, PromptId, PromptStore, PromptsUpdatedEvent, RulesFileContext,
-    UserRulesContext, WorktreeContext,
-};
-use serde::{Deserialize, Serialize};
-use sqlez::{
-    bindable::{Bind, Column},
-    connection::Connection,
-    statement::Statement,
-};
-use std::{
-    cell::{Ref, RefCell},
-    path::{Path, PathBuf},
-    rc::Rc,
-    sync::{Arc, LazyLock, Mutex},
-};
-use util::{ResultExt as _, rel_path::RelPath};
-
-use zed_env_vars::ZED_STATELESS;
-
-#[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<i32> {
-        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))
-    }
-}
-
-static RULES_FILE_NAMES: LazyLock<[&RelPath; 9]> = LazyLock::new(|| {
-    [
-        RelPath::unix(".rules").unwrap(),
-        RelPath::unix(".cursorrules").unwrap(),
-        RelPath::unix(".windsurfrules").unwrap(),
-        RelPath::unix(".clinerules").unwrap(),
-        RelPath::unix(".github/copilot-instructions.md").unwrap(),
-        RelPath::unix("CLAUDE.md").unwrap(),
-        RelPath::unix("AGENT.md").unwrap(),
-        RelPath::unix("AGENTS.md").unwrap(),
-        RelPath::unix("GEMINI.md").unwrap(),
-    ]
-});
-
-pub fn init(fs: Arc<dyn Fs>, cx: &mut App) {
-    ThreadsDatabase::init(fs, cx);
-}
-
-/// A system prompt shared by all threads created by this ThreadStore
-#[derive(Clone, Default)]
-pub struct SharedProjectContext(Rc<RefCell<Option<ProjectContext>>>);
-
-impl SharedProjectContext {
-    pub fn borrow(&self) -> Ref<'_, Option<ProjectContext>> {
-        self.0.borrow()
-    }
-}
-
-pub type TextThreadStore = assistant_context::ContextStore;
-
-pub struct ThreadStore {
-    project: Entity<Project>,
-    tools: Entity<ToolWorkingSet>,
-    prompt_builder: Arc<PromptBuilder>,
-    prompt_store: Option<Entity<PromptStore>>,
-    context_server_tool_ids: HashMap<ContextServerId, Vec<ToolId>>,
-    threads: Vec<SerializedThreadMetadata>,
-    project_context: SharedProjectContext,
-    reload_system_prompt_tx: mpsc::Sender<()>,
-    _reload_system_prompt_task: Task<()>,
-    _subscriptions: Vec<Subscription>,
-}
-
-pub struct RulesLoadingError {
-    pub message: SharedString,
-}
-
-impl EventEmitter<RulesLoadingError> for ThreadStore {}
-
-impl ThreadStore {
-    pub fn load(
-        project: Entity<Project>,
-        tools: Entity<ToolWorkingSet>,
-        prompt_store: Option<Entity<PromptStore>>,
-        prompt_builder: Arc<PromptBuilder>,
-        cx: &mut App,
-    ) -> Task<Result<Entity<Self>>> {
-        cx.spawn(async move |cx| {
-            let (thread_store, ready_rx) = cx.update(|cx| {
-                let mut option_ready_rx = None;
-                let thread_store = cx.new(|cx| {
-                    let (thread_store, ready_rx) =
-                        Self::new(project, tools, prompt_builder, prompt_store, cx);
-                    option_ready_rx = Some(ready_rx);
-                    thread_store
-                });
-                (thread_store, option_ready_rx.take().unwrap())
-            })?;
-            ready_rx.await?;
-            Ok(thread_store)
-        })
-    }
-
-    fn new(
-        project: Entity<Project>,
-        tools: Entity<ToolWorkingSet>,
-        prompt_builder: Arc<PromptBuilder>,
-        prompt_store: Option<Entity<PromptStore>>,
-        cx: &mut Context<Self>,
-    ) -> (Self, oneshot::Receiver<()>) {
-        let mut subscriptions = vec![cx.subscribe(&project, Self::handle_project_event)];
-
-        if let Some(prompt_store) = prompt_store.as_ref() {
-            subscriptions.push(cx.subscribe(
-                prompt_store,
-                |this, _prompt_store, PromptsUpdatedEvent, _cx| {
-                    this.enqueue_system_prompt_reload();
-                },
-            ))
-        }
-
-        // This channel and task prevent concurrent and redundant loading of the system prompt.
-        let (reload_system_prompt_tx, mut reload_system_prompt_rx) = mpsc::channel(1);
-        let (ready_tx, ready_rx) = oneshot::channel();
-        let mut ready_tx = Some(ready_tx);
-        let reload_system_prompt_task = cx.spawn({
-            let prompt_store = prompt_store.clone();
-            async move |thread_store, cx| {
-                loop {
-                    let Some(reload_task) = thread_store
-                        .update(cx, |thread_store, cx| {
-                            thread_store.reload_system_prompt(prompt_store.clone(), cx)
-                        })
-                        .ok()
-                    else {
-                        return;
-                    };
-                    reload_task.await;
-                    if let Some(ready_tx) = ready_tx.take() {
-                        ready_tx.send(()).ok();
-                    }
-                    reload_system_prompt_rx.next().await;
-                }
-            }
-        });
-
-        let this = Self {
-            project,
-            tools,
-            prompt_builder,
-            prompt_store,
-            context_server_tool_ids: HashMap::default(),
-            threads: Vec::new(),
-            project_context: SharedProjectContext::default(),
-            reload_system_prompt_tx,
-            _reload_system_prompt_task: reload_system_prompt_task,
-            _subscriptions: subscriptions,
-        };
-        this.register_context_server_handlers(cx);
-        this.reload(cx).detach_and_log_err(cx);
-        (this, ready_rx)
-    }
-
-    #[cfg(any(test, feature = "test-support"))]
-    pub fn fake(project: Entity<Project>, cx: &mut App) -> Self {
-        Self {
-            project,
-            tools: cx.new(|_| ToolWorkingSet::default()),
-            prompt_builder: Arc::new(PromptBuilder::new(None).unwrap()),
-            prompt_store: None,
-            context_server_tool_ids: HashMap::default(),
-            threads: Vec::new(),
-            project_context: SharedProjectContext::default(),
-            reload_system_prompt_tx: mpsc::channel(0).0,
-            _reload_system_prompt_task: Task::ready(()),
-            _subscriptions: vec![],
-        }
-    }
-
-    fn handle_project_event(
-        &mut self,
-        _project: Entity<Project>,
-        event: &project::Event,
-        _cx: &mut Context<Self>,
-    ) {
-        match event {
-            project::Event::WorktreeAdded(_) | project::Event::WorktreeRemoved(_) => {
-                self.enqueue_system_prompt_reload();
-            }
-            project::Event::WorktreeUpdatedEntries(_, items) => {
-                if items
-                    .iter()
-                    .any(|(path, _, _)| RULES_FILE_NAMES.iter().any(|name| path.as_ref() == *name))
-                {
-                    self.enqueue_system_prompt_reload();
-                }
-            }
-            _ => {}
-        }
-    }
-
-    fn enqueue_system_prompt_reload(&mut self) {
-        self.reload_system_prompt_tx.try_send(()).ok();
-    }
-
-    // Note that this should only be called from `reload_system_prompt_task`.
-    fn reload_system_prompt(
-        &self,
-        prompt_store: Option<Entity<PromptStore>>,
-        cx: &mut Context<Self>,
-    ) -> Task<()> {
-        let worktrees = self
-            .project
-            .read(cx)
-            .visible_worktrees(cx)
-            .collect::<Vec<_>>();
-        let worktree_tasks = worktrees
-            .into_iter()
-            .map(|worktree| {
-                Self::load_worktree_info_for_system_prompt(worktree, self.project.clone(), cx)
-            })
-            .collect::<Vec<_>>();
-        let default_user_rules_task = match prompt_store {
-            None => Task::ready(vec![]),
-            Some(prompt_store) => 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))
-            }),
-        };
-
-        cx.spawn(async move |this, 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)| {
-                    if let Some(rules_error) = rules_error {
-                        this.update(cx, |_, cx| cx.emit(rules_error)).ok();
-                    }
-                    worktree
-                })
-                .collect::<Vec<_>>();
-
-            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 {
-                            PromptId::User { uuid } => uuid,
-                            PromptId::EditWorkflow => return None,
-                        },
-                        title: prompt_metadata.title.map(|title| title.to_string()),
-                        contents,
-                    }),
-                    Err(err) => {
-                        this.update(cx, |_, cx| {
-                            cx.emit(RulesLoadingError {
-                                message: format!("{err:?}").into(),
-                            });
-                        })
-                        .ok();
-                        None
-                    }
-                })
-                .collect::<Vec<_>>();
-
-            this.update(cx, |this, _cx| {
-                *this.project_context.0.borrow_mut() =
-                    Some(ProjectContext::new(worktrees, default_user_rules));
-            })
-            .ok();
-        })
-    }
-
-    fn load_worktree_info_for_system_prompt(
-        worktree: Entity<Worktree>,
-        project: Entity<Project>,
-        cx: &mut App,
-    ) -> Task<(WorktreeContext, Option<RulesLoadingError>)> {
-        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<Worktree>,
-        project: Entity<Project>,
-        cx: &mut App,
-    ) -> Option<Task<Result<RulesFileContext>>> {
-        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(name)
-                    .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(),
-                })
-            })
-        })
-    }
-
-    pub fn prompt_store(&self) -> &Option<Entity<PromptStore>> {
-        &self.prompt_store
-    }
-
-    pub fn tools(&self) -> Entity<ToolWorkingSet> {
-        self.tools.clone()
-    }
-
-    /// Returns the number of threads.
-    pub fn thread_count(&self) -> usize {
-        self.threads.len()
-    }
-
-    pub fn reverse_chronological_threads(&self) -> impl Iterator<Item = &SerializedThreadMetadata> {
-        // ordering is from "ORDER BY" in `list_threads`
-        self.threads.iter()
-    }
-
-    pub fn create_thread(&mut self, cx: &mut Context<Self>) -> Entity<Thread> {
-        cx.new(|cx| {
-            Thread::new(
-                self.project.clone(),
-                self.tools.clone(),
-                self.prompt_builder.clone(),
-                self.project_context.clone(),
-                cx,
-            )
-        })
-    }
-
-    pub fn create_thread_from_serialized(
-        &mut self,
-        serialized: SerializedThread,
-        cx: &mut Context<Self>,
-    ) -> Entity<Thread> {
-        cx.new(|cx| {
-            Thread::deserialize(
-                ThreadId::new(),
-                serialized,
-                self.project.clone(),
-                self.tools.clone(),
-                self.prompt_builder.clone(),
-                self.project_context.clone(),
-                None,
-                cx,
-            )
-        })
-    }
-
-    pub fn open_thread(
-        &self,
-        id: &ThreadId,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) -> Task<Result<Entity<Thread>>> {
-        let id = id.clone();
-        let database_future = ThreadsDatabase::global_future(cx);
-        let this = cx.weak_entity();
-        window.spawn(cx, async move |cx| {
-            let database = database_future.await.map_err(|err| anyhow!(err))?;
-            let thread = database
-                .try_find_thread(id.clone())
-                .await?
-                .with_context(|| format!("no thread found with ID: {id:?}"))?;
-
-            let thread = this.update_in(cx, |this, window, cx| {
-                cx.new(|cx| {
-                    Thread::deserialize(
-                        id.clone(),
-                        thread,
-                        this.project.clone(),
-                        this.tools.clone(),
-                        this.prompt_builder.clone(),
-                        this.project_context.clone(),
-                        Some(window),
-                        cx,
-                    )
-                })
-            })?;
-
-            Ok(thread)
-        })
-    }
-
-    pub fn save_thread(&self, thread: &Entity<Thread>, cx: &mut Context<Self>) -> Task<Result<()>> {
-        let (metadata, serialized_thread) =
-            thread.update(cx, |thread, cx| (thread.id().clone(), thread.serialize(cx)));
-
-        let database_future = ThreadsDatabase::global_future(cx);
-        cx.spawn(async move |this, cx| {
-            let serialized_thread = serialized_thread.await?;
-            let database = database_future.await.map_err(|err| anyhow!(err))?;
-            database.save_thread(metadata, serialized_thread).await?;
-
-            this.update(cx, |this, cx| this.reload(cx))?.await
-        })
-    }
-
-    pub fn delete_thread(&mut self, id: &ThreadId, cx: &mut Context<Self>) -> Task<Result<()>> {
-        let id = id.clone();
-        let database_future = ThreadsDatabase::global_future(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.threads.retain(|thread| thread.id != id);
-                cx.notify();
-            })
-        })
-    }
-
-    pub fn reload(&self, cx: &mut Context<Self>) -> Task<Result<()>> {
-        let database_future = ThreadsDatabase::global_future(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| {
-                this.threads = threads;
-                cx.notify();
-            })
-        })
-    }
-
-    fn register_context_server_handlers(&self, cx: &mut Context<Self>) {
-        let context_server_store = self.project.read(cx).context_server_store();
-        cx.subscribe(&context_server_store, Self::handle_context_server_event)
-            .detach();
-
-        // Check for any servers that were already running before the handler was registered
-        for server in context_server_store.read(cx).running_servers() {
-            self.load_context_server_tools(server.id(), context_server_store.clone(), cx);
-        }
-    }
-
-    fn handle_context_server_event(
-        &mut self,
-        context_server_store: Entity<ContextServerStore>,
-        event: &project::context_server_store::Event,
-        cx: &mut Context<Self>,
-    ) {
-        let tool_working_set = self.tools.clone();
-        match event {
-            project::context_server_store::Event::ServerStatusChanged { server_id, status } => {
-                match status {
-                    ContextServerStatus::Starting => {}
-                    ContextServerStatus::Running => {
-                        self.load_context_server_tools(server_id.clone(), context_server_store, cx);
-                    }
-                    ContextServerStatus::Stopped | ContextServerStatus::Error(_) => {
-                        if let Some(tool_ids) = self.context_server_tool_ids.remove(server_id) {
-                            tool_working_set.update(cx, |tool_working_set, cx| {
-                                tool_working_set.remove(&tool_ids, cx);
-                            });
-                        }
-                    }
-                }
-            }
-        }
-    }
-
-    fn load_context_server_tools(
-        &self,
-        server_id: ContextServerId,
-        context_server_store: Entity<ContextServerStore>,
-        cx: &mut Context<Self>,
-    ) {
-        let Some(server) = context_server_store.read(cx).get_running_server(&server_id) else {
-            return;
-        };
-        let tool_working_set = self.tools.clone();
-        cx.spawn(async move |this, cx| {
-            let Some(protocol) = server.client() else {
-                return;
-            };
-
-            if protocol.capable(context_server::protocol::ServerCapability::Tools)
-                && let Some(response) = protocol
-                    .request::<context_server::types::requests::ListTools>(())
-                    .await
-                    .log_err()
-            {
-                let tool_ids = tool_working_set
-                    .update(cx, |tool_working_set, cx| {
-                        tool_working_set.extend(
-                            response.tools.into_iter().map(|tool| {
-                                Arc::new(ContextServerTool::new(
-                                    context_server_store.clone(),
-                                    server.id(),
-                                    tool,
-                                )) as Arc<dyn Tool>
-                            }),
-                            cx,
-                        )
-                    })
-                    .log_err();
-
-                if let Some(tool_ids) = tool_ids {
-                    this.update(cx, |this, _| {
-                        this.context_server_tool_ids.insert(server_id, tool_ids);
-                    })
-                    .log_err();
-                }
-            }
-        })
-        .detach();
-    }
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-pub struct SerializedThreadMetadata {
-    pub id: ThreadId,
-    pub summary: SharedString,
-    pub updated_at: DateTime<Utc>,
-}
-
-#[derive(Serialize, Deserialize, Debug, PartialEq)]
-pub struct SerializedThread {
-    pub version: String,
-    pub summary: SharedString,
-    pub updated_at: DateTime<Utc>,
-    pub messages: Vec<SerializedMessage>,
-    #[serde(default)]
-    pub initial_project_snapshot: Option<Arc<ProjectSnapshot>>,
-    #[serde(default)]
-    pub cumulative_token_usage: TokenUsage,
-    #[serde(default)]
-    pub request_token_usage: Vec<TokenUsage>,
-    #[serde(default)]
-    pub detailed_summary_state: DetailedSummaryState,
-    #[serde(default)]
-    pub exceeded_window_error: Option<ExceededWindowError>,
-    #[serde(default)]
-    pub model: Option<SerializedLanguageModel>,
-    #[serde(default)]
-    pub completion_mode: Option<CompletionMode>,
-    #[serde(default)]
-    pub tool_use_limit_reached: bool,
-    #[serde(default)]
-    pub profile: Option<AgentProfileId>,
-}
-
-#[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<Self> {
-        let saved_thread_json = serde_json::from_slice::<serde_json::Value>(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::<SerializedThreadV0_1_0>(saved_thread_json)?;
-                    Ok(saved_thread.upgrade())
-                }
-                SerializedThread::VERSION => Ok(serde_json::from_value::<SerializedThread>(
-                    saved_thread_json,
-                )?),
-                _ => anyhow::bail!("unrecognized serialized thread version: {version:?}"),
-            },
-            None => {
-                let saved_thread =
-                    serde_json::from_value::<LegacySerializedThread>(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<SerializedMessage> = 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<SerializedMessageSegment>,
-    #[serde(default)]
-    pub tool_uses: Vec<SerializedToolUse>,
-    #[serde(default)]
-    pub tool_results: Vec<SerializedToolResult>,
-    #[serde(default)]
-    pub context: String,
-    #[serde(default)]
-    pub creases: Vec<SerializedCrease>,
-    #[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<String>,
-    },
-    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<serde_json::Value>,
-}
-
-#[derive(Serialize, Deserialize)]
-struct LegacySerializedThread {
-    pub summary: SharedString,
-    pub updated_at: DateTime<Utc>,
-    pub messages: Vec<LegacySerializedMessage>,
-    #[serde(default)]
-    pub initial_project_snapshot: Option<Arc<ProjectSnapshot>>,
-}
-
-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(),
-            exceeded_window_error: None,
-            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<SerializedToolUse>,
-    #[serde(default)]
-    pub tool_results: Vec<SerializedToolResult>,
-}
-
-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,
-}
-
-struct GlobalThreadsDatabase(
-    Shared<BoxFuture<'static, Result<Arc<ThreadsDatabase>, Arc<anyhow::Error>>>>,
-);
-
-impl Global for GlobalThreadsDatabase {}
-
-pub(crate) struct ThreadsDatabase {
-    executor: BackgroundExecutor,
-    connection: Arc<Mutex<Connection>>,
-}
-
-impl ThreadsDatabase {
-    fn connection(&self) -> Arc<Mutex<Connection>> {
-        self.connection.clone()
-    }
-
-    const COMPRESSION_LEVEL: i32 = 3;
-}
-
-impl Bind for ThreadId {
-    fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
-        self.to_string().bind(statement, start_index)
-    }
-}
-
-impl Column for ThreadId {
-    fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
-        let (id_str, next_index) = String::column(statement, start_index)?;
-        Ok((ThreadId::from(id_str.as_str()), next_index))
-    }
-}
-
-impl ThreadsDatabase {
-    fn global_future(
-        cx: &mut App,
-    ) -> Shared<BoxFuture<'static, Result<Arc<ThreadsDatabase>, Arc<anyhow::Error>>>> {
-        GlobalThreadsDatabase::global(cx).0.clone()
-    }
-
-    fn init(fs: Arc<dyn Fs>, cx: &mut App) {
-        let executor = cx.background_executor().clone();
-        let database_future = executor
-            .spawn({
-                let executor = executor.clone();
-                let threads_dir = paths::data_dir().join("threads");
-                async move { ThreadsDatabase::new(fs, threads_dir, executor).await }
-            })
-            .then(|result| future::ready(result.map(Arc::new).map_err(Arc::new)))
-            .boxed()
-            .shared();
-
-        cx.set_global(GlobalThreadsDatabase(database_future));
-    }
-
-    pub async fn new(
-        fs: Arc<dyn Fs>,
-        threads_dir: PathBuf,
-        executor: BackgroundExecutor,
-    ) -> Result<Self> {
-        fs.create_dir(&threads_dir).await?;
-
-        let sqlite_path = threads_dir.join("threads.db");
-        let mdb_path = threads_dir.join("threads-db.1.mdb");
-
-        let needs_migration_from_heed = fs.is_file(&mdb_path).await;
-
-        let connection = if *ZED_STATELESS {
-            Connection::open_memory(Some("THREAD_FALLBACK_DB"))
-        } else if cfg!(any(feature = "test-support", test)) {
-            // rust stores the name of the test on the current thread.
-            // We use this to automatically create a database that will
-            // be shared within the test (for the test_retrieve_old_thread)
-            // but not with concurrent tests.
-            let thread = std::thread::current();
-            let test_name = thread.name();
-            Connection::open_memory(Some(&format!(
-                "THREAD_FALLBACK_{}",
-                test_name.unwrap_or_default()
-            )))
-        } else {
-            Connection::open_file(&sqlite_path.to_string_lossy())
-        };
-
-        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: executor.clone(),
-            connection: Arc::new(Mutex::new(connection)),
-        };
-
-        if needs_migration_from_heed {
-            let db_connection = db.connection();
-            let executor_clone = executor.clone();
-            executor
-                .spawn(async move {
-                    log::info!("Starting threads.db migration");
-                    Self::migrate_from_heed(&mdb_path, db_connection, executor_clone)?;
-                    fs.remove_dir(
-                        &mdb_path,
-                        RemoveOptions {
-                            recursive: true,
-                            ignore_if_not_exists: true,
-                        },
-                    )
-                    .await?;
-                    log::info!("threads.db migrated to sqlite");
-                    Ok::<(), anyhow::Error>(())
-                })
-                .detach();
-        }
-
-        Ok(db)
-    }
-
-    // Remove this migration after 2025-09-01
-    fn migrate_from_heed(
-        mdb_path: &Path,
-        connection: Arc<Mutex<Connection>>,
-        _executor: BackgroundExecutor,
-    ) -> Result<()> {
-        use heed::types::SerdeBincode;
-        struct SerializedThreadHeed(SerializedThread);
-
-        impl heed::BytesEncode<'_> for SerializedThreadHeed {
-            type EItem = SerializedThreadHeed;
-
-            fn bytes_encode(
-                item: &Self::EItem,
-            ) -> Result<std::borrow::Cow<'_, [u8]>, heed::BoxedError> {
-                serde_json::to_vec(&item.0)
-                    .map(std::borrow::Cow::Owned)
-                    .map_err(Into::into)
-            }
-        }
-
-        impl<'a> heed::BytesDecode<'a> for SerializedThreadHeed {
-            type DItem = SerializedThreadHeed;
-
-            fn bytes_decode(bytes: &'a [u8]) -> Result<Self::DItem, heed::BoxedError> {
-                SerializedThread::from_json(bytes)
-                    .map(SerializedThreadHeed)
-                    .map_err(Into::into)
-            }
-        }
-
-        const ONE_GB_IN_BYTES: usize = 1024 * 1024 * 1024;
-
-        let env = unsafe {
-            heed::EnvOpenOptions::new()
-                .map_size(ONE_GB_IN_BYTES)
-                .max_dbs(1)
-                .open(mdb_path)?
-        };
-
-        let txn = env.write_txn()?;
-        let threads: heed::Database<SerdeBincode<ThreadId>, SerializedThreadHeed> = env
-            .open_database(&txn, Some("threads"))?
-            .ok_or_else(|| anyhow!("threads database not found"))?;
-
-        for result in threads.iter(&txn)? {
-            let (thread_id, thread_heed) = result?;
-            Self::save_thread_sync(&connection, thread_id, thread_heed.0)?;
-        }
-
-        Ok(())
-    }
-
-    fn save_thread_sync(
-        connection: &Arc<Mutex<Connection>>,
-        id: ThreadId,
-        thread: SerializedThread,
-    ) -> Result<()> {
-        let json_data = serde_json::to_string(&thread)?;
-        let summary = thread.summary.to_string();
-        let updated_at = thread.updated_at.to_rfc3339();
-
-        let connection = connection.lock().unwrap();
-
-        let compressed = zstd::encode_all(json_data.as_bytes(), Self::COMPRESSION_LEVEL)?;
-        let data_type = DataType::Zstd;
-        let data = compressed;
-
-        let mut insert = connection.exec_bound::<(ThreadId, String, String, DataType, Vec<u8>)>(indoc! {"
-            INSERT OR REPLACE INTO threads (id, summary, updated_at, data_type, data) VALUES (?, ?, ?, ?, ?)
-        "})?;
-
-        insert((id, summary, updated_at, data_type, data))?;
-
-        Ok(())
-    }
-
-    pub fn list_threads(&self) -> Task<Result<Vec<SerializedThreadMetadata>>> {
-        let connection = self.connection.clone();
-
-        self.executor.spawn(async move {
-            let connection = connection.lock().unwrap();
-            let mut select =
-                connection.select_bound::<(), (ThreadId, 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(SerializedThreadMetadata {
-                    id,
-                    summary: summary.into(),
-                    updated_at: DateTime::parse_from_rfc3339(&updated_at)?.with_timezone(&Utc),
-                });
-            }
-
-            Ok(threads)
-        })
-    }
-
-    pub fn try_find_thread(&self, id: ThreadId) -> Task<Result<Option<SerializedThread>>> {
-        let connection = self.connection.clone();
-
-        self.executor.spawn(async move {
-            let connection = connection.lock().unwrap();
-            let mut select = connection.select_bound::<ThreadId, (DataType, Vec<u8>)>(indoc! {"
-                SELECT data_type, data FROM threads WHERE id = ? LIMIT 1
-            "})?;
-
-            let rows = select(id)?;
-            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 = SerializedThread::from_json(json_data.as_bytes())?;
-                Ok(Some(thread))
-            } else {
-                Ok(None)
-            }
-        })
-    }
-
-    pub fn save_thread(&self, id: ThreadId, thread: SerializedThread) -> Task<Result<()>> {
-        let connection = self.connection.clone();
-
-        self.executor
-            .spawn(async move { Self::save_thread_sync(&connection, id, thread) })
-    }
-
-    pub fn delete_thread(&self, id: ThreadId) -> Task<Result<()>> {
-        let connection = self.connection.clone();
-
-        self.executor.spawn(async move {
-            let connection = connection.lock().unwrap();
-
-            let mut delete = connection.exec_bound::<ThreadId>(indoc! {"
-                DELETE FROM threads WHERE id = ?
-            "})?;
-
-            delete(id)?;
-
-            Ok(())
-        })
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use crate::thread::{DetailedSummaryState, MessageId};
-    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(),
-                exceeded_window_error: None,
-                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(),
-            exceeded_window_error: None,
-            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(),
-                exceeded_window_error: None,
-                model: None,
-                completion_mode: None,
-                tool_use_limit_reached: false,
-                profile: None
-            }
-        )
-    }
-}

crates/assistant_tool/src/tool_schema.rs → crates/agent/src/tool_schema.rs 🔗

@@ -1,7 +1,48 @@
 use anyhow::Result;
+use language_model::LanguageModelToolSchemaFormat;
+use schemars::{
+    JsonSchema, Schema,
+    generate::SchemaSettings,
+    transform::{Transform, transform_subschemas},
+};
 use serde_json::Value;
 
-use crate::LanguageModelToolSchemaFormat;
+pub(crate) fn root_schema_for<T: JsonSchema>(format: LanguageModelToolSchemaFormat) -> Schema {
+    let mut generator = match format {
+        LanguageModelToolSchemaFormat::JsonSchema => SchemaSettings::draft07().into_generator(),
+        LanguageModelToolSchemaFormat::JsonSchemaSubset => SchemaSettings::openapi3()
+            .with(|settings| {
+                settings.meta_schema = None;
+                settings.inline_subschemas = true;
+            })
+            .with_transform(ToJsonSchemaSubsetTransform)
+            .into_generator(),
+    };
+    generator.root_schema_for::<T>()
+}
+
+#[derive(Debug, Clone)]
+struct ToJsonSchemaSubsetTransform;
+
+impl Transform for ToJsonSchemaSubsetTransform {
+    fn transform(&mut self, schema: &mut Schema) {
+        // Ensure that the type field is not an array, this happens when we use
+        // Option<T>, the type will be [T, "null"].
+        if let Some(type_field) = schema.get_mut("type")
+            && let Some(types) = type_field.as_array()
+            && let Some(first_type) = types.first()
+        {
+            *type_field = first_type.clone();
+        }
+
+        // oneOf is not supported, use anyOf instead
+        if let Some(one_of) = schema.remove("oneOf") {
+            schema.insert("anyOf".to_string(), one_of);
+        }
+
+        transform_subschemas(self, schema);
+    }
+}
 
 /// Tries to adapt a JSON schema representation to be compatible with the specified format.
 ///

crates/agent/src/tool_use.rs 🔗

@@ -1,575 +0,0 @@
-use crate::{
-    thread::{MessageId, PromptId, ThreadId},
-    thread_store::SerializedMessage,
-};
-use agent_settings::CompletionMode;
-use anyhow::Result;
-use assistant_tool::{
-    AnyToolCard, Tool, ToolResultContent, ToolResultOutput, ToolUseStatus, ToolWorkingSet,
-};
-use collections::HashMap;
-use futures::{FutureExt as _, future::Shared};
-use gpui::{App, Entity, SharedString, Task, Window};
-use icons::IconName;
-use language_model::{
-    ConfiguredModel, LanguageModel, LanguageModelExt, LanguageModelRequest,
-    LanguageModelToolResult, LanguageModelToolResultContent, LanguageModelToolUse,
-    LanguageModelToolUseId, Role,
-};
-use project::Project;
-use std::sync::Arc;
-use util::truncate_lines_to_byte_limit;
-
-#[derive(Debug)]
-pub struct ToolUse {
-    pub id: LanguageModelToolUseId,
-    pub name: SharedString,
-    pub ui_text: SharedString,
-    pub status: ToolUseStatus,
-    pub input: serde_json::Value,
-    pub icon: icons::IconName,
-    pub needs_confirmation: bool,
-}
-
-pub struct ToolUseState {
-    tools: Entity<ToolWorkingSet>,
-    tool_uses_by_assistant_message: HashMap<MessageId, Vec<LanguageModelToolUse>>,
-    tool_results: HashMap<LanguageModelToolUseId, LanguageModelToolResult>,
-    pending_tool_uses_by_id: HashMap<LanguageModelToolUseId, PendingToolUse>,
-    tool_result_cards: HashMap<LanguageModelToolUseId, AnyToolCard>,
-    tool_use_metadata_by_id: HashMap<LanguageModelToolUseId, ToolUseMetadata>,
-}
-
-impl ToolUseState {
-    pub fn new(tools: Entity<ToolWorkingSet>) -> Self {
-        Self {
-            tools,
-            tool_uses_by_assistant_message: HashMap::default(),
-            tool_results: HashMap::default(),
-            pending_tool_uses_by_id: HashMap::default(),
-            tool_result_cards: HashMap::default(),
-            tool_use_metadata_by_id: HashMap::default(),
-        }
-    }
-
-    /// Constructs a [`ToolUseState`] from the given list of [`SerializedMessage`]s.
-    ///
-    /// Accepts a function to filter the tools that should be used to populate the state.
-    ///
-    /// If `window` is `None` (e.g., when in headless mode or when running evals),
-    /// tool cards won't be deserialized
-    pub fn from_serialized_messages(
-        tools: Entity<ToolWorkingSet>,
-        messages: &[SerializedMessage],
-        project: Entity<Project>,
-        window: Option<&mut Window>, // None in headless mode
-        cx: &mut App,
-    ) -> Self {
-        let mut this = Self::new(tools);
-        let mut tool_names_by_id = HashMap::default();
-        let mut window = window;
-
-        for message in messages {
-            match message.role {
-                Role::Assistant => {
-                    if !message.tool_uses.is_empty() {
-                        let tool_uses = message
-                            .tool_uses
-                            .iter()
-                            .map(|tool_use| LanguageModelToolUse {
-                                id: tool_use.id.clone(),
-                                name: tool_use.name.clone().into(),
-                                raw_input: tool_use.input.to_string(),
-                                input: tool_use.input.clone(),
-                                is_input_complete: true,
-                            })
-                            .collect::<Vec<_>>();
-
-                        tool_names_by_id.extend(
-                            tool_uses
-                                .iter()
-                                .map(|tool_use| (tool_use.id.clone(), tool_use.name.clone())),
-                        );
-
-                        this.tool_uses_by_assistant_message
-                            .insert(message.id, tool_uses);
-
-                        for tool_result in &message.tool_results {
-                            let tool_use_id = tool_result.tool_use_id.clone();
-                            let Some(tool_use) = tool_names_by_id.get(&tool_use_id) else {
-                                log::warn!("no tool name found for tool use: {tool_use_id:?}");
-                                continue;
-                            };
-
-                            this.tool_results.insert(
-                                tool_use_id.clone(),
-                                LanguageModelToolResult {
-                                    tool_use_id: tool_use_id.clone(),
-                                    tool_name: tool_use.clone(),
-                                    is_error: tool_result.is_error,
-                                    content: tool_result.content.clone(),
-                                    output: tool_result.output.clone(),
-                                },
-                            );
-
-                            if let Some(window) = &mut window
-                                && let Some(tool) = this.tools.read(cx).tool(tool_use, cx)
-                                && let Some(output) = tool_result.output.clone()
-                                && let Some(card) =
-                                    tool.deserialize_card(output, project.clone(), window, cx)
-                            {
-                                this.tool_result_cards.insert(tool_use_id, card);
-                            }
-                        }
-                    }
-                }
-                Role::System | Role::User => {}
-            }
-        }
-
-        this
-    }
-
-    pub fn cancel_pending(&mut self) -> Vec<PendingToolUse> {
-        let mut canceled_tool_uses = Vec::new();
-        self.pending_tool_uses_by_id
-            .retain(|tool_use_id, tool_use| {
-                if matches!(tool_use.status, PendingToolUseStatus::Error { .. }) {
-                    return true;
-                }
-
-                let content = "Tool canceled by user".into();
-                self.tool_results.insert(
-                    tool_use_id.clone(),
-                    LanguageModelToolResult {
-                        tool_use_id: tool_use_id.clone(),
-                        tool_name: tool_use.name.clone(),
-                        content,
-                        output: None,
-                        is_error: true,
-                    },
-                );
-                canceled_tool_uses.push(tool_use.clone());
-                false
-            });
-        canceled_tool_uses
-    }
-
-    pub fn pending_tool_uses(&self) -> Vec<&PendingToolUse> {
-        self.pending_tool_uses_by_id.values().collect()
-    }
-
-    pub fn tool_uses_for_message(
-        &self,
-        id: MessageId,
-        project: &Entity<Project>,
-        cx: &App,
-    ) -> Vec<ToolUse> {
-        let Some(tool_uses_for_message) = &self.tool_uses_by_assistant_message.get(&id) else {
-            return Vec::new();
-        };
-
-        let mut tool_uses = Vec::new();
-
-        for tool_use in tool_uses_for_message.iter() {
-            let tool_result = self.tool_results.get(&tool_use.id);
-
-            let status = (|| {
-                if let Some(tool_result) = tool_result {
-                    let content = tool_result
-                        .content
-                        .to_str()
-                        .map(|str| str.to_owned().into())
-                        .unwrap_or_default();
-
-                    return if tool_result.is_error {
-                        ToolUseStatus::Error(content)
-                    } else {
-                        ToolUseStatus::Finished(content)
-                    };
-                }
-
-                if let Some(pending_tool_use) = self.pending_tool_uses_by_id.get(&tool_use.id) {
-                    match pending_tool_use.status {
-                        PendingToolUseStatus::Idle => ToolUseStatus::Pending,
-                        PendingToolUseStatus::NeedsConfirmation { .. } => {
-                            ToolUseStatus::NeedsConfirmation
-                        }
-                        PendingToolUseStatus::Running { .. } => ToolUseStatus::Running,
-                        PendingToolUseStatus::Error(ref err) => {
-                            ToolUseStatus::Error(err.clone().into())
-                        }
-                        PendingToolUseStatus::InputStillStreaming => {
-                            ToolUseStatus::InputStillStreaming
-                        }
-                    }
-                } else {
-                    ToolUseStatus::Pending
-                }
-            })();
-
-            let (icon, needs_confirmation) =
-                if let Some(tool) = self.tools.read(cx).tool(&tool_use.name, cx) {
-                    (
-                        tool.icon(),
-                        tool.needs_confirmation(&tool_use.input, project, cx),
-                    )
-                } else {
-                    (IconName::Cog, false)
-                };
-
-            tool_uses.push(ToolUse {
-                id: tool_use.id.clone(),
-                name: tool_use.name.clone().into(),
-                ui_text: self.tool_ui_label(
-                    &tool_use.name,
-                    &tool_use.input,
-                    tool_use.is_input_complete,
-                    cx,
-                ),
-                input: tool_use.input.clone(),
-                status,
-                icon,
-                needs_confirmation,
-            })
-        }
-
-        tool_uses
-    }
-
-    pub fn tool_ui_label(
-        &self,
-        tool_name: &str,
-        input: &serde_json::Value,
-        is_input_complete: bool,
-        cx: &App,
-    ) -> SharedString {
-        if let Some(tool) = self.tools.read(cx).tool(tool_name, cx) {
-            if is_input_complete {
-                tool.ui_text(input).into()
-            } else {
-                tool.still_streaming_ui_text(input).into()
-            }
-        } else {
-            format!("Unknown tool {tool_name:?}").into()
-        }
-    }
-
-    pub fn tool_results_for_message(
-        &self,
-        assistant_message_id: MessageId,
-    ) -> Vec<&LanguageModelToolResult> {
-        let Some(tool_uses) = self
-            .tool_uses_by_assistant_message
-            .get(&assistant_message_id)
-        else {
-            return Vec::new();
-        };
-
-        tool_uses
-            .iter()
-            .filter_map(|tool_use| self.tool_results.get(&tool_use.id))
-            .collect()
-    }
-
-    pub fn message_has_tool_results(&self, assistant_message_id: MessageId) -> bool {
-        self.tool_uses_by_assistant_message
-            .get(&assistant_message_id)
-            .is_some_and(|results| !results.is_empty())
-    }
-
-    pub fn tool_result(
-        &self,
-        tool_use_id: &LanguageModelToolUseId,
-    ) -> Option<&LanguageModelToolResult> {
-        self.tool_results.get(tool_use_id)
-    }
-
-    pub fn tool_result_card(&self, tool_use_id: &LanguageModelToolUseId) -> Option<&AnyToolCard> {
-        self.tool_result_cards.get(tool_use_id)
-    }
-
-    pub fn insert_tool_result_card(
-        &mut self,
-        tool_use_id: LanguageModelToolUseId,
-        card: AnyToolCard,
-    ) {
-        self.tool_result_cards.insert(tool_use_id, card);
-    }
-
-    pub fn request_tool_use(
-        &mut self,
-        assistant_message_id: MessageId,
-        tool_use: LanguageModelToolUse,
-        metadata: ToolUseMetadata,
-        cx: &App,
-    ) -> Arc<str> {
-        let tool_uses = self
-            .tool_uses_by_assistant_message
-            .entry(assistant_message_id)
-            .or_default();
-
-        let mut existing_tool_use_found = false;
-
-        for existing_tool_use in tool_uses.iter_mut() {
-            if existing_tool_use.id == tool_use.id {
-                *existing_tool_use = tool_use.clone();
-                existing_tool_use_found = true;
-            }
-        }
-
-        if !existing_tool_use_found {
-            tool_uses.push(tool_use.clone());
-        }
-
-        let status = if tool_use.is_input_complete {
-            self.tool_use_metadata_by_id
-                .insert(tool_use.id.clone(), metadata);
-
-            PendingToolUseStatus::Idle
-        } else {
-            PendingToolUseStatus::InputStillStreaming
-        };
-
-        let ui_text: Arc<str> = self
-            .tool_ui_label(
-                &tool_use.name,
-                &tool_use.input,
-                tool_use.is_input_complete,
-                cx,
-            )
-            .into();
-
-        let may_perform_edits = self
-            .tools
-            .read(cx)
-            .tool(&tool_use.name, cx)
-            .is_some_and(|tool| tool.may_perform_edits());
-
-        self.pending_tool_uses_by_id.insert(
-            tool_use.id.clone(),
-            PendingToolUse {
-                assistant_message_id,
-                id: tool_use.id,
-                name: tool_use.name.clone(),
-                ui_text: ui_text.clone(),
-                input: tool_use.input,
-                may_perform_edits,
-                status,
-            },
-        );
-
-        ui_text
-    }
-
-    pub fn run_pending_tool(
-        &mut self,
-        tool_use_id: LanguageModelToolUseId,
-        ui_text: SharedString,
-        task: Task<()>,
-    ) {
-        if let Some(tool_use) = self.pending_tool_uses_by_id.get_mut(&tool_use_id) {
-            tool_use.ui_text = ui_text.into();
-            tool_use.status = PendingToolUseStatus::Running {
-                _task: task.shared(),
-            };
-        }
-    }
-
-    pub fn confirm_tool_use(
-        &mut self,
-        tool_use_id: LanguageModelToolUseId,
-        ui_text: impl Into<Arc<str>>,
-        input: serde_json::Value,
-        request: Arc<LanguageModelRequest>,
-        tool: Arc<dyn Tool>,
-    ) {
-        if let Some(tool_use) = self.pending_tool_uses_by_id.get_mut(&tool_use_id) {
-            let ui_text = ui_text.into();
-            tool_use.ui_text = ui_text.clone();
-            let confirmation = Confirmation {
-                tool_use_id,
-                input,
-                request,
-                tool,
-                ui_text,
-            };
-            tool_use.status = PendingToolUseStatus::NeedsConfirmation(Arc::new(confirmation));
-        }
-    }
-
-    pub fn insert_tool_output(
-        &mut self,
-        tool_use_id: LanguageModelToolUseId,
-        tool_name: Arc<str>,
-        output: Result<ToolResultOutput>,
-        configured_model: Option<&ConfiguredModel>,
-        completion_mode: CompletionMode,
-    ) -> Option<PendingToolUse> {
-        let metadata = self.tool_use_metadata_by_id.remove(&tool_use_id);
-
-        telemetry::event!(
-            "Agent Tool Finished",
-            model = metadata
-                .as_ref()
-                .map(|metadata| metadata.model.telemetry_id()),
-            model_provider = metadata
-                .as_ref()
-                .map(|metadata| metadata.model.provider_id().to_string()),
-            thread_id = metadata.as_ref().map(|metadata| metadata.thread_id.clone()),
-            prompt_id = metadata.as_ref().map(|metadata| metadata.prompt_id.clone()),
-            tool_name,
-            success = output.is_ok()
-        );
-
-        match output {
-            Ok(output) => {
-                let tool_result = output.content;
-                const BYTES_PER_TOKEN_ESTIMATE: usize = 3;
-
-                let old_use = self.pending_tool_uses_by_id.remove(&tool_use_id);
-
-                // Protect from overly large output
-                let tool_output_limit = configured_model
-                    .map(|model| {
-                        model.model.max_token_count_for_mode(completion_mode.into()) as usize
-                            * BYTES_PER_TOKEN_ESTIMATE
-                    })
-                    .unwrap_or(usize::MAX);
-
-                let content = match tool_result {
-                    ToolResultContent::Text(text) => {
-                        let text = if text.len() < tool_output_limit {
-                            text
-                        } else {
-                            let truncated = truncate_lines_to_byte_limit(&text, tool_output_limit);
-                            format!(
-                                "Tool result too long. The first {} bytes:\n\n{}",
-                                truncated.len(),
-                                truncated
-                            )
-                        };
-                        LanguageModelToolResultContent::Text(text.into())
-                    }
-                    ToolResultContent::Image(language_model_image) => {
-                        if language_model_image.estimate_tokens() < tool_output_limit {
-                            LanguageModelToolResultContent::Image(language_model_image)
-                        } else {
-                            self.tool_results.insert(
-                                tool_use_id.clone(),
-                                LanguageModelToolResult {
-                                    tool_use_id: tool_use_id.clone(),
-                                    tool_name,
-                                    content: "Tool responded with an image that would exceeded the remaining tokens".into(),
-                                    is_error: true,
-                                    output: None,
-                                },
-                            );
-
-                            return old_use;
-                        }
-                    }
-                };
-
-                self.tool_results.insert(
-                    tool_use_id.clone(),
-                    LanguageModelToolResult {
-                        tool_use_id: tool_use_id.clone(),
-                        tool_name,
-                        content,
-                        is_error: false,
-                        output: output.output,
-                    },
-                );
-
-                old_use
-            }
-            Err(err) => {
-                self.tool_results.insert(
-                    tool_use_id.clone(),
-                    LanguageModelToolResult {
-                        tool_use_id: tool_use_id.clone(),
-                        tool_name,
-                        content: LanguageModelToolResultContent::Text(err.to_string().into()),
-                        is_error: true,
-                        output: None,
-                    },
-                );
-
-                if let Some(tool_use) = self.pending_tool_uses_by_id.get_mut(&tool_use_id) {
-                    tool_use.status = PendingToolUseStatus::Error(err.to_string().into());
-                }
-
-                self.pending_tool_uses_by_id.get(&tool_use_id).cloned()
-            }
-        }
-    }
-
-    pub fn has_tool_results(&self, assistant_message_id: MessageId) -> bool {
-        self.tool_uses_by_assistant_message
-            .contains_key(&assistant_message_id)
-    }
-
-    pub fn tool_results(
-        &self,
-        assistant_message_id: MessageId,
-    ) -> impl Iterator<Item = (&LanguageModelToolUse, Option<&LanguageModelToolResult>)> {
-        self.tool_uses_by_assistant_message
-            .get(&assistant_message_id)
-            .into_iter()
-            .flatten()
-            .map(|tool_use| (tool_use, self.tool_results.get(&tool_use.id)))
-    }
-}
-
-#[derive(Debug, Clone)]
-pub struct PendingToolUse {
-    pub id: LanguageModelToolUseId,
-    /// The ID of the Assistant message in which the tool use was requested.
-    #[allow(unused)]
-    pub assistant_message_id: MessageId,
-    pub name: Arc<str>,
-    pub ui_text: Arc<str>,
-    pub input: serde_json::Value,
-    pub status: PendingToolUseStatus,
-    pub may_perform_edits: bool,
-}
-
-#[derive(Debug, Clone)]
-pub struct Confirmation {
-    pub tool_use_id: LanguageModelToolUseId,
-    pub input: serde_json::Value,
-    pub ui_text: Arc<str>,
-    pub request: Arc<LanguageModelRequest>,
-    pub tool: Arc<dyn Tool>,
-}
-
-#[derive(Debug, Clone)]
-pub enum PendingToolUseStatus {
-    InputStillStreaming,
-    Idle,
-    NeedsConfirmation(Arc<Confirmation>),
-    Running { _task: Shared<Task<()>> },
-    Error(#[allow(unused)] Arc<str>),
-}
-
-impl PendingToolUseStatus {
-    pub fn is_idle(&self) -> bool {
-        matches!(self, PendingToolUseStatus::Idle)
-    }
-
-    pub fn is_error(&self) -> bool {
-        matches!(self, PendingToolUseStatus::Error(_))
-    }
-
-    pub fn needs_confirmation(&self) -> bool {
-        matches!(self, PendingToolUseStatus::NeedsConfirmation { .. })
-    }
-}
-
-#[derive(Clone)]
-pub struct ToolUseMetadata {
-    pub model: Arc<dyn LanguageModel>,
-    pub thread_id: ThreadId,
-    pub prompt_id: PromptId,
-}

crates/agent/src/tools.rs 🔗

@@ -0,0 +1,94 @@
+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<language_model::LanguageModelProviderId>) -> impl Iterator<Item = String> {
+            [
+                $(
+                    (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<Item = LanguageModelRequestTool> {
+            fn language_model_tool<T: AgentTool>() -> 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,
+}

crates/agent2/src/tools/context_server_registry.rs → crates/agent/src/tools/context_server_registry.rs 🔗

@@ -32,6 +32,17 @@ impl ContextServerRegistry {
         this
     }
 
+    pub fn tools_for_server(
+        &self,
+        server_id: &ContextServerId,
+    ) -> impl Iterator<Item = &Arc<dyn AnyAgentTool>> {
+        self.registered_servers
+            .get(server_id)
+            .map(|server| server.tools.values())
+            .into_iter()
+            .flatten()
+    }
+
     pub fn servers(
         &self,
     ) -> impl Iterator<
@@ -154,7 +165,7 @@ impl AnyAgentTool for ContextServerTool {
         format: language_model::LanguageModelToolSchemaFormat,
     ) -> Result<serde_json::Value> {
         let mut schema = self.tool.input_schema.clone();
-        assistant_tool::adapt_schema_to_format(&mut schema, format)?;
+        crate::tool_schema::adapt_schema_to_format(&mut schema, format)?;
         Ok(match schema {
             serde_json::Value::Null => {
                 serde_json::json!({ "type": "object", "properties": [] })

crates/agent2/src/tools/edit_file_tool.rs → crates/agent/src/tools/edit_file_tool.rs 🔗

@@ -1,8 +1,10 @@
-use crate::{AgentTool, Thread, ToolCallEventStream};
+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 assistant_tools::edit_agent::{EditAgent, EditAgentOutput, EditAgentOutputEvent, EditFormat};
 use cloud_llm_client::CompletionIntent;
 use collections::HashSet;
 use gpui::{App, AppContext, AsyncApp, Entity, Task, WeakEntity};
@@ -34,7 +36,7 @@ const DEFAULT_UI_TEXT: &str = "Editing file";
 ///
 /// 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(Debug, Serialize, Deserialize, JsonSchema)]
+#[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.
     ///
@@ -75,7 +77,7 @@ pub struct EditFileToolInput {
     pub mode: EditFileMode,
 }
 
-#[derive(Debug, Serialize, Deserialize, JsonSchema)]
+#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
 struct EditFileToolPartialInput {
     #[serde(default)]
     path: String,
@@ -123,6 +125,7 @@ pub struct EditFileTool {
     thread: WeakEntity<Thread>,
     language_registry: Arc<LanguageRegistry>,
     project: Entity<Project>,
+    templates: Arc<Templates>,
 }
 
 impl EditFileTool {
@@ -130,11 +133,13 @@ impl EditFileTool {
         project: Entity<Project>,
         thread: WeakEntity<Thread>,
         language_registry: Arc<LanguageRegistry>,
+        templates: Arc<Templates>,
     ) -> Self {
         Self {
             project,
             thread,
             language_registry,
+            templates,
         }
     }
 
@@ -294,8 +299,7 @@ impl AgentTool for EditFileTool {
                 model,
                 project.clone(),
                 action_log.clone(),
-                // TODO: move edit agent to this crate so we can use our templates
-                assistant_tools::templates::Templates::new(),
+                self.templates.clone(),
                 edit_format,
             );
 
@@ -599,6 +603,7 @@ mod tests {
                     project,
                     thread.downgrade(),
                     language_registry,
+                    Templates::new(),
                 ))
                 .run(input, ToolCallEventStream::test().0, cx)
             })
@@ -807,6 +812,7 @@ mod tests {
                     project.clone(),
                     thread.downgrade(),
                     language_registry.clone(),
+                    Templates::new(),
                 ))
                 .run(input, ToolCallEventStream::test().0, cx)
             });
@@ -865,6 +871,7 @@ mod tests {
                     project.clone(),
                     thread.downgrade(),
                     language_registry,
+                    Templates::new(),
                 ))
                 .run(input, ToolCallEventStream::test().0, cx)
             });
@@ -951,6 +958,7 @@ mod tests {
                     project.clone(),
                     thread.downgrade(),
                     language_registry.clone(),
+                    Templates::new(),
                 ))
                 .run(input, ToolCallEventStream::test().0, cx)
             });
@@ -1005,6 +1013,7 @@ mod tests {
                     project.clone(),
                     thread.downgrade(),
                     language_registry,
+                    Templates::new(),
                 ))
                 .run(input, ToolCallEventStream::test().0, cx)
             });
@@ -1057,6 +1066,7 @@ mod tests {
             project.clone(),
             thread.downgrade(),
             language_registry,
+            Templates::new(),
         ));
         fs.insert_tree("/root", json!({})).await;
 
@@ -1197,6 +1207,7 @@ mod tests {
             project.clone(),
             thread.downgrade(),
             language_registry,
+            Templates::new(),
         ));
 
         // Test global config paths - these should require confirmation if they exist and are outside the project
@@ -1309,6 +1320,7 @@ mod tests {
             project.clone(),
             thread.downgrade(),
             language_registry,
+            Templates::new(),
         ));
 
         // Test files in different worktrees
@@ -1393,6 +1405,7 @@ mod tests {
             project.clone(),
             thread.downgrade(),
             language_registry,
+            Templates::new(),
         ));
 
         // Test edge cases
@@ -1482,6 +1495,7 @@ mod tests {
             project.clone(),
             thread.downgrade(),
             language_registry,
+            Templates::new(),
         ));
 
         // Test different EditFileMode values
@@ -1566,6 +1580,7 @@ mod tests {
             project,
             thread.downgrade(),
             language_registry,
+            Templates::new(),
         ));
 
         cx.update(|cx| {
@@ -1653,6 +1668,7 @@ mod tests {
                 project.clone(),
                 thread.downgrade(),
                 languages.clone(),
+                Templates::new(),
             ));
             let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
             let edit = cx.update(|cx| {
@@ -1682,6 +1698,7 @@ mod tests {
                 project.clone(),
                 thread.downgrade(),
                 languages.clone(),
+                Templates::new(),
             ));
             let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
             let edit = cx.update(|cx| {
@@ -1709,6 +1726,7 @@ mod tests {
                 project.clone(),
                 thread.downgrade(),
                 languages.clone(),
+                Templates::new(),
             ));
             let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
             let edit = cx.update(|cx| {

crates/agent2/src/tools/read_file_tool.rs → crates/agent/src/tools/read_file_tool.rs 🔗

@@ -1,7 +1,6 @@
 use action_log::ActionLog;
 use agent_client_protocol::{self as acp, ToolCallUpdateFields};
 use anyhow::{Context as _, Result, anyhow};
-use assistant_tool::outline;
 use gpui::{App, Entity, SharedString, Task};
 use indoc::formatdoc;
 use language::Point;
@@ -13,7 +12,7 @@ use settings::Settings;
 use std::sync::Arc;
 use util::markdown::MarkdownCodeBlock;
 
-use crate::{AgentTool, ToolCallEventStream};
+use crate::{AgentTool, ToolCallEventStream, outline};
 
 /// Reads the content of the given file in the project.
 ///

crates/agent2/src/tools/web_search_tool.rs → crates/agent/src/tools/web_search_tool.rs 🔗

@@ -57,7 +57,7 @@ impl AgentTool for WebSearchTool {
     }
 
     /// We currently only support Zed Cloud as a provider.
-    fn supported_provider(&self, provider: &LanguageModelProviderId) -> bool {
+    fn supports_provider(provider: &LanguageModelProviderId) -> bool {
         provider == &ZED_CLOUD_PROVIDER_ID
     }
 

crates/agent2/Cargo.toml 🔗

@@ -1,102 +0,0 @@
-[package]
-name = "agent2"
-version = "0.1.0"
-edition.workspace = true
-publish.workspace = true
-license = "GPL-3.0-or-later"
-
-[lib]
-path = "src/agent2.rs"
-
-[features]
-test-support = ["db/test-support"]
-e2e = []
-
-[lints]
-workspace = true
-
-[dependencies]
-acp_thread.workspace = true
-action_log.workspace = true
-agent.workspace = true
-agent-client-protocol.workspace = true
-agent_servers.workspace = true
-agent_settings.workspace = true
-anyhow.workspace = true
-assistant_context.workspace = true
-assistant_tool.workspace = true
-assistant_tools.workspace = true
-chrono.workspace = true
-client.workspace = true
-cloud_llm_client.workspace = true
-collections.workspace = true
-context_server.workspace = true
-db.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
-rust-embed.workspace = true
-schemars.workspace = true
-serde.workspace = true
-serde_json.workspace = true
-settings.workspace = true
-smol.workspace = true
-sqlez.workspace = true
-task.workspace = true
-telemetry.workspace = true
-terminal.workspace = true
-thiserror.workspace = true
-text.workspace = true
-ui.workspace = true
-util.workspace = true
-uuid.workspace = true
-watch.workspace = true
-web_search.workspace = true
-workspace-hack.workspace = true
-zed_env_vars.workspace = true
-zstd.workspace = true
-
-[dev-dependencies]
-agent = { workspace = true, "features" = ["test-support"] }
-agent_servers = { workspace = true, "features" = ["test-support"] }
-assistant_context = { workspace = true, "features" = ["test-support"] }
-ctor.workspace = true
-client = { workspace = true, "features" = ["test-support"] }
-clock = { workspace = true, "features" = ["test-support"] }
-context_server = { workspace = true, "features" = ["test-support"] }
-db = { workspace = true, "features" = ["test-support"] }
-editor = { workspace = true, "features" = ["test-support"] }
-env_logger.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"] }
-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

crates/agent2/src/agent.rs 🔗

@@ -1,1588 +0,0 @@
-use crate::{
-    ContextServerRegistry, Thread, ThreadEvent, ThreadsDatabase, ToolCallAuthorization,
-    UserMessageContent, templates::Templates,
-};
-use crate::{HistoryStore, TerminalHandle, ThreadEnvironment, TitleUpdated, TokenUsageUpdated};
-use acp_thread::{AcpThread, AgentModelSelector};
-use action_log::ActionLog;
-use agent_client_protocol as acp;
-use anyhow::{Context as _, Result, anyhow};
-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, PromptId, PromptStore, RulesFileContext, UserRulesContext, WorktreeContext,
-};
-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;
-
-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<Thread>,
-    /// The ACP thread that handles protocol communication
-    acp_thread: WeakEntity<acp_thread::AcpThread>,
-    pending_save: Task<()>,
-    _subscriptions: Vec<Subscription>,
-}
-
-pub struct LanguageModels {
-    /// Access language model by ID
-    models: HashMap<acp::ModelId, Arc<dyn LanguageModel>>,
-    /// 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::<Vec<_>>();
-
-        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();
-                if !recommended_models.contains(&(model.provider_id(), model.id())) {
-                    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<Arc<dyn LanguageModel>> {
-        self.models.get(model_id).cloned()
-    }
-
-    fn map_language_model_to_info(
-        model: &Arc<dyn LanguageModel>,
-        provider: &Arc<dyn LanguageModelProvider>,
-    ) -> 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<dyn LanguageModel>) -> acp::ModelId {
-        acp::ModelId(format!("{}/{}", model.provider_id().0, model.id().0).into())
-    }
-
-    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::<Vec<_>>();
-
-        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<acp::SessionId, Session>,
-    history: Entity<HistoryStore>,
-    /// Shared project context for all threads
-    project_context: Entity<ProjectContext>,
-    project_context_needs_refresh: watch::Sender<()>,
-    _maintain_project_context: Task<Result<()>>,
-    context_server_registry: Entity<ContextServerRegistry>,
-    /// Shared templates for all threads
-    templates: Arc<Templates>,
-    /// Cached model information
-    models: LanguageModels,
-    project: Entity<Project>,
-    prompt_store: Option<Entity<PromptStore>>,
-    fs: Arc<dyn Fs>,
-    _subscriptions: Vec<Subscription>,
-}
-
-impl NativeAgent {
-    pub async fn new(
-        project: Entity<Project>,
-        history: Entity<HistoryStore>,
-        templates: Arc<Templates>,
-        prompt_store: Option<Entity<PromptStore>>,
-        fs: Arc<dyn Fs>,
-        cx: &mut AsyncApp,
-    ) -> Result<Entity<NativeAgent>> {
-        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<Thread>,
-        cx: &mut Context<Self>,
-    ) -> Entity<AcpThread> {
-        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<Self>,
-        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<Project>,
-        prompt_store: Option<&Entity<PromptStore>>,
-        cx: &mut App,
-    ) -> Task<ProjectContext> {
-        let worktrees = project.read(cx).visible_worktrees(cx).collect::<Vec<_>>();
-        let worktree_tasks = worktrees
-            .into_iter()
-            .map(|worktree| {
-                Self::load_worktree_info_for_system_prompt(worktree, project.clone(), cx)
-            })
-            .collect::<Vec<_>>();
-        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::<Vec<_>>();
-
-            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 {
-                            PromptId::User { uuid } => uuid,
-                            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::<Vec<_>>();
-
-            ProjectContext::new(worktrees, default_user_rules)
-        })
-    }
-
-    fn load_worktree_info_for_system_prompt(
-        worktree: Entity<Worktree>,
-        project: Entity<Project>,
-        cx: &mut App,
-    ) -> Task<(WorktreeContext, Option<RulesLoadingError>)> {
-        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<Worktree>,
-        project: Entity<Project>,
-        cx: &mut App,
-    ) -> Option<Task<Result<RulesFileContext>>> {
-        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<Thread>,
-        _: &TitleUpdated,
-        cx: &mut Context<Self>,
-    ) {
-        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<Thread>,
-        usage: &TokenUsageUpdated,
-        cx: &mut Context<Self>,
-    ) {
-        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<Project>,
-        event: &project::Event,
-        _cx: &mut Context<Self>,
-    ) {
-        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<PromptStore>,
-        _event: &prompt_store::PromptsUpdatedEvent,
-        _cx: &mut Context<Self>,
-    ) {
-        self.project_context_needs_refresh.send(()).ok();
-    }
-
-    fn handle_models_updated_event(
-        &mut self,
-        _registry: Entity<LanguageModelRegistry>,
-        _event: &language_model::Event,
-        cx: &mut Context<Self>,
-    ) {
-        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 open_thread(
-        &mut self,
-        id: acp::SessionId,
-        cx: &mut Context<Self>,
-    ) -> Task<Result<Entity<AcpThread>>> {
-        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:?}"))?;
-
-            let thread = this.update(cx, |this, cx| {
-                let action_log = cx.new(|_cx| ActionLog::new(this.project.clone()));
-                cx.new(|cx| {
-                    Thread::from_db(
-                        id.clone(),
-                        db_thread,
-                        this.project.clone(),
-                        this.project_context.clone(),
-                        this.context_server_registry.clone(),
-                        action_log.clone(),
-                        this.templates.clone(),
-                        cx,
-                    )
-                })
-            })?;
-            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<Self>,
-    ) -> Task<Result<SharedString>> {
-        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?;
-            drop(acp_thread);
-            Ok(result)
-        })
-    }
-
-    fn save_thread(&mut self, thread: Entity<Thread>, cx: &mut Context<Self>) {
-        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<NativeAgent>);
-
-impl NativeAgentConnection {
-    pub fn thread(&self, session_id: &acp::SessionId, cx: &App) -> Option<Entity<Thread>> {
-        self.0
-            .read(cx)
-            .sessions
-            .get(session_id)
-            .map(|session| session.thread.clone())
-    }
-
-    fn run_turn(
-        &self,
-        session_id: acp::SessionId,
-        cx: &mut App,
-        f: impl 'static
-        + FnOnce(Entity<Thread>, &mut App) -> Result<mpsc::UnboundedReceiver<Result<ThreadEvent>>>,
-    ) -> Task<Result<acp::PromptResponse>> {
-        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<Result<ThreadEvent>>,
-        acp_thread: WeakEntity<AcpThread>,
-        cx: &App,
-    ) -> Task<Result<acp::PromptResponse>> {
-        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(
-                                        acp::ContentBlock::Text(acp::TextContent {
-                                            text,
-                                            annotations: None,
-                                            meta: None,
-                                        }),
-                                        false,
-                                        cx,
-                                    )
-                                })?;
-                            }
-                            ThreadEvent::AgentThinking(text) => {
-                                acp_thread.update(cx, |thread, cx| {
-                                    thread.push_assistant_content_block(
-                                        acp::ContentBlock::Text(acp::TextContent {
-                                            text,
-                                            annotations: None,
-                                            meta: None,
-                                        }),
-                                        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 { 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 {
-                                    stop_reason,
-                                    meta: None,
-                                });
-                            }
-                        }
-                    }
-                    Err(e) => {
-                        log::error!("Error in model response stream: {:?}", e);
-                        return Err(e);
-                    }
-                }
-            }
-
-            log::debug!("Response stream completed");
-            anyhow::Ok(acp::PromptResponse {
-                stop_reason: acp::StopReason::EndTurn,
-                meta: None,
-            })
-        })
-    }
-}
-
-struct NativeAgentModelSelector {
-    session_id: acp::SessionId,
-    connection: NativeAgentConnection,
-}
-
-impl acp_thread::AgentModelSelector for NativeAgentModelSelector {
-    fn list_models(&self, cx: &mut App) -> Task<Result<acp_thread::AgentModelList>> {
-        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<Result<()>> {
-        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<Result<acp_thread::AgentModelInfo>> {
-        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<watch::Receiver<()>> {
-        Some(self.connection.0.read(cx).models.watch())
-    }
-}
-
-impl acp_thread::AgentConnection for NativeAgentConnection {
-    fn new_thread(
-        self: Rc<Self>,
-        project: Entity<Project>,
-        cwd: &Path,
-        cx: &mut App,
-    ) -> Task<Result<Entity<acp_thread::AcpThread>>> {
-        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<NativeAgent>| -> 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<Result<()>> {
-        Task::ready(Ok(()))
-    }
-
-    fn model_selector(&self, session_id: &acp::SessionId) -> Option<Rc<dyn AgentModelSelector>> {
-        Some(Rc::new(NativeAgentModelSelector {
-            session_id: session_id.clone(),
-            connection: self.clone(),
-        }) as Rc<dyn AgentModelSelector>)
-    }
-
-    fn prompt(
-        &self,
-        id: Option<acp_thread::UserMessageId>,
-        params: acp::PromptRequest,
-        cx: &mut App,
-    ) -> Task<Result<acp::PromptResponse>> {
-        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());
-
-        self.run_turn(session_id, cx, |thread, cx| {
-            let content: Vec<UserMessageContent> = params
-                .prompt
-                .into_iter()
-                .map(Into::into)
-                .collect::<Vec<_>>();
-            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<Rc<dyn acp_thread::AgentSessionResume>> {
-        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<Rc<dyn acp_thread::AgentSessionTruncate>> {
-        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<Rc<dyn acp_thread::AgentSessionSetTitle>> {
-        Some(Rc::new(NativeAgentSessionSetTitle {
-            connection: self.clone(),
-            session_id: session_id.clone(),
-        }) as _)
-    }
-
-    fn telemetry(&self) -> Option<Rc<dyn acp_thread::AgentTelemetry>> {
-        Some(Rc::new(self.clone()) as Rc<dyn acp_thread::AgentTelemetry>)
-    }
-
-    fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
-        self
-    }
-}
-
-impl acp_thread::AgentTelemetry for NativeAgentConnection {
-    fn agent_name(&self) -> String {
-        "Zed".into()
-    }
-
-    fn thread_data(
-        &self,
-        session_id: &acp::SessionId,
-        cx: &mut App,
-    ) -> Task<Result<serde_json::Value>> {
-        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<Thread>,
-    acp_thread: WeakEntity<AcpThread>,
-}
-
-impl acp_thread::AgentSessionTruncate for NativeAgentSessionTruncate {
-    fn run(&self, message_id: acp_thread::UserMessageId, cx: &mut App) -> Task<Result<()>> {
-        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<Result<acp::PromptResponse>> {
-        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<Result<()>> {
-        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<AcpThread>,
-}
-
-impl ThreadEnvironment for AcpThreadEnvironment {
-    fn create_terminal(
-        &self,
-        command: String,
-        cwd: Option<PathBuf>,
-        output_byte_limit: Option<u64>,
-        cx: &mut AsyncApp,
-    ) -> Task<Result<Rc<dyn TerminalHandle>>> {
-        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<acp_thread::Terminal>,
-    _drop_tx: Option<oneshot::Sender<()>>,
-}
-
-impl TerminalHandle for AcpTerminalHandle {
-    fn id(&self, cx: &AsyncApp) -> Result<acp::TerminalId> {
-        self.terminal.read_with(cx, |term, _cx| term.id().clone())
-    }
-
-    fn wait_for_exit(&self, cx: &AsyncApp) -> Result<Shared<Task<acp::TerminalExitStatus>>> {
-        self.terminal
-            .read_with(cx, |term, _cx| term.wait_for_exit())
-    }
-
-    fn current_output(&self, cx: &AsyncApp) -> Result<acp::TerminalOutputResponse> {
-        self.terminal
-            .read_with(cx, |term, cx| term.current_output(cx))
-    }
-}
-
-#[cfg(test)]
-mod 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 context_store = cx.new(|cx| assistant_context::ContextStore::fake(project.clone(), cx));
-        let history_store = cx.new(|cx| HistoryStore::new(context_store, cx));
-        let agent = NativeAgent::new(
-            project.clone(),
-            history_store,
-            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 context_store = cx.new(|cx| assistant_context::ContextStore::fake(project.clone(), cx));
-        let history_store = cx.new(|cx| HistoryStore::new(context_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("fake/fake".into()),
-                    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 context_store = cx.new(|cx| assistant_context::ContextStore::fake(project.clone(), cx));
-        let history_store = cx.new(|cx| HistoryStore::new(context_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("fake/fake".into());
-        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 context_store = cx.new(|cx| assistant_context::ContextStore::fake(project.clone(), cx));
-        let history_store = cx.new(|cx| HistoryStore::new(context_store, cx));
-        let agent = NativeAgent::new(
-            project.clone(),
-            history_store.clone(),
-            Templates::new(),
-            None,
-            fs.clone(),
-            &mut cx.to_async(),
-        )
-        .await
-        .unwrap();
-        let connection = Rc::new(NativeAgentConnection(agent.clone()));
-
-        let acp_thread = cx
-            .update(|cx| {
-                connection
-                    .clone()
-                    .new_thread(project.clone(), Path::new(""), cx)
-            })
-            .await
-            .unwrap();
-        let session_id = acp_thread.read_with(cx, |thread, _| thread.session_id().clone());
-        let thread = agent.read_with(cx, |agent, _| {
-            agent.sessions.get(&session_id).unwrap().thread.clone()
-        });
-
-        // Ensure empty threads are not saved, even if they get mutated.
-        let model = Arc::new(FakeLanguageModel::default());
-        let summary_model = Arc::new(FakeLanguageModel::default());
-        thread.update(cx, |thread, cx| {
-            thread.set_model(model.clone(), cx);
-            thread.set_summarization_model(Some(summary_model.clone()), cx);
-        });
-        cx.run_until_parked();
-        assert_eq!(history_entries(&history_store, cx), vec![]);
-
-        let send = acp_thread.update(cx, |thread, cx| {
-            thread.send(
-                vec![
-                    "What does ".into(),
-                    acp::ContentBlock::ResourceLink(acp::ResourceLink {
-                        name: "b.md".into(),
-                        uri: MentionUri::File {
-                            abs_path: path!("/a/b.md").into(),
-                        }
-                        .to_uri()
-                        .to_string(),
-                        annotations: None,
-                        description: None,
-                        mime_type: None,
-                        size: None,
-                        title: None,
-                        meta: None,
-                    }),
-                    " mean?".into(),
-                ],
-                cx,
-            )
-        });
-        let send = cx.foreground_executor().spawn(send);
-        cx.run_until_parked();
-
-        model.send_last_completion_stream_text_chunk("Lorem.");
-        model.end_last_completion_stream();
-        cx.run_until_parked();
-        summary_model
-            .send_last_completion_stream_text_chunk(&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::<Vec<_>>(), []);
-        });
-
-        // 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<HistoryStore>,
-        cx: &mut TestAppContext,
-    ) -> Vec<(HistoryEntryId, String)> {
-        history.read_with(cx, |history, _| {
-            history
-                .entries()
-                .map(|e| (e.id(), e.title().to_string()))
-                .collect::<Vec<_>>()
-        })
-    }
-
-    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);
-            Project::init_settings(cx);
-            agent_settings::init(cx);
-            language::init(cx);
-            LanguageModelRegistry::test(cx);
-        });
-    }
-}

crates/agent2/src/agent2.rs 🔗

@@ -1,19 +0,0 @@
-mod agent;
-mod db;
-mod history_store;
-mod native_agent_server;
-mod templates;
-mod thread;
-mod tool_schema;
-mod tools;
-
-#[cfg(test)]
-mod tests;
-
-pub use agent::*;
-pub use db::*;
-pub use history_store::*;
-pub use native_agent_server::NativeAgentServer;
-pub use templates::*;
-pub use thread::*;
-pub use tools::*;

crates/agent2/src/thread.rs 🔗

@@ -1,2663 +0,0 @@
-use crate::{
-    ContextServerRegistry, CopyPathTool, CreateDirectoryTool, DbLanguageModel, DbThread,
-    DeletePathTool, DiagnosticsTool, EditFileTool, FetchTool, FindPathTool, GrepTool,
-    ListDirectoryTool, MovePathTool, NowTool, OpenTool, ReadFileTool, SystemPromptTemplate,
-    Template, Templates, TerminalTool, ThinkingTool, WebSearchTool,
-};
-use acp_thread::{MentionUri, UserMessageId};
-use action_log::ActionLog;
-use agent::thread::{GitState, ProjectSnapshot, WorktreeSnapshot};
-use agent_client_protocol as acp;
-use agent_settings::{
-    AgentProfileId, AgentProfileSettings, AgentSettings, CompletionMode,
-    SUMMARIZE_THREAD_DETAILED_PROMPT, SUMMARIZE_THREAD_PROMPT,
-};
-use anyhow::{Context as _, Result, anyhow};
-use assistant_tool::adapt_schema_to_format;
-use chrono::{DateTime, Utc};
-use client::{ModelRequestUsage, RequestUsage, UserStore};
-use cloud_llm_client::{CompletionIntent, CompletionRequestStatus, Plan, UsageLimit};
-use collections::{HashMap, HashSet, IndexMap};
-use fs::Fs;
-use futures::stream;
-use futures::{
-    FutureExt,
-    channel::{mpsc, oneshot},
-    future::Shared,
-    stream::FuturesUnordered,
-};
-use git::repository::DiffType;
-use gpui::{
-    App, AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task, WeakEntity,
-};
-use language_model::{
-    LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelExt,
-    LanguageModelImage, LanguageModelProviderId, LanguageModelRegistry, LanguageModelRequest,
-    LanguageModelRequestMessage, LanguageModelRequestTool, LanguageModelToolResult,
-    LanguageModelToolResultContent, LanguageModelToolSchemaFormat, LanguageModelToolUse,
-    LanguageModelToolUseId, Role, SelectedModel, StopReason, TokenUsage, ZED_CLOUD_PROVIDER_ID,
-};
-use project::{
-    Project,
-    git_store::{GitStore, RepositoryState},
-};
-use prompt_store::ProjectContext;
-use schemars::{JsonSchema, Schema};
-use serde::{Deserialize, Serialize};
-use settings::{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};
-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<str>);
-
-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<LanguageModelRequestMessage> {
-        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,
-            }],
-        }
-    }
-
-    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<UserMessageContent>,
-}
-
-#[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("<image />\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,
-        };
-
-        const OPEN_CONTEXT: &str = "<context>\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 = "<files>";
-        const OPEN_DIRECTORIES_TAG: &str = "<directories>";
-        const OPEN_SYMBOLS_TAG: &str = "<symbols>";
-        const OPEN_SELECTIONS_TAG: &str = "<selections>";
-        const OPEN_THREADS_TAG: &str = "<threads>";
-        const OPEN_FETCH_TAG: &str = "<fetched_urls>";
-        const OPEN_RULES_TAG: &str =
-            "<rules>\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("</files>\n");
-            message
-                .content
-                .push(language_model::MessageContent::Text(file_context));
-        }
-
-        if directory_context.len() > OPEN_DIRECTORIES_TAG.len() {
-            directory_context.push_str("</directories>\n");
-            message
-                .content
-                .push(language_model::MessageContent::Text(directory_context));
-        }
-
-        if symbol_context.len() > OPEN_SYMBOLS_TAG.len() {
-            symbol_context.push_str("</symbols>\n");
-            message
-                .content
-                .push(language_model::MessageContent::Text(symbol_context));
-        }
-
-        if selection_context.len() > OPEN_SELECTIONS_TAG.len() {
-            selection_context.push_str("</selections>\n");
-            message
-                .content
-                .push(language_model::MessageContent::Text(selection_context));
-        }
-
-        if thread_context.len() > OPEN_THREADS_TAG.len() {
-            thread_context.push_str("</threads>\n");
-            message
-                .content
-                .push(language_model::MessageContent::Text(thread_context));
-        }
-
-        if fetch_context.len() > OPEN_FETCH_TAG.len() {
-            fetch_context.push_str("</fetched_urls>\n");
-            message
-                .content
-                .push(language_model::MessageContent::Text(fetch_context));
-        }
-
-        if rules_context.len() > OPEN_RULES_TAG.len() {
-            rules_context.push_str("</user_rules>\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("</context>".into()));
-        }
-
-        message
-    }
-}
-
-fn codeblock_tag(full_path: &Path, line_range: Option<&RangeInclusive<u32>>) -> 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("<think>");
-                    markdown.push_str(text);
-                    markdown.push_str("</think>\n");
-                }
-                AgentMessageContent::RedactedThinking(_) => {
-                    markdown.push_str("<redacted_thinking />\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, "<image />\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<LanguageModelRequestMessage> {
-        let mut assistant_message = LanguageModelRequestMessage {
-            role: Role::Assistant,
-            content: Vec::with_capacity(self.content.len()),
-            cache: false,
-        };
-        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,
-        };
-
-        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 = "<Tool returned an empty string>".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<AgentMessageContent>,
-    pub tool_results: IndexMap<LanguageModelToolUseId, LanguageModelToolResult>,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-pub enum AgentMessageContent {
-    Text(String),
-    Thinking {
-        text: String,
-        signature: Option<String>,
-    },
-    RedactedThinking(String),
-    ToolUse(LanguageModelToolUse),
-}
-
-pub trait TerminalHandle {
-    fn id(&self, cx: &AsyncApp) -> Result<acp::TerminalId>;
-    fn current_output(&self, cx: &AsyncApp) -> Result<acp::TerminalOutputResponse>;
-    fn wait_for_exit(&self, cx: &AsyncApp) -> Result<Shared<Task<acp::TerminalExitStatus>>>;
-}
-
-pub trait ThreadEnvironment {
-    fn create_terminal(
-        &self,
-        command: String,
-        cwd: Option<PathBuf>,
-        output_byte_limit: Option<u64>,
-        cx: &mut AsyncApp,
-    ) -> Task<Result<Rc<dyn TerminalHandle>>>;
-}
-
-#[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<u64>,
-    pub cwd: Option<PathBuf>,
-    pub response: oneshot::Sender<Result<Entity<acp_thread::Terminal>>>,
-}
-
-#[derive(Debug)]
-pub struct ToolCallAuthorization {
-    pub tool_call: acp::ToolCallUpdate,
-    pub options: Vec<acp::PermissionOption>,
-    pub response: oneshot::Sender<acp::PermissionOptionId>,
-}
-
-#[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<Utc>,
-    title: Option<SharedString>,
-    pending_title_generation: Option<Task<()>>,
-    summary: Option<SharedString>,
-    messages: Vec<Message>,
-    user_store: Entity<UserStore>,
-    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<RunningTurn>,
-    pending_message: Option<AgentMessage>,
-    tools: BTreeMap<SharedString, Arc<dyn AnyAgentTool>>,
-    tool_use_limit_reached: bool,
-    request_token_usage: HashMap<UserMessageId, language_model::TokenUsage>,
-    #[allow(unused)]
-    cumulative_token_usage: TokenUsage,
-    #[allow(unused)]
-    initial_project_snapshot: Shared<Task<Option<Arc<ProjectSnapshot>>>>,
-    context_server_registry: Entity<ContextServerRegistry>,
-    profile_id: AgentProfileId,
-    project_context: Entity<ProjectContext>,
-    templates: Arc<Templates>,
-    model: Option<Arc<dyn LanguageModel>>,
-    summarization_model: Option<Arc<dyn LanguageModel>>,
-    prompt_capabilities_tx: watch::Sender<acp::PromptCapabilities>,
-    pub(crate) prompt_capabilities_rx: watch::Receiver<acp::PromptCapabilities>,
-    pub(crate) project: Entity<Project>,
-    pub(crate) action_log: Entity<ActionLog>,
-}
-
-impl Thread {
-    fn prompt_capabilities(model: Option<&dyn LanguageModel>) -> acp::PromptCapabilities {
-        let image = model.map_or(true, |model| model.supports_images());
-        acp::PromptCapabilities {
-            meta: None,
-            image,
-            audio: false,
-            embedded_context: true,
-        }
-    }
-
-    pub fn new(
-        project: Entity<Project>,
-        project_context: Entity<ProjectContext>,
-        context_server_registry: Entity<ContextServerRegistry>,
-        templates: Arc<Templates>,
-        model: Option<Arc<dyn LanguageModel>>,
-        cx: &mut Context<Self>,
-    ) -> Self {
-        let profile_id = AgentSettings::get_global(cx).default_profile.clone();
-        let action_log = cx.new(|_cx| ActionLog::new(project.clone()));
-        let (prompt_capabilities_tx, prompt_capabilities_rx) =
-            watch::channel(Self::prompt_capabilities(model.as_deref()));
-        Self {
-            id: acp::SessionId(uuid::Uuid::new_v4().to_string().into()),
-            prompt_id: PromptId::new(),
-            updated_at: Utc::now(),
-            title: None,
-            pending_title_generation: None,
-            summary: None,
-            messages: Vec::new(),
-            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,
-        }
-    }
-
-    pub fn id(&self) -> &acp::SessionId {
-        &self.id
-    }
-
-    pub fn replay(
-        &mut self,
-        cx: &mut Context<Self>,
-    ) -> mpsc::UnboundedReceiver<Result<ThreadEvent>> {
-        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<Self>,
-    ) {
-        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 {
-                    meta: None,
-                    id: acp::ToolCallId(tool_use.id.to_string().into()),
-                    title: tool_use.name.to_string(),
-                    kind: acp::ToolKind::Other,
-                    status: acp::ToolCallStatus::Failed,
-                    content: Vec::new(),
-                    locations: Vec::new(),
-                    raw_input: Some(tool_use.input.clone()),
-                    raw_output: None,
-                })))
-                .ok();
-            return;
-        };
-
-        let title = tool.initial_title(tool_use.input.clone(), cx);
-        let kind = tool.kind();
-        stream.send_tool_call(&tool_use.id, 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 {
-                status: Some(
-                    tool_result
-                        .as_ref()
-                        .map_or(acp::ToolCallStatus::Failed, |result| {
-                            if result.is_error {
-                                acp::ToolCallStatus::Failed
-                            } else {
-                                acp::ToolCallStatus::Completed
-                            }
-                        }),
-                ),
-                raw_output: output,
-                ..Default::default()
-            },
-        );
-    }
-
-    pub fn from_db(
-        id: acp::SessionId,
-        db_thread: DbThread,
-        project: Entity<Project>,
-        project_context: Entity<ProjectContext>,
-        context_server_registry: Entity<ContextServerRegistry>,
-        action_log: Entity<ActionLog>,
-        templates: Arc<Templates>,
-        cx: &mut Context<Self>,
-    ) -> Self {
-        let profile_id = db_thread
-            .profile
-            .unwrap_or_else(|| AgentSettings::get_global(cx).default_profile.clone());
-        let 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)
-        });
-        let (prompt_capabilities_tx, prompt_capabilities_rx) =
-            watch::channel(Self::prompt_capabilities(model.as_deref()));
-
-        Self {
-            id,
-            prompt_id: PromptId::new(),
-            title: if db_thread.title.is_empty() {
-                None
-            } else {
-                Some(db_thread.title.clone())
-            },
-            pending_title_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,
-        }
-    }
-
-    pub fn to_db(&self, cx: &App) -> Task<DbThread> {
-        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<Project>,
-        cx: &mut Context<Self>,
-    ) -> Task<Arc<agent::thread::ProjectSnapshot>> {
-        let git_store = project.read(cx).git_store().clone();
-        let worktree_snapshots: Vec<_> = project
-            .read(cx)
-            .visible_worktrees(cx)
-            .map(|worktree| Self::worktree_snapshot(worktree, git_store.clone(), cx))
-            .collect();
-
-        cx.spawn(async move |_, _| {
-            let worktree_snapshots = futures::future::join_all(worktree_snapshots).await;
-
-            Arc::new(ProjectSnapshot {
-                worktree_snapshots,
-                timestamp: Utc::now(),
-            })
-        })
-    }
-
-    fn worktree_snapshot(
-        worktree: Entity<project::Worktree>,
-        git_store: Entity<GitStore>,
-        cx: &App,
-    ) -> Task<agent::thread::WorktreeSnapshot> {
-        cx.spawn(async move |cx| {
-            // Get worktree path and snapshot
-            let worktree_info = cx.update(|app_cx| {
-                let worktree = worktree.read(app_cx);
-                let path = worktree.abs_path().to_string_lossy().into_owned();
-                let snapshot = worktree.snapshot();
-                (path, snapshot)
-            });
-
-            let Ok((worktree_path, _snapshot)) = worktree_info else {
-                return WorktreeSnapshot {
-                    worktree_path: String::new(),
-                    git_state: None,
-                };
-            };
-
-            let git_state = git_store
-                .update(cx, |git_store, cx| {
-                    git_store
-                        .repositories()
-                        .values()
-                        .find(|repo| {
-                            repo.read(cx)
-                                .abs_path_to_repo_path(&worktree.read(cx).abs_path())
-                                .is_some()
-                        })
-                        .cloned()
-                })
-                .ok()
-                .flatten()
-                .map(|repo| {
-                    repo.update(cx, |repo, _| {
-                        let current_branch =
-                            repo.branch.as_ref().map(|branch| branch.name().to_owned());
-                        repo.send_job(None, |state, _| async move {
-                            let RepositoryState::Local { backend, .. } = state else {
-                                return GitState {
-                                    remote_url: None,
-                                    head_sha: None,
-                                    current_branch,
-                                    diff: None,
-                                };
-                            };
-
-                            let remote_url = backend.remote_url("origin");
-                            let head_sha = backend.head_sha().await;
-                            let diff = backend.diff(DiffType::HeadToWorktree).await.ok();
-
-                            GitState {
-                                remote_url,
-                                head_sha,
-                                current_branch,
-                                diff,
-                            }
-                        })
-                    })
-                });
-
-            let git_state = match git_state {
-                Some(git_state) => match git_state.ok() {
-                    Some(git_state) => git_state.await.ok(),
-                    None => None,
-                },
-                None => None,
-            };
-
-            WorktreeSnapshot {
-                worktree_path,
-                git_state,
-            }
-        })
-    }
-
-    pub fn project_context(&self) -> &Entity<ProjectContext> {
-        &self.project_context
-    }
-
-    pub fn project(&self) -> &Entity<Project> {
-        &self.project
-    }
-
-    pub fn action_log(&self) -> &Entity<ActionLog> {
-        &self.action_log
-    }
-
-    pub fn is_empty(&self) -> bool {
-        self.messages.is_empty() && self.title.is_none()
-    }
-
-    pub fn model(&self) -> Option<&Arc<dyn LanguageModel>> {
-        self.model.as_ref()
-    }
-
-    pub fn set_model(&mut self, model: Arc<dyn LanguageModel>, cx: &mut Context<Self>) {
-        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<dyn LanguageModel>> {
-        self.summarization_model.as_ref()
-    }
-
-    pub fn set_summarization_model(
-        &mut self,
-        model: Option<Arc<dyn LanguageModel>>,
-        cx: &mut Context<Self>,
-    ) {
-        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<Self>) {
-        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<Message> {
-        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<dyn ThreadEnvironment>,
-        cx: &mut Context<Self>,
-    ) {
-        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,
-        ));
-        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(
-            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<T: AgentTool>(&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) {
-        self.profile_id = profile_id;
-    }
-
-    pub fn cancel(&mut self, cx: &mut Context<Self>) {
-        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<Self>) {
-        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<Self>) -> 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.summary = None;
-        cx.notify();
-        Ok(())
-    }
-
-    pub fn latest_token_usage(&self) -> Option<acp_thread::TokenUsage> {
-        let last_user_message = self.last_user_message()?;
-        let tokens = self.request_token_usage.get(&last_user_message.id)?;
-        let model = self.model.clone()?;
-
-        Some(acp_thread::TokenUsage {
-            max_tokens: model.max_token_count_for_mode(self.completion_mode.into()),
-            used_tokens: tokens.total_tokens(),
-        })
-    }
-
-    pub fn resume(
-        &mut self,
-        cx: &mut Context<Self>,
-    ) -> Result<mpsc::UnboundedReceiver<Result<ThreadEvent>>> {
-        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<T>(
-        &mut self,
-        id: UserMessageId,
-        content: impl IntoIterator<Item = T>,
-        cx: &mut Context<Self>,
-    ) -> Result<mpsc::UnboundedReceiver<Result<ThreadEvent>>>
-    where
-        T: Into<UserMessageContent>,
-    {
-        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::<Vec<_>>();
-        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)
-    }
-
-    fn run_turn(
-        &mut self,
-        cx: &mut Context<Self>,
-    ) -> Result<mpsc::UnboundedReceiver<Result<ThreadEvent>>> {
-        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::<Result<ThreadEvent>>();
-        let event_stream = ThreadEventStream(events_tx);
-        let message_ix = self.messages.len().saturating_sub(1);
-        self.tool_use_limit_reached = false;
-        self.summary = None;
-        self.running_turn = Some(RunningTurn {
-            event_stream: event_stream.clone(),
-            tools: self.enabled_tools(profile, &model, cx),
-            _task: cx.spawn(async move |this, cx| {
-                log::debug!("Starting agent turn execution");
-
-                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::<CompletionError>() {
-                            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<Self>,
-        model: Arc<dyn LanguageModel>,
-        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 {
-                        status: Some(if tool_result.is_error {
-                            acp::ToolCallStatus::Failed
-                        } else {
-                            acp::ToolCallStatus::Completed
-                        }),
-                        raw_output: tool_result.output.clone(),
-                        ..Default::default()
-                    },
-                );
-                this.update(cx, |this, _cx| {
-                    this.pending_message()
-                        .tool_results
-                        .insert(tool_result.tool_use_id.clone(), tool_result);
-                })?;
-            }
-
-            this.update(cx, |this, cx| {
-                this.flush_pending_message(cx);
-                if this.title.is_none() && this.pending_title_generation.is_none() {
-                    this.generate_title(cx);
-                }
-            })?;
-
-            if let Some(error) = error {
-                attempt += 1;
-                let retry = this.update(cx, |this, 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<Plan>,
-    ) -> Result<acp_thread::RetryStatus> {
-        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<Self>,
-    ) -> Result<Option<Task<LanguageModelToolResult>>> {
-        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),
-            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);
-            }
-            StatusUpdate(CompletionRequestStatus::UsageUpdated { amount, limit }) => {
-                self.update_model_request_usage(amount, limit, cx);
-            }
-            StatusUpdate(
-                CompletionRequestStatus::Started
-                | CompletionRequestStatus::Queued { .. }
-                | CompletionRequestStatus::Failed { .. },
-            ) => {}
-            StatusUpdate(CompletionRequestStatus::ToolUseLimitReached) => {
-                self.tool_use_limit_reached = true;
-            }
-            Stop(StopReason::Refusal) => return Err(CompletionError::Refusal.into()),
-            Stop(StopReason::MaxTokens) => return Err(CompletionError::MaxTokens.into()),
-            Stop(StopReason::ToolUse | StopReason::EndTurn) => {}
-        }
-
-        Ok(None)
-    }
-
-    fn handle_text_event(
-        &mut self,
-        new_text: String,
-        event_stream: &ThreadEventStream,
-        cx: &mut Context<Self>,
-    ) {
-        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<String>,
-        event_stream: &ThreadEventStream,
-        cx: &mut Context<Self>,
-    ) {
-        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<Self>) {
-        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<Self>,
-    ) -> Option<Task<LanguageModelToolResult>> {
-        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, 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 {
-                    title: Some(title.into()),
-                    kind: Some(kind),
-                    raw_input: Some(tool_use.input.clone()),
-                    ..Default::default()
-                },
-            );
-        }
-
-        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 {
-            status: Some(acp::ToolCallStatus::InProgress),
-            ..Default::default()
-        });
-        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<str>,
-        raw_input: Arc<str>,
-        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>) {
-        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 summary(&mut self, cx: &mut Context<Self>) -> Task<Result<SharedString>> {
-        if let Some(summary) = self.summary.as_ref() {
-            return Task::ready(Ok(summary.clone()));
-        }
-        let Some(model) = self.summarization_model.clone() else {
-            return Task::ready(Err(anyhow!("No summarization model available")));
-        };
-        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,
-        });
-        cx.spawn(async move |this, cx| {
-            let mut summary = String::new();
-            let mut messages = model.stream_completion(request, cx).await?;
-            while let Some(event) = messages.next().await {
-                let event = event?;
-                let text = match event {
-                    LanguageModelCompletionEvent::Text(text) => text,
-                    LanguageModelCompletionEvent::StatusUpdate(
-                        CompletionRequestStatus::UsageUpdated { amount, limit },
-                    ) => {
-                        this.update(cx, |thread, cx| {
-                            thread.update_model_request_usage(amount, limit, cx);
-                        })?;
-                        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());
-                cx.notify()
-            })?;
-
-            Ok(summary)
-        })
-    }
-
-    fn generate_title(&mut self, cx: &mut Context<Self>) {
-        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,
-        });
-        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::StatusUpdate(
-                            CompletionRequestStatus::UsageUpdated { amount, limit },
-                        ) => {
-                            this.update(cx, |thread, cx| {
-                                thread.update_model_request_usage(amount, limit, cx);
-                            })?;
-                            continue;
-                        }
-                        _ => continue,
-                    };
-
-                    let mut lines = text.lines();
-                    title.extend(lines.next());
-
-                    // Stop if the LLM generated multiple lines.
-                    if lines.next().is_some() {
-                        break;
-                    }
-                }
-                anyhow::Ok(())
-            };
-
-            if generate.await.context("failed to generate title").is_ok() {
-                _ = this.update(cx, |this, cx| this.set_title(title.into(), cx));
-            }
-            _ = this.update(cx, |this, _| this.pending_title_generation = None);
-        }));
-    }
-
-    pub fn set_title(&mut self, title: SharedString, cx: &mut Context<Self>) {
-        self.pending_title_generation = None;
-        if Some(&title) != self.title.as_ref() {
-            self.title = Some(title);
-            cx.emit(TitleUpdated);
-            cx.notify();
-        }
-    }
-
-    fn last_user_message(&self) -> Option<&UserMessage> {
-        self.messages
-            .iter()
-            .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<Self>) {
-        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.summary = None;
-        cx.notify()
-    }
-
-    pub(crate) fn build_completion_request(
-        &self,
-        completion_intent: CompletionIntent,
-        cx: &App,
-    ) -> Result<LanguageModelRequest> {
-        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::<Vec<_>>()
-        } else {
-            Vec::new()
-        };
-
-        log::debug!("Building completion request");
-        log::debug!("Completion intent: {:?}", completion_intent);
-        log::debug!("Completion mode: {:?}", self.completion_mode);
-
-        let messages = self.build_request_messages(cx);
-        log::debug!("Request will include {} messages", messages.len());
-        log::debug!("Request includes {} tools", tools.len());
-
-        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<dyn LanguageModel>,
-        cx: &App,
-    ) -> BTreeMap<SharedString, Arc<dyn AnyAgentTool>> {
-        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.supported_provider(&model.provider_id())
-                    && profile.is_tool_enabled(tool_name)
-                {
-                    Some((truncate(tool_name), tool.clone()))
-                } else {
-                    None
-                }
-            })
-            .collect::<BTreeMap<_, _>>();
-
-        let mut context_server_tools = Vec::new();
-        let mut seen_tools = tools.keys().cloned().collect::<HashSet<_>>();
-        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<Arc<dyn AnyAgentTool>> {
-        self.running_turn.as_ref()?.tools.get(name).cloned()
-    }
-
-    fn build_request_messages(&self, cx: &App) -> Vec<LanguageModelRequestMessage> {
-        log::trace!(
-            "Building request messages from {} thread messages",
-            self.messages.len()
-        );
-
-        let system_prompt = SystemPromptTemplate {
-            project: self.project_context.read(cx),
-            available_tools: self.tools.keys().cloned().collect(),
-        }
-        .render(&self.templates)
-        .context("failed to build system prompt")
-        .expect("Invalid template");
-        let mut messages = vec![LanguageModelRequestMessage {
-            role: Role::System,
-            content: vec![system_prompt.into()],
-            cache: false,
-        }];
-        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<RetryStrategy> {
-        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::<language_model::PaymentRequiredError>()
-                    || err.is::<language_model::ModelRequestLimitReachedError>() =>
-            {
-                // 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<SharedString, Arc<dyn AnyAgentTool>>,
-}
-
-impl RunningTurn {
-    fn cancel(self) {
-        log::debug!("Cancelling in progress turn");
-        self.event_stream.send_canceled();
-    }
-}
-
-pub struct TokenUsageUpdated(pub Option<acp_thread::TokenUsage>);
-
-impl EventEmitter<TokenUsageUpdated> for Thread {}
-
-pub struct TitleUpdated;
-
-impl EventEmitter<TitleUpdated> 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<LanguageModelToolResultContent>;
-
-    fn name() -> &'static str;
-
-    fn description(&self) -> 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<Self::Input, serde_json::Value>,
-        cx: &mut App,
-    ) -> SharedString;
-
-    /// Returns the JSON schema that describes the tool's input.
-    fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Schema {
-        crate::tool_schema::root_schema_for::<Self::Input>(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 supported_provider(&self, _provider: &LanguageModelProviderId) -> bool {
-        true
-    }
-
-    /// Runs the tool with the provided input.
-    fn run(
-        self: Arc<Self>,
-        input: Self::Input,
-        event_stream: ToolCallEventStream,
-        cx: &mut App,
-    ) -> Task<Result<Self::Output>>;
-
-    /// 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<dyn AnyAgentTool> {
-        Arc::new(Erased(Arc::new(self)))
-    }
-}
-
-pub struct Erased<T>(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<serde_json::Value>;
-    fn supported_provider(&self, _provider: &LanguageModelProviderId) -> bool {
-        true
-    }
-    fn run(
-        self: Arc<Self>,
-        input: serde_json::Value,
-        event_stream: ToolCallEventStream,
-        cx: &mut App,
-    ) -> Task<Result<AgentToolOutput>>;
-    fn replay(
-        &self,
-        input: serde_json::Value,
-        output: serde_json::Value,
-        event_stream: ToolCallEventStream,
-        cx: &mut App,
-    ) -> Result<()>;
-}
-
-impl<T> AnyAgentTool for Erased<Arc<T>>
-where
-    T: AgentTool,
-{
-    fn name(&self) -> SharedString {
-        T::name().into()
-    }
-
-    fn description(&self) -> SharedString {
-        self.0.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<serde_json::Value> {
-        let mut json = serde_json::to_value(self.0.input_schema(format))?;
-        adapt_schema_to_format(&mut json, format)?;
-        Ok(json)
-    }
-
-    fn supported_provider(&self, provider: &LanguageModelProviderId) -> bool {
-        self.0.supported_provider(provider)
-    }
-
-    fn run(
-        self: Arc<Self>,
-        input: serde_json::Value,
-        event_stream: ToolCallEventStream,
-        cx: &mut App,
-    ) -> Task<Result<AgentToolOutput>> {
-        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<Result<ThreadEvent>>);
-
-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,
-        title: SharedString,
-        kind: acp::ToolKind,
-        input: serde_json::Value,
-    ) {
-        self.0
-            .unbounded_send(Ok(ThreadEvent::ToolCall(Self::initial_tool_call(
-                id,
-                title.to_string(),
-                kind,
-                input,
-            ))))
-            .ok();
-    }
-
-    fn initial_tool_call(
-        id: &LanguageModelToolUseId,
-        title: String,
-        kind: acp::ToolKind,
-        input: serde_json::Value,
-    ) -> acp::ToolCall {
-        acp::ToolCall {
-            meta: None,
-            id: acp::ToolCallId(id.to_string().into()),
-            title,
-            kind,
-            status: acp::ToolCallStatus::Pending,
-            content: vec![],
-            locations: vec![],
-            raw_input: Some(input),
-            raw_output: None,
-        }
-    }
-
-    fn update_tool_call_fields(
-        &self,
-        tool_use_id: &LanguageModelToolUseId,
-        fields: acp::ToolCallUpdateFields,
-    ) {
-        self.0
-            .unbounded_send(Ok(ThreadEvent::ToolCallUpdate(
-                acp::ToolCallUpdate {
-                    meta: None,
-                    id: acp::ToolCallId(tool_use_id.to_string().into()),
-                    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<anyhow::Error>) {
-        self.0.unbounded_send(Err(error.into())).ok();
-    }
-}
-
-#[derive(Clone)]
-pub struct ToolCallEventStream {
-    tool_use_id: LanguageModelToolUseId,
-    stream: ThreadEventStream,
-    fs: Option<Arc<dyn Fs>>,
-}
-
-impl ToolCallEventStream {
-    #[cfg(test)]
-    pub fn test() -> (Self, ToolCallEventStreamReceiver) {
-        let (events_tx, events_rx) = mpsc::unbounded::<Result<ThreadEvent>>();
-
-        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<Arc<dyn Fs>>,
-    ) -> 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<acp_thread::Diff>) {
-        self.stream
-            .0
-            .unbounded_send(Ok(ThreadEvent::ToolCallUpdate(
-                acp_thread::ToolCallUpdateDiff {
-                    id: acp::ToolCallId(self.tool_use_id.to_string().into()),
-                    diff,
-                }
-                .into(),
-            )))
-            .ok();
-    }
-
-    pub fn authorize(&self, title: impl Into<String>, cx: &mut App) -> Task<Result<()>> {
-        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 {
-                        meta: None,
-                        id: acp::ToolCallId(self.tool_use_id.to_string().into()),
-                        fields: acp::ToolCallUpdateFields {
-                            title: Some(title.into()),
-                            ..Default::default()
-                        },
-                    },
-                    options: vec![
-                        acp::PermissionOption {
-                            id: acp::PermissionOptionId("always_allow".into()),
-                            name: "Always Allow".into(),
-                            kind: acp::PermissionOptionKind::AllowAlways,
-                            meta: None,
-                        },
-                        acp::PermissionOption {
-                            id: acp::PermissionOptionId("allow".into()),
-                            name: "Allow".into(),
-                            kind: acp::PermissionOptionKind::AllowOnce,
-                            meta: None,
-                        },
-                        acp::PermissionOption {
-                            id: acp::PermissionOptionId("deny".into()),
-                            name: "Deny".into(),
-                            kind: acp::PermissionOptionKind::RejectOnce,
-                            meta: None,
-                        },
-                    ],
-                    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(test)]
-pub struct ToolCallEventStreamReceiver(mpsc::UnboundedReceiver<Result<ThreadEvent>>);
-
-#[cfg(test)]
-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<acp_thread::Diff> {
-        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<acp_thread::Terminal> {
-        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(test)]
-impl std::ops::Deref for ToolCallEventStreamReceiver {
-    type Target = mpsc::UnboundedReceiver<Result<ThreadEvent>>;
-
-    fn deref(&self) -> &Self::Target {
-        &self.0
-    }
-}
-
-#[cfg(test)]
-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 From<acp::ContentBlock> for UserMessageContent {
-    fn from(value: acp::ContentBlock) -> 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) {
-                    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) {
-                        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())
-                }
-            },
-        }
-    }
-}
-
-impl From<UserMessageContent> for acp::ContentBlock {
-    fn from(content: UserMessageContent) -> Self {
-        match content {
-            UserMessageContent::Text(text) => acp::ContentBlock::Text(acp::TextContent {
-                text,
-                annotations: None,
-                meta: None,
-            }),
-            UserMessageContent::Image(image) => acp::ContentBlock::Image(acp::ImageContent {
-                data: image.source.to_string(),
-                mime_type: "image/png".to_string(),
-                meta: None,
-                annotations: None,
-                uri: None,
-            }),
-            UserMessageContent::Mention { uri, content } => {
-                acp::ContentBlock::Resource(acp::EmbeddedResource {
-                    meta: None,
-                    resource: acp::EmbeddedResourceResource::TextResourceContents(
-                        acp::TextResourceContents {
-                            meta: None,
-                            mime_type: None,
-                            text: content,
-                            uri: uri.to_uri().to_string(),
-                        },
-                    ),
-                    annotations: None,
-                })
-            }
-        }
-    }
-}
-
-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()),
-    }
-}

crates/agent2/src/tool_schema.rs 🔗

@@ -1,43 +0,0 @@
-use language_model::LanguageModelToolSchemaFormat;
-use schemars::{
-    JsonSchema, Schema,
-    generate::SchemaSettings,
-    transform::{Transform, transform_subschemas},
-};
-
-pub(crate) fn root_schema_for<T: JsonSchema>(format: LanguageModelToolSchemaFormat) -> Schema {
-    let mut generator = match format {
-        LanguageModelToolSchemaFormat::JsonSchema => SchemaSettings::draft07().into_generator(),
-        LanguageModelToolSchemaFormat::JsonSchemaSubset => SchemaSettings::openapi3()
-            .with(|settings| {
-                settings.meta_schema = None;
-                settings.inline_subschemas = true;
-            })
-            .with_transform(ToJsonSchemaSubsetTransform)
-            .into_generator(),
-    };
-    generator.root_schema_for::<T>()
-}
-
-#[derive(Debug, Clone)]
-struct ToJsonSchemaSubsetTransform;
-
-impl Transform for ToJsonSchemaSubsetTransform {
-    fn transform(&mut self, schema: &mut Schema) {
-        // Ensure that the type field is not an array, this happens when we use
-        // Option<T>, the type will be [T, "null"].
-        if let Some(type_field) = schema.get_mut("type")
-            && let Some(types) = type_field.as_array()
-            && let Some(first_type) = types.first()
-        {
-            *type_field = first_type.clone();
-        }
-
-        // oneOf is not supported, use anyOf instead
-        if let Some(one_of) = schema.remove("oneOf") {
-            schema.insert("anyOf".to_string(), one_of);
-        }
-
-        transform_subschemas(self, schema);
-    }
-}

crates/agent2/src/tools.rs 🔗

@@ -1,60 +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;
-
-/// A list of all built in tool names, for use in deduplicating MCP tool names
-pub fn default_tool_names() -> impl Iterator<Item = &'static str> {
-    [
-        CopyPathTool::name(),
-        CreateDirectoryTool::name(),
-        DeletePathTool::name(),
-        DiagnosticsTool::name(),
-        EditFileTool::name(),
-        FetchTool::name(),
-        FindPathTool::name(),
-        GrepTool::name(),
-        ListDirectoryTool::name(),
-        MovePathTool::name(),
-        NowTool::name(),
-        OpenTool::name(),
-        ReadFileTool::name(),
-        TerminalTool::name(),
-        ThinkingTool::name(),
-        WebSearchTool::name(),
-    ]
-    .into_iter()
-}
-
-pub use context_server_registry::*;
-pub use copy_path_tool::*;
-pub use create_directory_tool::*;
-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::*;
-
-use crate::AgentTool;

crates/agent_servers/Cargo.toml 🔗

@@ -51,7 +51,6 @@ terminal.workspace = true
 uuid.workspace = true
 util.workspace = true
 watch.workspace = true
-workspace-hack.workspace = true
 
 [target.'cfg(unix)'.dependencies]
 libc.workspace = true

crates/agent_servers/src/acp.rs 🔗

@@ -9,9 +9,7 @@ use futures::io::BufReader;
 use project::Project;
 use project::agent_server_store::AgentServerCommand;
 use serde::Deserialize;
-use settings::{Settings as _, SettingsLocation};
-use task::Shell;
-use util::{ResultExt as _, get_default_system_shell_preferring_bash};
+use util::ResultExt as _;
 
 use std::path::PathBuf;
 use std::{any::Any, cell::RefCell};
@@ -23,7 +21,7 @@ use gpui::{App, AppContext as _, AsyncApp, Entity, SharedString, Task, WeakEntit
 
 use acp_thread::{AcpThread, AuthRequired, LoadError, TerminalProviderEvent};
 use terminal::TerminalBuilder;
-use terminal::terminal_settings::{AlternateScroll, CursorShape, TerminalSettings};
+use terminal::terminal_settings::{AlternateScroll, CursorShape};
 
 #[derive(Debug, Error)]
 #[error("Unsupported version")]
@@ -40,7 +38,7 @@ pub struct AcpConnection {
     // 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<Result<()>>,
+    _io_task: Task<Result<(), acp::Error>>,
     _wait_task: Task<Result<()>>,
     _stderr_task: Task<Result<()>>,
 }
@@ -816,62 +814,18 @@ impl acp::Client for ClientDelegate {
         let thread = self.session_thread(&args.session_id)?;
         let project = thread.read_with(&self.cx, |thread, _cx| thread.project().clone())?;
 
-        let mut env = if let Some(dir) = &args.cwd {
-            project
-                .update(&mut self.cx.clone(), |project, cx| {
-                    let worktree = project.find_worktree(dir.as_path(), cx);
-                    let shell = TerminalSettings::get(
-                        worktree.as_ref().map(|(worktree, path)| SettingsLocation {
-                            worktree_id: worktree.read(cx).id(),
-                            path: &path,
-                        }),
-                        cx,
-                    )
-                    .shell
-                    .clone();
-                    project.directory_environment(&shell, dir.clone().into(), cx)
-                })?
-                .await
-                .unwrap_or_default()
-        } else {
-            Default::default()
-        };
-        // Disables paging for `git` and hopefully other commands
-        env.insert("PAGER".into(), "".into());
-        for var in args.env {
-            env.insert(var.name, var.value);
-        }
-
-        // Use remote shell or default system shell, as appropriate
-        let shell = project
-            .update(&mut self.cx.clone(), |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(&self.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(args.command.clone()), &args.args);
-
-        let terminal_entity = project
-            .update(&mut self.cx.clone(), |project, cx| {
-                project.create_terminal_task(
-                    task::SpawnInTerminal {
-                        command: Some(task_command),
-                        args: task_args,
-                        cwd: args.cwd.clone(),
-                        env,
-                        ..Default::default()
-                    },
-                    cx,
-                )
-            })?
-            .await?;
+        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| {

crates/agent_settings/Cargo.toml 🔗

@@ -24,7 +24,6 @@ schemars.workspace = true
 serde.workspace = true
 settings.workspace = true
 util.workspace = true
-workspace-hack.workspace = true
 
 [dev-dependencies]
 fs.workspace = true

crates/agent_settings/src/agent_settings.rs 🔗

@@ -10,15 +10,14 @@ use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 use settings::{
     DefaultAgentView, DockPosition, LanguageModelParameters, LanguageModelSelection,
-    NotifyWhenAgentWaiting, Settings, SettingsContent,
+    NotifyWhenAgentWaiting, Settings,
 };
 
 pub use crate::agent_profile::*;
 
-pub const SUMMARIZE_THREAD_PROMPT: &str =
-    include_str!("../../agent/src/prompts/summarize_thread_prompt.txt");
+pub const SUMMARIZE_THREAD_PROMPT: &str = include_str!("prompts/summarize_thread_prompt.txt");
 pub const SUMMARIZE_THREAD_DETAILED_PROMPT: &str =
-    include_str!("../../agent/src/prompts/summarize_thread_detailed_prompt.txt");
+    include_str!("prompts/summarize_thread_detailed_prompt.txt");
 
 pub fn init(cx: &mut App) {
     AgentSettings::register(cx);
@@ -42,7 +41,6 @@ pub struct AgentSettings {
     pub always_allow_tool_actions: bool,
     pub notify_when_agent_waiting: NotifyWhenAgentWaiting,
     pub play_sound_when_agent_done: bool,
-    pub stream_edits: bool,
     pub single_file_review: bool,
     pub model_parameters: Vec<LanguageModelParameters>,
     pub preferred_completion_mode: CompletionMode,
@@ -175,7 +173,6 @@ impl Settings for AgentSettings {
             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(),
-            stream_edits: agent.stream_edits.unwrap(),
             single_file_review: agent.single_file_review.unwrap(),
             model_parameters: agent.model_parameters,
             preferred_completion_mode: agent.preferred_completion_mode.unwrap().into(),
@@ -186,14 +183,4 @@ impl Settings for AgentSettings {
             message_editor_min_lines: agent.message_editor_min_lines.unwrap(),
         }
     }
-
-    fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut SettingsContent) {
-        if let Some(b) = vscode
-            .read_value("chat.agent.enabled")
-            .and_then(|b| b.as_bool())
-        {
-            current.agent.get_or_insert_default().enabled = Some(b);
-            current.agent.get_or_insert_default().button = Some(b);
-        }
-    }
 }

crates/agent_ui/Cargo.toml 🔗

@@ -20,16 +20,14 @@ acp_thread.workspace = true
 action_log.workspace = true
 agent-client-protocol.workspace = true
 agent.workspace = true
-agent2.workspace = true
 agent_servers.workspace = true
 agent_settings.workspace = true
 ai_onboarding.workspace = true
 anyhow.workspace = true
 arrayvec.workspace = true
-assistant_context.workspace = true
+assistant_text_thread.workspace = true
 assistant_slash_command.workspace = true
 assistant_slash_commands.workspace = true
-assistant_tool.workspace = true
 audio.workspace = true
 buffer_diff.workspace = true
 chrono.workspace = true
@@ -71,6 +69,7 @@ postage.workspace = true
 project.workspace = true
 prompt_store.workspace = true
 proto.workspace = true
+ref-cast.workspace = true
 release_channel.workspace = true
 rope.workspace = true
 rules_library.workspace = true
@@ -97,16 +96,13 @@ url.workspace = true
 urlencoding.workspace = true
 util.workspace = true
 watch.workspace = true
-workspace-hack.workspace = true
 workspace.workspace = true
 zed_actions.workspace = true
 
 [dev-dependencies]
 acp_thread = { workspace = true, features = ["test-support"] }
 agent = { workspace = true, features = ["test-support"] }
-agent2 = { workspace = true, features = ["test-support"] }
-assistant_context = { workspace = true, features = ["test-support"] }
-assistant_tools.workspace = true
+assistant_text_thread = { workspace = true, features = ["test-support"] }
 buffer_diff = { workspace = true, features = ["test-support"] }
 db = { workspace = true, features = ["test-support"] }
 editor = { workspace = true, features = ["test-support"] }

crates/agent_ui/src/acp/completion_provider.rs 🔗

@@ -6,8 +6,8 @@ use std::sync::Arc;
 use std::sync::atomic::AtomicBool;
 
 use acp_thread::MentionUri;
+use agent::{HistoryEntry, HistoryStore};
 use agent_client_protocol as acp;
-use agent2::{HistoryEntry, HistoryStore};
 use anyhow::Result;
 use editor::{CompletionProvider, Editor, ExcerptId};
 use fuzzy::{StringMatch, StringMatchCandidate};
@@ -32,6 +32,7 @@ use crate::context_picker::file_context_picker::{FileMatch, search_files};
 use crate::context_picker::rules_context_picker::{RulesContextEntry, search_rules};
 use crate::context_picker::symbol_context_picker::SymbolMatch;
 use crate::context_picker::symbol_context_picker::search_symbols;
+use crate::context_picker::thread_context_picker::search_threads;
 use crate::context_picker::{
     ContextPickerAction, ContextPickerEntry, ContextPickerMode, selection_ranges,
 };
@@ -658,7 +659,9 @@ impl ContextPickerCompletionProvider {
             .active_item(cx)
             .and_then(|item| item.downcast::<Editor>())
             .is_some_and(|editor| {
-                editor.update(cx, |editor, cx| editor.has_non_empty_selection(cx))
+                editor.update(cx, |editor, cx| {
+                    editor.has_non_empty_selection(&editor.display_snapshot(cx))
+                })
             });
         if has_selection {
             entries.push(ContextPickerEntry::Action(
@@ -947,42 +950,6 @@ impl CompletionProvider for ContextPickerCompletionProvider {
     }
 }
 
-pub(crate) fn search_threads(
-    query: String,
-    cancellation_flag: Arc<AtomicBool>,
-    history_store: &Entity<HistoryStore>,
-    cx: &mut App,
-) -> Task<Vec<HistoryEntry>> {
-    let threads = history_store.read(cx).entries().collect();
-    if query.is_empty() {
-        return Task::ready(threads);
-    }
-
-    let executor = cx.background_executor().clone();
-    cx.background_spawn(async move {
-        let candidates = threads
-            .iter()
-            .enumerate()
-            .map(|(id, thread)| StringMatchCandidate::new(id, thread.title()))
-            .collect::<Vec<_>>();
-        let matches = fuzzy::match_strings(
-            &candidates,
-            &query,
-            false,
-            true,
-            100,
-            &cancellation_flag,
-            executor,
-        )
-        .await;
-
-        matches
-            .into_iter()
-            .map(|mat| threads[mat.candidate_id].clone())
-            .collect()
-    })
-}
-
 fn confirm_completion_callback(
     crease_text: SharedString,
     start: Anchor,

crates/agent_ui/src/acp/entry_view_state.rs 🔗

@@ -1,8 +1,8 @@
 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 agent2::HistoryStore;
 use collections::HashMap;
 use editor::{Editor, EditorMode, MinimapVisibility};
 use gpui::{
@@ -399,10 +399,10 @@ mod tests {
     use std::{path::Path, rc::Rc};
 
     use acp_thread::{AgentConnection, StubAgentConnection};
+    use agent::HistoryStore;
     use agent_client_protocol as acp;
     use agent_settings::AgentSettings;
-    use agent2::HistoryStore;
-    use assistant_context::ContextStore;
+    use assistant_text_thread::TextThreadStore;
     use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind};
     use editor::{EditorSettings, RowInfo};
     use fs::FakeFs;
@@ -466,8 +466,8 @@ mod tests {
             connection.send_update(session_id, acp::SessionUpdate::ToolCall(tool_call), cx)
         });
 
-        let context_store = cx.new(|cx| ContextStore::fake(project.clone(), cx));
-        let history_store = cx.new(|cx| HistoryStore::new(context_store, 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(

crates/agent_ui/src/acp/message_editor.rs 🔗

@@ -3,19 +3,18 @@ use crate::{
     context_picker::{ContextPickerAction, fetch_context_picker::fetch_url_content},
 };
 use acp_thread::{MentionUri, selection_name};
+use agent::{HistoryStore, outline};
 use agent_client_protocol as acp;
 use agent_servers::{AgentServer, AgentServerDelegate};
-use agent2::HistoryStore;
 use anyhow::{Result, anyhow};
 use assistant_slash_commands::codeblock_fence_for_path;
-use assistant_tool::outline;
 use collections::{HashMap, HashSet};
 use editor::{
     Addon, Anchor, AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement,
-    EditorEvent, EditorMode, EditorSnapshot, EditorStyle, ExcerptId, FoldPlaceholder, InlayId,
+    EditorEvent, EditorMode, EditorSnapshot, EditorStyle, ExcerptId, FoldPlaceholder, Inlay,
     MultiBuffer, ToOffset,
     actions::Paste,
-    display_map::{Crease, CreaseId, FoldId, Inlay},
+    display_map::{Crease, CreaseId, FoldId},
 };
 use futures::{
     FutureExt as _,
@@ -30,7 +29,8 @@ use language::{Buffer, Language, language_settings::InlayHintKind};
 use language_model::LanguageModelImage;
 use postage::stream::Stream as _;
 use project::{
-    CompletionIntent, InlayHint, InlayHintLabel, Project, ProjectItem, ProjectPath, Worktree,
+    CompletionIntent, InlayHint, InlayHintLabel, InlayId, Project, ProjectItem, ProjectPath,
+    Worktree,
 };
 use prompt_store::{PromptId, PromptStore};
 use rope::Point;
@@ -76,7 +76,7 @@ pub enum MessageEditorEvent {
 
 impl EventEmitter<MessageEditorEvent> for MessageEditor {}
 
-const COMMAND_HINT_INLAY_ID: u32 = 0;
+const COMMAND_HINT_INLAY_ID: InlayId = InlayId::Hint(0);
 
 impl MessageEditor {
     pub fn new(
@@ -152,7 +152,7 @@ impl MessageEditor {
                         let has_new_hint = !new_hints.is_empty();
                         editor.splice_inlays(
                             if has_hint {
-                                &[InlayId::Hint(COMMAND_HINT_INLAY_ID)]
+                                &[COMMAND_HINT_INLAY_ID]
                             } else {
                                 &[]
                             },
@@ -230,7 +230,7 @@ impl MessageEditor {
 
     pub fn insert_thread_summary(
         &mut self,
-        thread: agent2::DbThreadMetadata,
+        thread: agent::DbThreadMetadata,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
@@ -599,7 +599,7 @@ impl MessageEditor {
         id: acp::SessionId,
         cx: &mut Context<Self>,
     ) -> Task<Result<Mention>> {
-        let server = Rc::new(agent2::NativeAgentServer::new(
+        let server = Rc::new(agent::NativeAgentServer::new(
             self.project.read(cx).fs().clone(),
             self.history_store.clone(),
         ));
@@ -612,7 +612,7 @@ impl MessageEditor {
         let connection = server.connect(None, delegate, cx);
         cx.spawn(async move |_, cx| {
             let (agent, _) = connection.await?;
-            let agent = agent.downcast::<agent2::NativeAgentConnection>().unwrap();
+            let agent = agent.downcast::<agent::NativeAgentConnection>().unwrap();
             let summary = agent
                 .0
                 .update(cx, |agent, cx| agent.thread_summary(id, cx))?
@@ -629,12 +629,12 @@ impl MessageEditor {
         path: PathBuf,
         cx: &mut Context<Self>,
     ) -> Task<Result<Mention>> {
-        let context = self.history_store.update(cx, |text_thread_store, cx| {
-            text_thread_store.load_text_thread(path.as_path().into(), cx)
+        let text_thread_task = self.history_store.update(cx, |store, cx| {
+            store.load_text_thread(path.as_path().into(), cx)
         });
         cx.spawn(async move |_, cx| {
-            let context = context.await?;
-            let xml = context.update(cx, |context, cx| context.to_xml(cx))?;
+            let text_thread = text_thread_task.await?;
+            let xml = text_thread.update(cx, |text_thread, cx| text_thread.to_xml(cx))?;
             Ok(Mention::Text {
                 content: xml,
                 tracked_buffers: Vec::new(),
@@ -1589,10 +1589,9 @@ 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 agent2::HistoryStore;
-    use assistant_context::ContextStore;
-    use assistant_tool::outline;
+    use assistant_text_thread::TextThreadStore;
     use editor::{AnchorRangeExt as _, Editor, EditorMode};
     use fs::FakeFs;
     use futures::StreamExt as _;
@@ -1623,8 +1622,8 @@ mod tests {
         let (workspace, cx) =
             cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
 
-        let context_store = cx.new(|cx| ContextStore::fake(project.clone(), cx));
-        let history_store = cx.new(|cx| HistoryStore::new(context_store, 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| {
@@ -1729,8 +1728,8 @@ mod tests {
         .await;
 
         let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
-        let context_store = cx.new(|cx| ContextStore::fake(project.clone(), cx));
-        let history_store = cx.new(|cx| HistoryStore::new(context_store, 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()));
         // Start with no available commands - simulating Claude which doesn't support slash commands
         let available_commands = Rc::new(RefCell::new(vec![]));
@@ -1893,8 +1892,8 @@ mod tests {
 
         let mut cx = VisualTestContext::from_window(*window, cx);
 
-        let context_store = cx.new(|cx| ContextStore::fake(project.clone(), cx));
-        let history_store = cx.new(|cx| HistoryStore::new(context_store, 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 {
@@ -2133,8 +2132,8 @@ mod tests {
             opened_editors.push(buffer);
         }
 
-        let context_store = cx.new(|cx| ContextStore::fake(project.clone(), cx));
-        let history_store = cx.new(|cx| HistoryStore::new(context_store, 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 (message_editor, editor) = workspace.update_in(&mut cx, |workspace, window, cx| {
@@ -2660,8 +2659,8 @@ mod tests {
         let (workspace, cx) =
             cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
 
-        let context_store = cx.new(|cx| ContextStore::fake(project.clone(), cx));
-        let history_store = cx.new(|cx| HistoryStore::new(context_store, 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| {

crates/agent_ui/src/acp/mode_selector.rs 🔗

@@ -194,7 +194,7 @@ impl Render for ModeSelector {
                 trigger_button,
                 Tooltip::element({
                     let focus_handle = self.focus_handle.clone();
-                    move |window, cx| {
+                    move |_window, cx| {
                         v_flex()
                             .gap_1()
                             .child(
@@ -205,10 +205,9 @@ impl Render for ModeSelector {
                                     .border_b_1()
                                     .border_color(cx.theme().colors().border_variant)
                                     .child(Label::new("Cycle Through Modes"))
-                                    .children(KeyBinding::for_action_in(
+                                    .child(KeyBinding::for_action_in(
                                         &CycleModeSelector,
                                         &focus_handle,
-                                        window,
                                         cx,
                                     )),
                             )
@@ -217,10 +216,9 @@ impl Render for ModeSelector {
                                     .gap_2()
                                     .justify_between()
                                     .child(Label::new("Toggle Mode Menu"))
-                                    .children(KeyBinding::for_action_in(
+                                    .child(KeyBinding::for_action_in(
                                         &ToggleProfileSelector,
                                         &focus_handle,
-                                        window,
                                         cx,
                                     )),
                             )

crates/agent_ui/src/acp/model_selector_popover.rs 🔗

@@ -77,14 +77,8 @@ impl Render for AcpModelSelectorPopover {
                         .ml_0p5(),
                 )
                 .child(Icon::new(icon).color(Color::Muted).size(IconSize::XSmall)),
-            move |window, cx| {
-                Tooltip::for_action_in(
-                    "Change Model",
-                    &ToggleModelSelector,
-                    &focus_handle,
-                    window,
-                    cx,
-                )
+            move |_window, cx| {
+                Tooltip::for_action_in("Change Model", &ToggleModelSelector, &focus_handle, cx)
             },
             gpui::Corner::BottomRight,
             cx,

crates/agent_ui/src/acp/thread_history.rs 🔗

@@ -1,6 +1,6 @@
 use crate::acp::AcpThreadView;
 use crate::{AgentPanel, RemoveSelectedThread};
-use agent2::{HistoryEntry, HistoryStore};
+use agent::{HistoryEntry, HistoryStore};
 use chrono::{Datelike as _, Local, NaiveDate, TimeDelta};
 use editor::{Editor, EditorEvent};
 use fuzzy::StringMatchCandidate;
@@ -23,11 +23,8 @@ pub struct AcpThreadHistory {
     hovered_index: Option<usize>,
     search_editor: Entity<Editor>,
     search_query: SharedString,
-
     visible_items: Vec<ListItemType>,
-
     local_timezone: UtcOffset,
-
     _update_task: Task<()>,
     _subscriptions: Vec<gpui::Subscription>,
 }
@@ -62,7 +59,7 @@ impl EventEmitter<ThreadHistoryEvent> for AcpThreadHistory {}
 
 impl AcpThreadHistory {
     pub(crate) fn new(
-        history_store: Entity<agent2::HistoryStore>,
+        history_store: Entity<agent::HistoryStore>,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) -> Self {
@@ -327,8 +324,8 @@ impl AcpThreadHistory {
             HistoryEntry::AcpThread(thread) => self
                 .history_store
                 .update(cx, |this, cx| this.delete_thread(thread.id.clone(), cx)),
-            HistoryEntry::TextThread(context) => self.history_store.update(cx, |this, cx| {
-                this.delete_text_thread(context.path.clone(), cx)
+            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);
@@ -426,8 +423,8 @@ impl AcpThreadHistory {
                                 .shape(IconButtonShape::Square)
                                 .icon_size(IconSize::XSmall)
                                 .icon_color(Color::Muted)
-                                .tooltip(move |window, cx| {
-                                    Tooltip::for_action("Delete", &RemoveSelectedThread, window, cx)
+                                .tooltip(move |_window, cx| {
+                                    Tooltip::for_action("Delete", &RemoveSelectedThread, cx)
                                 })
                                 .on_click(
                                     cx.listener(move |this, _, _, cx| this.remove_thread(ix, cx)),
@@ -598,8 +595,8 @@ impl RenderOnce for AcpHistoryEntryElement {
                         .shape(IconButtonShape::Square)
                         .icon_size(IconSize::XSmall)
                         .icon_color(Color::Muted)
-                        .tooltip(move |window, cx| {
-                            Tooltip::for_action("Delete", &RemoveSelectedThread, window, cx)
+                        .tooltip(move |_window, cx| {
+                            Tooltip::for_action("Delete", &RemoveSelectedThread, cx)
                         })
                         .on_click({
                             let thread_view = self.thread_view.clone();
@@ -638,12 +635,12 @@ impl RenderOnce for AcpHistoryEntryElement {
                                     });
                                 }
                             }
-                            HistoryEntry::TextThread(context) => {
+                            HistoryEntry::TextThread(text_thread) => {
                                 if let Some(panel) = workspace.read(cx).panel::<AgentPanel>(cx) {
                                     panel.update(cx, |panel, cx| {
                                         panel
-                                            .open_saved_prompt_editor(
-                                                context.path.clone(),
+                                            .open_saved_text_thread(
+                                                text_thread.path.clone(),
                                                 window,
                                                 cx,
                                             )

crates/agent_ui/src/acp/thread_view.rs 🔗

@@ -5,10 +5,10 @@ use acp_thread::{
 };
 use acp_thread::{AgentConnection, Plan};
 use action_log::ActionLog;
+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 agent2::{DbThreadMetadata, HistoryEntry, HistoryEntryId, HistoryStore, NativeAgentServer};
 use anyhow::{Result, anyhow, bail};
 use arrayvec::ArrayVec;
 use audio::{Audio, Sound};
@@ -117,7 +117,7 @@ impl ThreadError {
     }
 }
 
-impl ProfileProvider for Entity<agent2::Thread> {
+impl ProfileProvider for Entity<agent::Thread> {
     fn profile_id(&self, cx: &App) -> AgentProfileId {
         self.read(cx).profile().clone()
     }
@@ -529,7 +529,7 @@ impl AcpThreadView {
 
             let result = if let Some(native_agent) = connection
                 .clone()
-                .downcast::<agent2::NativeAgentConnection>()
+                .downcast::<agent::NativeAgentConnection>()
                 && let Some(resume) = resume_thread.clone()
             {
                 cx.update(|_, cx| {
@@ -1259,6 +1259,7 @@ impl AcpThreadView {
                 .await?;
             this.update_in(cx, |this, window, cx| {
                 this.send_impl(message_editor, window, cx);
+                this.focus_handle(cx).focus(window);
             })?;
             anyhow::Ok(())
         })
@@ -2157,7 +2158,6 @@ impl AcpThreadView {
                             options,
                             entry_ix,
                             tool_call.id.clone(),
-                            window,
                             cx,
                         ))
                         .into_any(),
@@ -2558,7 +2558,6 @@ impl AcpThreadView {
         options: &[acp::PermissionOption],
         entry_ix: usize,
         tool_call_id: acp::ToolCallId,
-        window: &Window,
         cx: &Context<Self>,
     ) -> Div {
         let is_first = self.thread().is_some_and(|thread| {
@@ -2615,7 +2614,7 @@ impl AcpThreadView {
                         seen_kinds.push(option.kind);
 
                         this.key_binding(
-                            KeyBinding::for_action_in(action, &self.focus_handle, window, cx)
+                            KeyBinding::for_action_in(action, &self.focus_handle, cx)
                                 .map(|kb| kb.size(rems_from_px(10.))),
                         )
                     })
@@ -2796,12 +2795,11 @@ impl AcpThreadView {
                         .icon_size(IconSize::Small)
                         .icon_color(Color::Error)
                         .label_size(LabelSize::Small)
-                        .tooltip(move |window, cx| {
+                        .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.",
-                                window,
                                 cx,
                             )
                         })
@@ -3102,11 +3100,11 @@ impl AcpThreadView {
         )
     }
 
-    fn render_recent_history(&self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
+    fn render_recent_history(&self, cx: &mut Context<Self>) -> AnyElement {
         let render_history = self
             .agent
             .clone()
-            .downcast::<agent2::NativeAgentServer>()
+            .downcast::<agent::NativeAgentServer>()
             .is_some()
             && self
                 .history_store
@@ -3131,7 +3129,6 @@ impl AcpThreadView {
                                             KeyBinding::for_action_in(
                                                 &OpenHistory,
                                                 &self.focus_handle(cx),
-                                                window,
                                                 cx,
                                             )
                                             .map(|kb| kb.size(rems_from_px(12.))),
@@ -3459,7 +3456,6 @@ impl AcpThreadView {
                     &changed_buffers,
                     self.edits_expanded,
                     pending_edits,
-                    window,
                     cx,
                 ))
                 .when(self.edits_expanded, |parent| {
@@ -3619,7 +3615,6 @@ impl AcpThreadView {
         changed_buffers: &BTreeMap<Entity<Buffer>, Entity<BufferDiff>>,
         expanded: bool,
         pending_edits: bool,
-        window: &mut Window,
         cx: &Context<Self>,
     ) -> Div {
         const EDIT_NOT_READY_TOOLTIP_LABEL: &str = "Wait until file edits are complete.";
@@ -3695,12 +3690,11 @@ impl AcpThreadView {
                             .icon_size(IconSize::Small)
                             .tooltip({
                                 let focus_handle = focus_handle.clone();
-                                move |window, cx| {
+                                move |_window, cx| {
                                     Tooltip::for_action_in(
                                         "Review Changes",
                                         &OpenAgentDiff,
                                         &focus_handle,
-                                        window,
                                         cx,
                                     )
                                 }
@@ -3718,13 +3712,8 @@ impl AcpThreadView {
                                 this.tooltip(Tooltip::text(EDIT_NOT_READY_TOOLTIP_LABEL))
                             })
                             .key_binding(
-                                KeyBinding::for_action_in(
-                                    &RejectAll,
-                                    &focus_handle.clone(),
-                                    window,
-                                    cx,
-                                )
-                                .map(|kb| kb.size(rems_from_px(10.))),
+                                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);
@@ -3738,7 +3727,7 @@ impl AcpThreadView {
                                 this.tooltip(Tooltip::text(EDIT_NOT_READY_TOOLTIP_LABEL))
                             })
                             .key_binding(
-                                KeyBinding::for_action_in(&KeepAll, &focus_handle, window, cx)
+                                KeyBinding::for_action_in(&KeepAll, &focus_handle, cx)
                                     .map(|kb| kb.size(rems_from_px(10.))),
                             )
                             .on_click(cx.listener(move |this, _, window, cx| {
@@ -3968,12 +3957,11 @@ impl AcpThreadView {
                                     .icon_size(IconSize::Small)
                                     .icon_color(Color::Muted)
                                     .tooltip({
-                                        move |window, cx| {
+                                        move |_window, cx| {
                                             Tooltip::for_action_in(
                                                 expand_tooltip,
                                                 &ExpandMessageEditor,
                                                 &focus_handle,
-                                                window,
                                                 cx,
                                             )
                                         }
@@ -4011,12 +3999,12 @@ impl AcpThreadView {
     pub(crate) fn as_native_connection(
         &self,
         cx: &App,
-    ) -> Option<Rc<agent2::NativeAgentConnection>> {
+    ) -> Option<Rc<agent::NativeAgentConnection>> {
         let acp_thread = self.thread()?.read(cx);
         acp_thread.connection().clone().downcast()
     }
 
-    pub(crate) fn as_native_thread(&self, cx: &App) -> Option<Entity<agent2::Thread>> {
+    pub(crate) fn as_native_thread(&self, cx: &App) -> Option<Entity<agent::Thread>> {
         let acp_thread = self.thread()?.read(cx);
         self.as_native_connection(cx)?
             .thread(acp_thread.session_id(), cx)
@@ -4198,8 +4186,8 @@ impl AcpThreadView {
             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, window, cx)
+                .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()
@@ -4221,7 +4209,7 @@ impl AcpThreadView {
                         this.icon_color(Color::Accent)
                     }
                 })
-                .tooltip(move |window, cx| Tooltip::for_action(send_btn_tooltip, &Chat, window, cx))
+                .tooltip(move |_window, cx| Tooltip::for_action(send_btn_tooltip, &Chat, cx))
                 .on_click(cx.listener(|this, _, window, cx| {
                     this.send(window, cx);
                 }))
@@ -4282,15 +4270,14 @@ impl AcpThreadView {
             .icon_color(Color::Muted)
             .toggle_state(following)
             .selected_icon_color(Some(Color::Custom(cx.theme().players().agent().cursor)))
-            .tooltip(move |window, cx| {
+            .tooltip(move |_window, cx| {
                 if following {
-                    Tooltip::for_action(tooltip_label.clone(), &Follow, window, cx)
+                    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.",
-                        window,
                         cx,
                     )
                 }
@@ -4404,7 +4391,7 @@ impl AcpThreadView {
                     if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
                         panel.update(cx, |panel, cx| {
                             panel
-                                .open_saved_prompt_editor(path.as_path().into(), window, cx)
+                                .open_saved_text_thread(path.as_path().into(), window, cx)
                                 .detach_and_log_err(cx);
                         });
                     }
@@ -5079,7 +5066,7 @@ impl AcpThreadView {
         }
     }
 
-    fn render_thread_error(&self, window: &mut Window, cx: &mut Context<Self>) -> Option<Div> {
+    fn render_thread_error(&self, cx: &mut Context<Self>) -> Option<Div> {
         let content = match self.thread_error.as_ref()? {
             ThreadError::Other(error) => self.render_any_thread_error(error.clone(), cx),
             ThreadError::Refusal => self.render_refusal_error(cx),
@@ -5090,9 +5077,7 @@ impl AcpThreadView {
             ThreadError::ModelRequestLimitReached(plan) => {
                 self.render_model_request_limit_reached_error(*plan, cx)
             }
-            ThreadError::ToolUseLimitReached => {
-                self.render_tool_use_limit_reached_error(window, cx)?
-            }
+            ThreadError::ToolUseLimitReached => self.render_tool_use_limit_reached_error(cx)?,
         };
 
         Some(div().child(content))
@@ -5137,7 +5122,7 @@ impl AcpThreadView {
         if self
             .agent
             .clone()
-            .downcast::<agent2::NativeAgentServer>()
+            .downcast::<agent::NativeAgentServer>()
             .is_some()
         {
             // Native agent - use the model name
@@ -5283,11 +5268,7 @@ impl AcpThreadView {
             .dismiss_action(self.dismiss_error_button(cx))
     }
 
-    fn render_tool_use_limit_reached_error(
-        &self,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) -> Option<Callout> {
+    fn render_tool_use_limit_reached_error(&self, cx: &mut Context<Self>) -> Option<Callout> {
         let thread = self.as_native_thread(cx)?;
         let supports_burn_mode = thread
             .read(cx)
@@ -5314,7 +5295,6 @@ impl AcpThreadView {
                                         KeyBinding::for_action_in(
                                             &ContinueWithBurnMode,
                                             &focus_handle,
-                                            window,
                                             cx,
                                         )
                                         .map(|kb| kb.size(rems_from_px(10.))),
@@ -5338,13 +5318,8 @@ impl AcpThreadView {
                                 .layer(ElevationIndex::ModalSurface)
                                 .label_size(LabelSize::Small)
                                 .key_binding(
-                                    KeyBinding::for_action_in(
-                                        &ContinueThread,
-                                        &focus_handle,
-                                        window,
-                                        cx,
-                                    )
-                                    .map(|kb| kb.size(rems_from_px(10.))),
+                                    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);
@@ -5439,9 +5414,11 @@ impl AcpThreadView {
             HistoryEntry::AcpThread(thread) => self.history_store.update(cx, |history, cx| {
                 history.delete_thread(thread.id.clone(), cx)
             }),
-            HistoryEntry::TextThread(context) => self.history_store.update(cx, |history, cx| {
-                history.delete_text_thread(context.path.clone(), cx)
-            }),
+            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);
     }
@@ -5520,7 +5497,7 @@ impl Render for AcpThreadView {
                     .into_any(),
                 ThreadState::Loading { .. } => v_flex()
                     .flex_1()
-                    .child(self.render_recent_history(window, cx))
+                    .child(self.render_recent_history(cx))
                     .into_any(),
                 ThreadState::LoadError(e) => v_flex()
                     .flex_1()
@@ -5551,8 +5528,7 @@ impl Render for AcpThreadView {
                         .vertical_scrollbar_for(self.list_state.clone(), window, cx)
                         .into_any()
                     } else {
-                        this.child(self.render_recent_history(window, cx))
-                            .into_any()
+                        this.child(self.render_recent_history(cx)).into_any()
                     }
                 }),
             })
@@ -5576,7 +5552,7 @@ impl Render for AcpThreadView {
                     Vec::<Empty>::new()
                 }
             })
-            .children(self.render_thread_error(window, cx))
+            .children(self.render_thread_error(cx))
             .when_some(
                 self.new_server_version_available.as_ref().filter(|_| {
                     !has_messages || !matches!(self.thread_state, ThreadState::Ready { .. })
@@ -5761,7 +5737,7 @@ fn terminal_command_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
 pub(crate) mod tests {
     use acp_thread::StubAgentConnection;
     use agent_client_protocol::SessionId;
-    use assistant_context::ContextStore;
+    use assistant_text_thread::TextThreadStore;
     use editor::EditorSettings;
     use fs::FakeFs;
     use gpui::{EventEmitter, SemanticVersion, TestAppContext, VisualTestContext};
@@ -5924,10 +5900,10 @@ pub(crate) mod tests {
         let (workspace, cx) =
             cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
 
-        let context_store =
-            cx.update(|_window, cx| cx.new(|cx| ContextStore::fake(project.clone(), 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(context_store, cx)));
+            cx.update(|_window, cx| cx.new(|cx| HistoryStore::new(text_thread_store, cx)));
 
         let thread_view = cx.update(|window, cx| {
             cx.new(|cx| {
@@ -6196,10 +6172,10 @@ pub(crate) mod tests {
         let (workspace, cx) =
             cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
 
-        let context_store =
-            cx.update(|_window, cx| cx.new(|cx| ContextStore::fake(project.clone(), 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(context_store, cx)));
+            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| {

crates/agent_ui/src/agent_configuration.rs 🔗

@@ -6,8 +6,8 @@ mod tool_picker;
 
 use std::{ops::Range, sync::Arc};
 
+use agent::ContextServerRegistry;
 use anyhow::Result;
-use assistant_tool::{ToolSource, ToolWorkingSet};
 use cloud_llm_client::{Plan, PlanV1, PlanV2};
 use collections::HashMap;
 use context_server::ContextServerId;
@@ -17,7 +17,7 @@ use extension_host::ExtensionStore;
 use fs::Fs;
 use gpui::{
     Action, AnyView, App, AsyncWindowContext, Corner, Entity, EventEmitter, FocusHandle, Focusable,
-    Hsla, ScrollHandle, Subscription, Task, WeakEntity,
+    ScrollHandle, Subscription, Task, WeakEntity,
 };
 use language::LanguageRegistry;
 use language_model::{
@@ -54,9 +54,8 @@ pub struct AgentConfiguration {
     focus_handle: FocusHandle,
     configuration_views_by_provider: HashMap<LanguageModelProviderId, AnyView>,
     context_server_store: Entity<ContextServerStore>,
-    expanded_context_server_tools: HashMap<ContextServerId, bool>,
     expanded_provider_configurations: HashMap<LanguageModelProviderId, bool>,
-    tools: Entity<ToolWorkingSet>,
+    context_server_registry: Entity<ContextServerRegistry>,
     _registry_subscription: Subscription,
     scroll_handle: ScrollHandle,
     _check_for_gemini: Task<()>,
@@ -67,7 +66,7 @@ impl AgentConfiguration {
         fs: Arc<dyn Fs>,
         agent_server_store: Entity<AgentServerStore>,
         context_server_store: Entity<ContextServerStore>,
-        tools: Entity<ToolWorkingSet>,
+        context_server_registry: Entity<ContextServerRegistry>,
         language_registry: Arc<LanguageRegistry>,
         workspace: WeakEntity<Workspace>,
         window: &mut Window,
@@ -103,9 +102,8 @@ impl AgentConfiguration {
             configuration_views_by_provider: HashMap::default(),
             agent_server_store,
             context_server_store,
-            expanded_context_server_tools: HashMap::default(),
             expanded_provider_configurations: HashMap::default(),
-            tools,
+            context_server_registry,
             _registry_subscription: registry_subscription,
             scroll_handle: ScrollHandle::new(),
             _check_for_gemini: Task::ready(()),
@@ -438,10 +436,6 @@ impl AgentConfiguration {
         }
     }
 
-    fn card_item_border_color(&self, cx: &mut Context<Self>) -> Hsla {
-        cx.theme().colors().border.opacity(0.6)
-    }
-
     fn render_context_servers_section(
         &mut self,
         window: &mut Window,
@@ -567,7 +561,6 @@ impl AgentConfiguration {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) -> impl use<> + IntoElement {
-        let tools_by_source = self.tools.read(cx).tools_by_source(cx);
         let server_status = self
             .context_server_store
             .read(cx)
@@ -596,17 +589,11 @@ impl AgentConfiguration {
             None
         };
 
-        let are_tools_expanded = self
-            .expanded_context_server_tools
-            .get(&context_server_id)
-            .copied()
-            .unwrap_or_default();
-        let tools = tools_by_source
-            .get(&ToolSource::ContextServer {
-                id: context_server_id.0.clone().into(),
-            })
-            .map_or([].as_slice(), |tools| tools.as_slice());
-        let tool_count = tools.len();
+        let tool_count = self
+            .context_server_registry
+            .read(cx)
+            .tools_for_server(&context_server_id)
+            .count();
 
         let (source_icon, source_tooltip) = if is_from_extension {
             (
@@ -660,7 +647,7 @@ impl AgentConfiguration {
                 let language_registry = self.language_registry.clone();
                 let context_server_store = self.context_server_store.clone();
                 let workspace = self.workspace.clone();
-                let tools = self.tools.clone();
+                let context_server_registry = self.context_server_registry.clone();
 
                 move |window, cx| {
                     Some(ContextMenu::build(window, cx, |menu, _window, _cx| {
@@ -678,20 +665,16 @@ impl AgentConfiguration {
                                 )
                                 .detach_and_log_err(cx);
                             }
-                        }).when(tool_count >= 1, |this| this.entry("View Tools", None, {
+                        }).when(tool_count > 0, |this| this.entry("View Tools", None, {
                             let context_server_id = context_server_id.clone();
-                            let tools = tools.clone();
+                            let context_server_registry = context_server_registry.clone();
                             let workspace = workspace.clone();
-
                             move |window, cx| {
                                 let context_server_id = context_server_id.clone();
-                                let tools = tools.clone();
-                                let workspace = workspace.clone();
-
                                 workspace.update(cx, |workspace, cx| {
                                     ConfigureContextServerToolsModal::toggle(
                                         context_server_id,
-                                        tools,
+                                        context_server_registry.clone(),
                                         workspace,
                                         window,
                                         cx,
@@ -773,14 +756,6 @@ impl AgentConfiguration {
             .child(
                 h_flex()
                     .justify_between()
-                    .when(
-                        error.is_none() && are_tools_expanded && tool_count >= 1,
-                        |element| {
-                            element
-                                .border_b_1()
-                                .border_color(self.card_item_border_color(cx))
-                        },
-                    )
                     .child(
                         h_flex()
                             .flex_1()
@@ -904,11 +879,6 @@ impl AgentConfiguration {
                             ),
                     );
                 }
-
-                if !are_tools_expanded || tools.is_empty() {
-                    return parent;
-                }
-
                 parent
             })
     }

crates/agent_ui/src/agent_configuration/add_llm_provider_modal.rs 🔗

@@ -10,7 +10,7 @@ use settings::{OpenAiCompatibleSettingsContent, update_settings_file};
 use ui::{
     Banner, Checkbox, KeyBinding, Modal, ModalFooter, ModalHeader, Section, ToggleState, prelude::*,
 };
-use ui_input::SingleLineInput;
+use ui_input::InputField;
 use workspace::{ModalView, Workspace};
 
 #[derive(Clone, Copy)]
@@ -33,9 +33,9 @@ impl LlmCompatibleProvider {
 }
 
 struct AddLlmProviderInput {
-    provider_name: Entity<SingleLineInput>,
-    api_url: Entity<SingleLineInput>,
-    api_key: Entity<SingleLineInput>,
+    provider_name: Entity<InputField>,
+    api_url: Entity<InputField>,
+    api_key: Entity<InputField>,
     models: Vec<ModelInput>,
 }
 
@@ -76,10 +76,10 @@ struct ModelCapabilityToggles {
 }
 
 struct ModelInput {
-    name: Entity<SingleLineInput>,
-    max_completion_tokens: Entity<SingleLineInput>,
-    max_output_tokens: Entity<SingleLineInput>,
-    max_tokens: Entity<SingleLineInput>,
+    name: Entity<InputField>,
+    max_completion_tokens: Entity<InputField>,
+    max_output_tokens: Entity<InputField>,
+    max_tokens: Entity<InputField>,
     capabilities: ModelCapabilityToggles,
 }
 
@@ -171,9 +171,9 @@ fn single_line_input(
     text: Option<&str>,
     window: &mut Window,
     cx: &mut App,
-) -> Entity<SingleLineInput> {
+) -> Entity<InputField> {
     cx.new(|cx| {
-        let input = SingleLineInput::new(window, cx, placeholder).label(label);
+        let input = InputField::new(window, cx, placeholder).label(label);
         if let Some(text) = text {
             input
                 .editor()
@@ -431,7 +431,7 @@ impl Focusable for AddLlmProviderModal {
 impl ModalView for AddLlmProviderModal {}
 
 impl Render for AddLlmProviderModal {
-    fn render(&mut self, window: &mut ui::Window, cx: &mut ui::Context<Self>) -> impl IntoElement {
+    fn render(&mut self, _window: &mut ui::Window, cx: &mut ui::Context<Self>) -> impl IntoElement {
         let focus_handle = self.focus_handle(cx);
 
         div()
@@ -484,7 +484,6 @@ impl Render for AddLlmProviderModal {
                                             KeyBinding::for_action_in(
                                                 &menu::Cancel,
                                                 &focus_handle,
-                                                window,
                                                 cx,
                                             )
                                             .map(|kb| kb.size(rems_from_px(12.))),
@@ -499,7 +498,6 @@ impl Render for AddLlmProviderModal {
                                             KeyBinding::for_action_in(
                                                 &menu::Confirm,
                                                 &focus_handle,
-                                                window,
                                                 cx,
                                             )
                                             .map(|kb| kb.size(rems_from_px(12.))),
@@ -757,12 +755,7 @@ mod tests {
         models: Vec<(&str, &str, &str, &str)>,
         cx: &mut VisualTestContext,
     ) -> Option<SharedString> {
-        fn set_text(
-            input: &Entity<SingleLineInput>,
-            text: &str,
-            window: &mut Window,
-            cx: &mut App,
-        ) {
+        fn set_text(input: &Entity<InputField>, 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);

crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs 🔗

@@ -566,7 +566,7 @@ impl ConfigureContextServerModal {
             .into_any_element()
     }
 
-    fn render_modal_footer(&self, window: &mut Window, cx: &mut Context<Self>) -> ModalFooter {
+    fn render_modal_footer(&self, cx: &mut Context<Self>) -> ModalFooter {
         let focus_handle = self.focus_handle(cx);
         let is_connecting = matches!(self.state, State::Waiting);
 
@@ -584,12 +584,11 @@ impl ConfigureContextServerModal {
                             .icon_size(IconSize::Small)
                             .tooltip({
                                 let repository_url = repository_url.clone();
-                                move |window, cx| {
+                                move |_window, cx| {
                                     Tooltip::with_meta(
                                         "Open Repository",
                                         None,
                                         repository_url.clone(),
-                                        window,
                                         cx,
                                     )
                                 }
@@ -616,7 +615,7 @@ impl ConfigureContextServerModal {
                             },
                         )
                         .key_binding(
-                            KeyBinding::for_action_in(&menu::Cancel, &focus_handle, window, cx)
+                            KeyBinding::for_action_in(&menu::Cancel, &focus_handle, cx)
                                 .map(|kb| kb.size(rems_from_px(12.))),
                         )
                         .on_click(
@@ -634,7 +633,7 @@ impl ConfigureContextServerModal {
                         )
                         .disabled(is_connecting)
                         .key_binding(
-                            KeyBinding::for_action_in(&menu::Confirm, &focus_handle, window, cx)
+                            KeyBinding::for_action_in(&menu::Confirm, &focus_handle, cx)
                                 .map(|kb| kb.size(rems_from_px(12.))),
                         )
                         .on_click(
@@ -709,7 +708,7 @@ impl Render for ConfigureContextServerModal {
                                 State::Error(error) => Self::render_modal_error(error.clone()),
                             }),
                     )
-                    .footer(self.render_modal_footer(window, cx)),
+                    .footer(self.render_modal_footer(cx)),
             )
     }
 }

crates/agent_ui/src/agent_configuration/configure_context_server_tools_modal.rs 🔗

@@ -1,4 +1,5 @@
-use assistant_tool::{ToolSource, ToolWorkingSet};
+use agent::ContextServerRegistry;
+use collections::HashMap;
 use context_server::ContextServerId;
 use gpui::{
     DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, ScrollHandle, Window, prelude::*,
@@ -8,37 +9,37 @@ use workspace::{ModalView, Workspace};
 
 pub struct ConfigureContextServerToolsModal {
     context_server_id: ContextServerId,
-    tools: Entity<ToolWorkingSet>,
+    context_server_registry: Entity<ContextServerRegistry>,
     focus_handle: FocusHandle,
-    expanded_tools: std::collections::HashMap<String, bool>,
+    expanded_tools: HashMap<SharedString, bool>,
     scroll_handle: ScrollHandle,
 }
 
 impl ConfigureContextServerToolsModal {
     fn new(
         context_server_id: ContextServerId,
-        tools: Entity<ToolWorkingSet>,
+        context_server_registry: Entity<ContextServerRegistry>,
         _window: &mut Window,
         cx: &mut Context<Self>,
     ) -> Self {
         Self {
             context_server_id,
-            tools,
+            context_server_registry,
             focus_handle: cx.focus_handle(),
-            expanded_tools: std::collections::HashMap::new(),
+            expanded_tools: HashMap::default(),
             scroll_handle: ScrollHandle::new(),
         }
     }
 
     pub fn toggle(
         context_server_id: ContextServerId,
-        tools: Entity<ToolWorkingSet>,
+        context_server_registry: Entity<ContextServerRegistry>,
         workspace: &mut Workspace,
         window: &mut Window,
         cx: &mut Context<Workspace>,
     ) {
         workspace.toggle_modal(window, cx, |window, cx| {
-            Self::new(context_server_id, tools, window, cx)
+            Self::new(context_server_id, context_server_registry, window, cx)
         });
     }
 
@@ -51,13 +52,11 @@ impl ConfigureContextServerToolsModal {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) -> impl IntoElement {
-        let tools_by_source = self.tools.read(cx).tools_by_source(cx);
-        let server_tools = tools_by_source
-            .get(&ToolSource::ContextServer {
-                id: self.context_server_id.0.clone().into(),
-            })
-            .map(|tools| tools.as_slice())
-            .unwrap_or(&[]);
+        let tools = self
+            .context_server_registry
+            .read(cx)
+            .tools_for_server(&self.context_server_id)
+            .collect::<Vec<_>>();
 
         div()
             .size_full()
@@ -70,11 +69,11 @@ impl ConfigureContextServerToolsModal {
                     .max_h_128()
                     .overflow_y_scroll()
                     .track_scroll(&self.scroll_handle)
-                    .children(server_tools.iter().enumerate().flat_map(|(index, tool)| {
+                    .children(tools.iter().enumerate().flat_map(|(index, tool)| {
                         let tool_name = tool.name();
                         let is_expanded = self
                             .expanded_tools
-                            .get(&tool_name)
+                            .get(tool_name.as_ref())
                             .copied()
                             .unwrap_or(false);
 
@@ -110,7 +109,7 @@ impl ConfigureContextServerToolsModal {
                                             move |this, _event, _window, _cx| {
                                                 let current = this
                                                     .expanded_tools
-                                                    .get(&tool_name)
+                                                    .get(tool_name.as_ref())
                                                     .copied()
                                                     .unwrap_or(false);
                                                 this.expanded_tools
@@ -127,7 +126,7 @@ impl ConfigureContextServerToolsModal {
                                 .into_any_element(),
                         ];
 
-                        if index < server_tools.len() - 1 {
+                        if index < tools.len() - 1 {
                             items.push(
                                 h_flex()
                                     .w_full()

crates/agent_ui/src/agent_configuration/manage_profiles_modal.rs 🔗

@@ -2,11 +2,12 @@ mod profile_modal_header;
 
 use std::sync::Arc;
 
+use agent::ContextServerRegistry;
 use agent_settings::{AgentProfile, AgentProfileId, AgentSettings, builtin_profiles};
-use assistant_tool::ToolWorkingSet;
 use editor::Editor;
 use fs::Fs;
 use gpui::{DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Subscription, prelude::*};
+use language_model::LanguageModel;
 use settings::Settings as _;
 use ui::{
     KeyBinding, ListItem, ListItemSpacing, ListSeparator, Navigable, NavigableEntry, prelude::*,
@@ -17,8 +18,6 @@ use crate::agent_configuration::manage_profiles_modal::profile_modal_header::Pro
 use crate::agent_configuration::tool_picker::{ToolPicker, ToolPickerDelegate};
 use crate::{AgentPanel, ManageProfiles};
 
-use super::tool_picker::ToolPickerMode;
-
 enum Mode {
     ChooseProfile(ChooseProfileMode),
     NewProfile(NewProfileMode),
@@ -97,7 +96,8 @@ pub struct NewProfileMode {
 
 pub struct ManageProfilesModal {
     fs: Arc<dyn Fs>,
-    tools: Entity<ToolWorkingSet>,
+    context_server_registry: Entity<ContextServerRegistry>,
+    active_model: Option<Arc<dyn LanguageModel>>,
     focus_handle: FocusHandle,
     mode: Mode,
 }
@@ -111,10 +111,14 @@ impl ManageProfilesModal {
         workspace.register_action(|workspace, action: &ManageProfiles, window, cx| {
             if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
                 let fs = workspace.app_state().fs.clone();
-                let thread_store = panel.read(cx).thread_store();
-                let tools = thread_store.read(cx).tools();
+                let active_model = panel
+                    .read(cx)
+                    .active_native_agent_thread(cx)
+                    .and_then(|thread| thread.read(cx).model().cloned());
+
+                let context_server_registry = panel.read(cx).context_server_registry().clone();
                 workspace.toggle_modal(window, cx, |window, cx| {
-                    let mut this = Self::new(fs, tools, window, cx);
+                    let mut this = Self::new(fs, active_model, context_server_registry, window, cx);
 
                     if let Some(profile_id) = action.customize_tools.clone() {
                         this.configure_builtin_tools(profile_id, window, cx);
@@ -128,7 +132,8 @@ impl ManageProfilesModal {
 
     pub fn new(
         fs: Arc<dyn Fs>,
-        tools: Entity<ToolWorkingSet>,
+        active_model: Option<Arc<dyn LanguageModel>>,
+        context_server_registry: Entity<ContextServerRegistry>,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) -> Self {
@@ -136,7 +141,8 @@ impl ManageProfilesModal {
 
         Self {
             fs,
-            tools,
+            active_model,
+            context_server_registry,
             focus_handle,
             mode: Mode::choose_profile(window, cx),
         }
@@ -193,10 +199,9 @@ impl ManageProfilesModal {
         };
 
         let tool_picker = cx.new(|cx| {
-            let delegate = ToolPickerDelegate::new(
-                ToolPickerMode::McpTools,
+            let delegate = ToolPickerDelegate::mcp_tools(
+                &self.context_server_registry,
                 self.fs.clone(),
-                self.tools.clone(),
                 profile_id.clone(),
                 profile,
                 cx,
@@ -230,10 +235,14 @@ impl ManageProfilesModal {
         };
 
         let tool_picker = cx.new(|cx| {
-            let delegate = ToolPickerDelegate::new(
-                ToolPickerMode::BuiltinTools,
+            let delegate = ToolPickerDelegate::builtin_tools(
+                //todo: This causes the web search tool to show up even it only works when using zed hosted models
+                agent::supported_built_in_tool_names(
+                    self.active_model.as_ref().map(|model| model.provider_id()),
+                )
+                .map(|s| s.into())
+                .collect::<Vec<_>>(),
                 self.fs.clone(),
-                self.tools.clone(),
                 profile_id.clone(),
                 profile,
                 cx,
@@ -343,10 +352,9 @@ impl ManageProfilesModal {
                                         .size(LabelSize::Small)
                                         .color(Color::Muted),
                                 )
-                                .children(KeyBinding::for_action_in(
+                                .child(KeyBinding::for_action_in(
                                     &menu::Confirm,
                                     &self.focus_handle,
-                                    window,
                                     cx,
                                 )),
                         )
@@ -640,14 +648,13 @@ impl ManageProfilesModal {
                                         )
                                         .child(Label::new("Go Back"))
                                         .end_slot(
-                                            div().children(
+                                            div().child(
                                                 KeyBinding::for_action_in(
                                                     &menu::Cancel,
                                                     &self.focus_handle,
-                                                    window,
                                                     cx,
                                                 )
-                                                .map(|kb| kb.size(rems_from_px(12.))),
+                                                .size(rems_from_px(12.)),
                                             ),
                                         )
                                         .on_click({
@@ -691,14 +698,9 @@ impl Render for ManageProfilesModal {
                     )
                     .child(Label::new("Go Back"))
                     .end_slot(
-                        div().children(
-                            KeyBinding::for_action_in(
-                                &menu::Cancel,
-                                &self.focus_handle,
-                                window,
-                                cx,
-                            )
-                            .map(|kb| kb.size(rems_from_px(12.))),
+                        div().child(
+                            KeyBinding::for_action_in(&menu::Cancel, &self.focus_handle, cx)
+                                .size(rems_from_px(12.)),
                         ),
                     )
                     .on_click({

crates/agent_ui/src/agent_configuration/tool_picker.rs 🔗

@@ -1,7 +1,7 @@
 use std::{collections::BTreeMap, sync::Arc};
 
+use agent::ContextServerRegistry;
 use agent_settings::{AgentProfileId, AgentProfileSettings};
-use assistant_tool::{ToolSource, ToolWorkingSet};
 use fs::Fs;
 use gpui::{App, Context, DismissEvent, Entity, EventEmitter, Focusable, Task, WeakEntity, Window};
 use picker::{Picker, PickerDelegate};
@@ -14,7 +14,7 @@ pub struct ToolPicker {
 }
 
 #[derive(Clone, Copy, Debug, PartialEq)]
-pub enum ToolPickerMode {
+enum ToolPickerMode {
     BuiltinTools,
     McpTools,
 }
@@ -76,59 +76,79 @@ pub struct ToolPickerDelegate {
 }
 
 impl ToolPickerDelegate {
-    pub fn new(
-        mode: ToolPickerMode,
+    pub fn builtin_tools(
+        tool_names: Vec<Arc<str>>,
         fs: Arc<dyn Fs>,
-        tool_set: Entity<ToolWorkingSet>,
         profile_id: AgentProfileId,
         profile_settings: AgentProfileSettings,
         cx: &mut Context<ToolPicker>,
     ) -> Self {
-        let items = Arc::new(Self::resolve_items(mode, &tool_set, cx));
+        Self::new(
+            Arc::new(
+                tool_names
+                    .into_iter()
+                    .map(|name| PickerItem::Tool {
+                        name,
+                        server_id: None,
+                    })
+                    .collect(),
+            ),
+            ToolPickerMode::BuiltinTools,
+            fs,
+            profile_id,
+            profile_settings,
+            cx,
+        )
+    }
 
+    pub fn mcp_tools(
+        registry: &Entity<ContextServerRegistry>,
+        fs: Arc<dyn Fs>,
+        profile_id: AgentProfileId,
+        profile_settings: AgentProfileSettings,
+        cx: &mut Context<ToolPicker>,
+    ) -> Self {
+        let mut items = Vec::new();
+
+        for (id, tools) in registry.read(cx).servers() {
+            let server_id = id.clone().0;
+            items.push(PickerItem::ContextServer {
+                server_id: server_id.clone(),
+            });
+            items.extend(tools.keys().map(|tool_name| PickerItem::Tool {
+                name: tool_name.clone().into(),
+                server_id: Some(server_id.clone()),
+            }));
+        }
+
+        Self::new(
+            Arc::new(items),
+            ToolPickerMode::McpTools,
+            fs,
+            profile_id,
+            profile_settings,
+            cx,
+        )
+    }
+
+    fn new(
+        items: Arc<Vec<PickerItem>>,
+        mode: ToolPickerMode,
+        fs: Arc<dyn Fs>,
+        profile_id: AgentProfileId,
+        profile_settings: AgentProfileSettings,
+        cx: &mut Context<ToolPicker>,
+    ) -> Self {
         Self {
             tool_picker: cx.entity().downgrade(),
+            mode,
             fs,
             items,
             profile_id,
             profile_settings,
             filtered_items: Vec::new(),
             selected_index: 0,
-            mode,
-        }
-    }
-
-    fn resolve_items(
-        mode: ToolPickerMode,
-        tool_set: &Entity<ToolWorkingSet>,
-        cx: &mut App,
-    ) -> Vec<PickerItem> {
-        let mut items = Vec::new();
-        for (source, tools) in tool_set.read(cx).tools_by_source(cx) {
-            match source {
-                ToolSource::Native => {
-                    if mode == ToolPickerMode::BuiltinTools {
-                        items.extend(tools.into_iter().map(|tool| PickerItem::Tool {
-                            name: tool.name().into(),
-                            server_id: None,
-                        }));
-                    }
-                }
-                ToolSource::ContextServer { id } => {
-                    if mode == ToolPickerMode::McpTools && !tools.is_empty() {
-                        let server_id: Arc<str> = id.clone().into();
-                        items.push(PickerItem::ContextServer {
-                            server_id: server_id.clone(),
-                        });
-                        items.extend(tools.into_iter().map(|tool| PickerItem::Tool {
-                            name: tool.name().into(),
-                            server_id: Some(server_id.clone()),
-                        }));
-                    }
-                }
-            }
         }
-        items
     }
 }
 

crates/agent_ui/src/agent_diff.rs 🔗

@@ -452,7 +452,10 @@ fn update_editor_selection(
     window: &mut Window,
     cx: &mut Context<Editor>,
 ) {
-    let newest_cursor = editor.selections.newest::<Point>(cx).head();
+    let newest_cursor = editor
+        .selections
+        .newest::<Point>(&editor.display_snapshot(cx))
+        .head();
 
     if !diff_hunks.iter().any(|hunk| {
         hunk.row_range
@@ -578,11 +581,13 @@ impl Item for AgentDiffPane {
         _workspace_id: Option<workspace::WorkspaceId>,
         window: &mut Window,
         cx: &mut Context<Self>,
-    ) -> Option<Entity<Self>>
+    ) -> Task<Option<Entity<Self>>>
     where
         Self: Sized,
     {
-        Some(cx.new(|cx| Self::new(self.thread.clone(), self.workspace.clone(), window, cx)))
+        Task::ready(Some(cx.new(|cx| {
+            Self::new(self.thread.clone(), self.workspace.clone(), window, cx)
+        })))
     }
 
     fn is_dirty(&self, cx: &App) -> bool {
@@ -666,7 +671,7 @@ impl Item for AgentDiffPane {
 }
 
 impl Render for AgentDiffPane {
-    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
         let is_empty = self.multibuffer.read(cx).is_empty();
         let focus_handle = &self.focus_handle;
 
@@ -699,7 +704,6 @@ impl Render for AgentDiffPane {
                                 .key_binding(KeyBinding::for_action_in(
                                     &ToggleFocus,
                                     &focus_handle.clone(),
-                                    window,
                                     cx,
                                 ))
                                 .on_click(|_event, window, cx| {
@@ -716,14 +720,7 @@ fn diff_hunk_controls(thread: &AgentDiffThread) -> editor::RenderDiffHunkControl
     let thread = thread.clone();
 
     Arc::new(
-        move |row,
-              status: &DiffHunkStatus,
-              hunk_range,
-              is_created_file,
-              line_height,
-              editor: &Entity<Editor>,
-              window: &mut Window,
-              cx: &mut App| {
+        move |row, status, hunk_range, is_created_file, line_height, editor, _, cx| {
             {
                 render_diff_hunk_controls(
                     row,
@@ -733,7 +730,6 @@ fn diff_hunk_controls(thread: &AgentDiffThread) -> editor::RenderDiffHunkControl
                     line_height,
                     &thread,
                     editor,
-                    window,
                     cx,
                 )
             }
@@ -749,7 +745,6 @@ fn render_diff_hunk_controls(
     line_height: Pixels,
     thread: &AgentDiffThread,
     editor: &Entity<Editor>,
-    window: &mut Window,
     cx: &mut App,
 ) -> AnyElement {
     let editor = editor.clone();
@@ -772,13 +767,8 @@ fn render_diff_hunk_controls(
             Button::new(("reject", row as u64), "Reject")
                 .disabled(is_created_file)
                 .key_binding(
-                    KeyBinding::for_action_in(
-                        &Reject,
-                        &editor.read(cx).focus_handle(cx),
-                        window,
-                        cx,
-                    )
-                    .map(|kb| kb.size(rems_from_px(12.))),
+                    KeyBinding::for_action_in(&Reject, &editor.read(cx).focus_handle(cx), cx)
+                        .map(|kb| kb.size(rems_from_px(12.))),
                 )
                 .on_click({
                     let editor = editor.clone();
@@ -799,7 +789,7 @@ fn render_diff_hunk_controls(
                 }),
             Button::new(("keep", row as u64), "Keep")
                 .key_binding(
-                    KeyBinding::for_action_in(&Keep, &editor.read(cx).focus_handle(cx), window, cx)
+                    KeyBinding::for_action_in(&Keep, &editor.read(cx).focus_handle(cx), cx)
                         .map(|kb| kb.size(rems_from_px(12.))),
                 )
                 .on_click({
@@ -830,14 +820,8 @@ fn render_diff_hunk_controls(
                         // .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,
-                                )
+                            move |_window, cx| {
+                                Tooltip::for_action_in("Next Hunk", &GoToHunk, &focus_handle, cx)
                             }
                         })
                         .on_click({
@@ -866,12 +850,11 @@ fn render_diff_hunk_controls(
                         // .disabled(!has_multiple_hunks)
                         .tooltip({
                             let focus_handle = editor.focus_handle(cx);
-                            move |window, cx| {
+                            move |_window, cx| {
                                 Tooltip::for_action_in(
                                     "Previous Hunk",
                                     &GoToPreviousHunk,
                                     &focus_handle,
-                                    window,
                                     cx,
                                 )
                             }
@@ -1036,7 +1019,7 @@ impl ToolbarItemView for AgentDiffToolbar {
 }
 
 impl Render for AgentDiffToolbar {
-    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
         let spinner_icon = div()
             .px_0p5()
             .id("generating")
@@ -1111,7 +1094,6 @@ impl Render for AgentDiffToolbar {
                                         KeyBinding::for_action_in(
                                             &RejectAll,
                                             &editor_focus_handle,
-                                            window,
                                             cx,
                                         )
                                         .map(|kb| kb.size(rems_from_px(12.)))
@@ -1126,7 +1108,6 @@ impl Render for AgentDiffToolbar {
                                         KeyBinding::for_action_in(
                                             &KeepAll,
                                             &editor_focus_handle,
-                                            window,
                                             cx,
                                         )
                                         .map(|kb| kb.size(rems_from_px(12.)))
@@ -1203,13 +1184,8 @@ impl Render for AgentDiffToolbar {
                             .child(
                                 Button::new("reject-all", "Reject All")
                                     .key_binding({
-                                        KeyBinding::for_action_in(
-                                            &RejectAll,
-                                            &focus_handle,
-                                            window,
-                                            cx,
-                                        )
-                                        .map(|kb| kb.size(rems_from_px(12.)))
+                                        KeyBinding::for_action_in(&RejectAll, &focus_handle, cx)
+                                            .map(|kb| kb.size(rems_from_px(12.)))
                                     })
                                     .on_click(cx.listener(|this, _, window, cx| {
                                         this.dispatch_action(&RejectAll, window, cx)
@@ -1218,13 +1194,8 @@ impl Render for AgentDiffToolbar {
                             .child(
                                 Button::new("keep-all", "Keep All")
                                     .key_binding({
-                                        KeyBinding::for_action_in(
-                                            &KeepAll,
-                                            &focus_handle,
-                                            window,
-                                            cx,
-                                        )
-                                        .map(|kb| kb.size(rems_from_px(12.)))
+                                        KeyBinding::for_action_in(&KeepAll, &focus_handle, cx)
+                                            .map(|kb| kb.size(rems_from_px(12.)))
                                     })
                                     .on_click(cx.listener(|this, _, window, cx| {
                                         this.dispatch_action(&KeepAll, window, cx)
@@ -1895,7 +1866,9 @@ mod tests {
         );
         assert_eq!(
             editor
-                .update(cx, |editor, cx| editor.selections.newest::<Point>(cx))
+                .update(cx, |editor, cx| editor
+                    .selections
+                    .newest::<Point>(&editor.display_snapshot(cx)))
                 .range(),
             Point::new(1, 0)..Point::new(1, 0)
         );
@@ -1909,7 +1882,9 @@ mod tests {
         );
         assert_eq!(
             editor
-                .update(cx, |editor, cx| editor.selections.newest::<Point>(cx))
+                .update(cx, |editor, cx| editor
+                    .selections
+                    .newest::<Point>(&editor.display_snapshot(cx)))
                 .range(),
             Point::new(3, 0)..Point::new(3, 0)
         );
@@ -1930,7 +1905,9 @@ mod tests {
         );
         assert_eq!(
             editor
-                .update(cx, |editor, cx| editor.selections.newest::<Point>(cx))
+                .update(cx, |editor, cx| editor
+                    .selections
+                    .newest::<Point>(&editor.display_snapshot(cx)))
                 .range(),
             Point::new(3, 0)..Point::new(3, 0)
         );
@@ -1962,7 +1939,9 @@ mod tests {
         );
         assert_eq!(
             editor
-                .update(cx, |editor, cx| editor.selections.newest::<Point>(cx))
+                .update(cx, |editor, cx| editor
+                    .selections
+                    .newest::<Point>(&editor.display_snapshot(cx)))
                 .range(),
             Point::new(3, 0)..Point::new(3, 0)
         );
@@ -2119,7 +2098,9 @@ mod tests {
         );
         assert_eq!(
             editor1
-                .update(cx, |editor, cx| editor.selections.newest::<Point>(cx))
+                .update(cx, |editor, cx| editor
+                    .selections
+                    .newest::<Point>(&editor.display_snapshot(cx)))
                 .range(),
             Point::new(1, 0)..Point::new(1, 0)
         );
@@ -2160,7 +2141,9 @@ mod tests {
         );
         assert_eq!(
             editor1
-                .update(cx, |editor, cx| editor.selections.newest::<Point>(cx))
+                .update(cx, |editor, cx| editor
+                    .selections
+                    .newest::<Point>(&editor.display_snapshot(cx)))
                 .range(),
             Point::new(3, 0)..Point::new(3, 0)
         );
@@ -2181,7 +2164,9 @@ mod tests {
         );
         assert_eq!(
             editor1
-                .update(cx, |editor, cx| editor.selections.newest::<Point>(cx))
+                .update(cx, |editor, cx| editor
+                    .selections
+                    .newest::<Point>(&editor.display_snapshot(cx)))
                 .range(),
             Point::new(3, 0)..Point::new(3, 0)
         );
@@ -2207,7 +2192,9 @@ mod tests {
         );
         assert_eq!(
             editor1
-                .update(cx, |editor, cx| editor.selections.newest::<Point>(cx))
+                .update(cx, |editor, cx| editor
+                    .selections
+                    .newest::<Point>(&editor.display_snapshot(cx)))
                 .range(),
             Point::new(3, 0)..Point::new(3, 0)
         );
@@ -2240,7 +2227,9 @@ mod tests {
         );
         assert_eq!(
             editor2
-                .update(cx, |editor, cx| editor.selections.newest::<Point>(cx))
+                .update(cx, |editor, cx| editor
+                    .selections
+                    .newest::<Point>(&editor.display_snapshot(cx)))
                 .range(),
             Point::new(0, 0)..Point::new(0, 0)
         );

crates/agent_ui/src/agent_model_selector.rs 🔗

@@ -96,14 +96,8 @@ impl Render for AgentModelSelector {
                         .color(color)
                         .size(IconSize::XSmall),
                 ),
-            move |window, cx| {
-                Tooltip::for_action_in(
-                    "Change Model",
-                    &ToggleModelSelector,
-                    &focus_handle,
-                    window,
-                    cx,
-                )
+            move |_window, cx| {
+                Tooltip::for_action_in("Change Model", &ToggleModelSelector, &focus_handle, cx)
             },
             gpui::Corner::TopRight,
             cx,

crates/agent_ui/src/agent_panel.rs 🔗

@@ -4,7 +4,7 @@ use std::rc::Rc;
 use std::sync::Arc;
 
 use acp_thread::AcpThread;
-use agent2::{DbThreadMetadata, HistoryEntry};
+use agent::{ContextServerRegistry, DbThreadMetadata, HistoryEntry, HistoryStore};
 use db::kvp::{Dismissable, KEY_VALUE_STORE};
 use project::agent_server_store::{
     AgentServerCommand, AllAgentServersSettings, CLAUDE_CODE_NAME, CODEX_NAME, GEMINI_NAME,
@@ -17,6 +17,7 @@ use zed_actions::OpenBrowser;
 use zed_actions::agent::{OpenClaudeCodeOnboardingModal, ReauthenticateAgent};
 
 use crate::acp::{AcpThreadHistory, ThreadHistoryEvent};
+use crate::context_store::ContextStore;
 use crate::ui::{AcpOnboardingModal, ClaudeCodeOnboardingModal};
 use crate::{
     AddContextServer, AgentDiffPane, DeleteRecentlyOpenThread, Follow, InlineAssistant,
@@ -32,16 +33,11 @@ use crate::{
 use crate::{
     ExternalAgent, NewExternalAgentThread, NewNativeAgentThreadFromSummary, placeholder_command,
 };
-use agent::{
-    context_store::ContextStore,
-    thread_store::{TextThreadStore, ThreadStore},
-};
 use agent_settings::AgentSettings;
 use ai_onboarding::AgentPanelOnboarding;
 use anyhow::{Result, anyhow};
-use assistant_context::{AssistantContext, ContextEvent, ContextSummary};
 use assistant_slash_command::SlashCommandWorkingSet;
-use assistant_tool::ToolWorkingSet;
+use assistant_text_thread::{TextThread, TextThreadEvent, TextThreadSummary};
 use client::{UserStore, zed_urls};
 use cloud_llm_client::{Plan, PlanV1, PlanV2, UsageLimit};
 use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer};
@@ -118,7 +114,7 @@ pub fn init(cx: &mut App) {
                 .register_action(|workspace, _: &NewTextThread, window, cx| {
                     if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
                         workspace.focus_panel::<AgentPanel>(window, cx);
-                        panel.update(cx, |panel, cx| panel.new_prompt_editor(window, cx));
+                        panel.update(cx, |panel, cx| panel.new_text_thread(window, cx));
                     }
                 })
                 .register_action(|workspace, action: &NewExternalAgentThread, window, cx| {
@@ -203,7 +199,7 @@ enum ActiveView {
         thread_view: Entity<AcpThreadView>,
     },
     TextThread {
-        context_editor: Entity<TextThreadEditor>,
+        text_thread_editor: Entity<TextThreadEditor>,
         title_editor: Entity<Editor>,
         buffer_search_bar: Entity<BufferSearchBar>,
         _subscriptions: Vec<gpui::Subscription>,
@@ -281,7 +277,7 @@ impl ActiveView {
     pub fn native_agent(
         fs: Arc<dyn Fs>,
         prompt_store: Option<Entity<PromptStore>>,
-        acp_history_store: Entity<agent2::HistoryStore>,
+        history_store: Entity<agent::HistoryStore>,
         project: Entity<Project>,
         workspace: WeakEntity<Workspace>,
         window: &mut Window,
@@ -289,12 +285,12 @@ impl ActiveView {
     ) -> Self {
         let thread_view = cx.new(|cx| {
             crate::acp::AcpThreadView::new(
-                ExternalAgent::NativeAgent.server(fs, acp_history_store.clone()),
+                ExternalAgent::NativeAgent.server(fs, history_store.clone()),
                 None,
                 None,
                 workspace,
                 project,
-                acp_history_store,
+                history_store,
                 prompt_store,
                 window,
                 cx,
@@ -304,14 +300,14 @@ impl ActiveView {
         Self::ExternalAgentThread { thread_view }
     }
 
-    pub fn prompt_editor(
-        context_editor: Entity<TextThreadEditor>,
-        acp_history_store: Entity<agent2::HistoryStore>,
+    pub fn text_thread(
+        text_thread_editor: Entity<TextThreadEditor>,
+        acp_history_store: Entity<agent::HistoryStore>,
         language_registry: Arc<LanguageRegistry>,
         window: &mut Window,
         cx: &mut App,
     ) -> Self {
-        let title = context_editor.read(cx).title(cx).to_string();
+        let title = text_thread_editor.read(cx).title(cx).to_string();
 
         let editor = cx.new(|cx| {
             let mut editor = Editor::single_line(window, cx);
@@ -327,7 +323,7 @@ impl ActiveView {
         let subscriptions = vec![
             window.subscribe(&editor, cx, {
                 {
-                    let context_editor = context_editor.clone();
+                    let text_thread_editor = text_thread_editor.clone();
                     move |editor, event, window, cx| match event {
                         EditorEvent::BufferEdited => {
                             if suppress_first_edit {
@@ -336,19 +332,19 @@ impl ActiveView {
                             }
                             let new_summary = editor.read(cx).text(cx);
 
-                            context_editor.update(cx, |context_editor, cx| {
-                                context_editor
-                                    .context()
-                                    .update(cx, |assistant_context, cx| {
-                                        assistant_context.set_custom_summary(new_summary, cx);
+                            text_thread_editor.update(cx, |text_thread_editor, cx| {
+                                text_thread_editor
+                                    .text_thread()
+                                    .update(cx, |text_thread, cx| {
+                                        text_thread.set_custom_summary(new_summary, cx);
                                     })
                             })
                         }
                         EditorEvent::Blurred => {
                             if editor.read(cx).text(cx).is_empty() {
-                                let summary = context_editor
+                                let summary = text_thread_editor
                                     .read(cx)
-                                    .context()
+                                    .text_thread()
                                     .read(cx)
                                     .summary()
                                     .or_default();
@@ -362,24 +358,24 @@ impl ActiveView {
                     }
                 }
             }),
-            window.subscribe(&context_editor.read(cx).context().clone(), cx, {
+            window.subscribe(&text_thread_editor.read(cx).text_thread().clone(), cx, {
                 let editor = editor.clone();
-                move |assistant_context, event, window, cx| match event {
-                    ContextEvent::SummaryGenerated => {
-                        let summary = assistant_context.read(cx).summary().or_default();
+                move |text_thread, event, window, cx| match event {
+                    TextThreadEvent::SummaryGenerated => {
+                        let summary = text_thread.read(cx).summary().or_default();
 
                         editor.update(cx, |editor, cx| {
                             editor.set_text(summary, window, cx);
                         })
                     }
-                    ContextEvent::PathChanged { old_path, new_path } => {
+                    TextThreadEvent::PathChanged { old_path, new_path } => {
                         acp_history_store.update(cx, |history_store, cx| {
                             if let Some(old_path) = old_path {
                                 history_store
                                     .replace_recently_opened_text_thread(old_path, new_path, cx);
                             } else {
                                 history_store.push_recently_opened_entry(
-                                    agent2::HistoryEntryId::TextThread(new_path.clone()),
+                                    agent::HistoryEntryId::TextThread(new_path.clone()),
                                     cx,
                                 );
                             }
@@ -393,11 +389,11 @@ impl ActiveView {
         let buffer_search_bar =
             cx.new(|cx| BufferSearchBar::new(Some(language_registry), window, cx));
         buffer_search_bar.update(cx, |buffer_search_bar, cx| {
-            buffer_search_bar.set_active_pane_item(Some(&context_editor), window, cx)
+            buffer_search_bar.set_active_pane_item(Some(&text_thread_editor), window, cx)
         });
 
         Self::TextThread {
-            context_editor,
+            text_thread_editor,
             title_editor: editor,
             buffer_search_bar,
             _subscriptions: subscriptions,
@@ -412,11 +408,11 @@ pub struct AgentPanel {
     project: Entity<Project>,
     fs: Arc<dyn Fs>,
     language_registry: Arc<LanguageRegistry>,
-    thread_store: Entity<ThreadStore>,
     acp_history: Entity<AcpThreadHistory>,
-    history_store: Entity<agent2::HistoryStore>,
-    context_store: Entity<TextThreadStore>,
+    history_store: Entity<agent::HistoryStore>,
+    text_thread_store: Entity<assistant_text_thread::TextThreadStore>,
     prompt_store: Option<Entity<PromptStore>>,
+    context_server_registry: Entity<ContextServerRegistry>,
     inline_assist_context_store: Entity<ContextStore>,
     configuration: Option<Entity<AgentConfiguration>>,
     configuration_subscription: Option<Subscription>,
@@ -424,8 +420,8 @@ pub struct AgentPanel {
     previous_view: Option<ActiveView>,
     new_thread_menu_handle: PopoverMenuHandle<ContextMenu>,
     agent_panel_menu_handle: PopoverMenuHandle<ContextMenu>,
-    assistant_navigation_menu_handle: PopoverMenuHandle<ContextMenu>,
-    assistant_navigation_menu: Option<Entity<ContextMenu>>,
+    agent_navigation_menu_handle: PopoverMenuHandle<ContextMenu>,
+    agent_navigation_menu: Option<Entity<ContextMenu>>,
     width: Option<Pixels>,
     height: Option<Pixels>,
     zoomed: bool,
@@ -463,33 +459,6 @@ impl AgentPanel {
                 Ok(prompt_store) => prompt_store.await.ok(),
                 Err(_) => None,
             };
-            let tools = cx.new(|_| ToolWorkingSet::default())?;
-            let thread_store = workspace
-                .update(cx, |workspace, cx| {
-                    let project = workspace.project().clone();
-                    ThreadStore::load(
-                        project,
-                        tools.clone(),
-                        prompt_store.clone(),
-                        prompt_builder.clone(),
-                        cx,
-                    )
-                })?
-                .await?;
-
-            let slash_commands = Arc::new(SlashCommandWorkingSet::default());
-            let context_store = workspace
-                .update(cx, |workspace, cx| {
-                    let project = workspace.project().clone();
-                    assistant_context::ContextStore::new(
-                        project,
-                        prompt_builder.clone(),
-                        slash_commands,
-                        cx,
-                    )
-                })?
-                .await?;
-
             let serialized_panel = if let Some(panel) = cx
                 .background_spawn(async move { KEY_VALUE_STORE.read_kvp(AGENT_PANEL_KEY) })
                 .await
@@ -501,17 +470,22 @@ impl AgentPanel {
                 None
             };
 
-            let panel = workspace.update_in(cx, |workspace, window, cx| {
-                let panel = cx.new(|cx| {
-                    Self::new(
-                        workspace,
-                        thread_store,
-                        context_store,
-                        prompt_store,
-                        window,
+            let slash_commands = Arc::new(SlashCommandWorkingSet::default());
+            let text_thread_store = workspace
+                .update(cx, |workspace, cx| {
+                    let project = workspace.project().clone();
+                    assistant_text_thread::TextThreadStore::new(
+                        project,
+                        prompt_builder,
+                        slash_commands,
                         cx,
                     )
-                });
+                })?
+                .await?;
+
+            let panel = workspace.update_in(cx, |workspace, window, cx| {
+                let panel =
+                    cx.new(|cx| Self::new(workspace, text_thread_store, prompt_store, window, cx));
 
                 panel.as_mut(cx).loading = true;
                 if let Some(serialized_panel) = serialized_panel {
@@ -538,8 +512,7 @@ impl AgentPanel {
 
     fn new(
         workspace: &Workspace,
-        thread_store: Entity<ThreadStore>,
-        context_store: Entity<TextThreadStore>,
+        text_thread_store: Entity<assistant_text_thread::TextThreadStore>,
         prompt_store: Option<Entity<PromptStore>>,
         window: &mut Window,
         cx: &mut Context<Self>,
@@ -551,10 +524,11 @@ impl AgentPanel {
         let client = workspace.client().clone();
         let workspace = workspace.weak_handle();
 
-        let inline_assist_context_store =
-            cx.new(|_cx| ContextStore::new(project.downgrade(), Some(thread_store.downgrade())));
+        let inline_assist_context_store = cx.new(|_cx| ContextStore::new(project.downgrade()));
+        let context_server_registry =
+            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
 
-        let history_store = cx.new(|cx| agent2::HistoryStore::new(context_store.clone(), cx));
+        let history_store = cx.new(|cx| agent::HistoryStore::new(text_thread_store.clone(), cx));
         let acp_history = cx.new(|cx| AcpThreadHistory::new(history_store.clone(), window, cx));
         cx.subscribe_in(
             &acp_history,
@@ -570,7 +544,7 @@ impl AgentPanel {
                     );
                 }
                 ThreadHistoryEvent::Open(HistoryEntry::TextThread(thread)) => {
-                    this.open_saved_prompt_editor(thread.path.clone(), window, cx)
+                    this.open_saved_text_thread(thread.path.clone(), window, cx)
                         .detach_and_log_err(cx);
                 }
             },
@@ -589,11 +563,10 @@ impl AgentPanel {
                 cx,
             ),
             DefaultView::TextThread => {
-                let context =
-                    context_store.update(cx, |context_store, cx| context_store.create(cx));
+                let context = text_thread_store.update(cx, |store, cx| store.create(cx));
                 let lsp_adapter_delegate = make_lsp_adapter_delegate(&project.clone(), cx).unwrap();
-                let context_editor = cx.new(|cx| {
-                    let mut editor = TextThreadEditor::for_context(
+                let text_thread_editor = cx.new(|cx| {
+                    let mut editor = TextThreadEditor::for_text_thread(
                         context,
                         fs.clone(),
                         workspace.clone(),
@@ -605,8 +578,8 @@ impl AgentPanel {
                     editor.insert_default_prompt(window, cx);
                     editor
                 });
-                ActiveView::prompt_editor(
-                    context_editor,
+                ActiveView::text_thread(
+                    text_thread_editor,
                     history_store.clone(),
                     language_registry.clone(),
                     window,
@@ -619,7 +592,7 @@ impl AgentPanel {
 
         window.defer(cx, move |window, cx| {
             let panel = weak_panel.clone();
-            let assistant_navigation_menu =
+            let agent_navigation_menu =
                 ContextMenu::build_persistent(window, cx, move |mut menu, _window, cx| {
                     if let Some(panel) = panel.upgrade() {
                         menu = Self::populate_recently_opened_menu_section(menu, panel, cx);
@@ -633,7 +606,7 @@ impl AgentPanel {
             weak_panel
                 .update(cx, |panel, cx| {
                     cx.subscribe_in(
-                        &assistant_navigation_menu,
+                        &agent_navigation_menu,
                         window,
                         |_, menu, _: &DismissEvent, window, cx| {
                             menu.update(cx, |menu, _| {
@@ -643,7 +616,7 @@ impl AgentPanel {
                         },
                     )
                     .detach();
-                    panel.assistant_navigation_menu = Some(assistant_navigation_menu);
+                    panel.agent_navigation_menu = Some(agent_navigation_menu);
                 })
                 .ok();
         });
@@ -666,17 +639,17 @@ impl AgentPanel {
             project: project.clone(),
             fs: fs.clone(),
             language_registry,
-            thread_store: thread_store.clone(),
-            context_store,
+            text_thread_store,
             prompt_store,
             configuration: None,
             configuration_subscription: None,
+            context_server_registry,
             inline_assist_context_store,
             previous_view: None,
             new_thread_menu_handle: PopoverMenuHandle::default(),
             agent_panel_menu_handle: PopoverMenuHandle::default(),
-            assistant_navigation_menu_handle: PopoverMenuHandle::default(),
-            assistant_navigation_menu: None,
+            agent_navigation_menu_handle: PopoverMenuHandle::default(),
+            agent_navigation_menu: None,
             width: None,
             height: None,
             zoomed: false,
@@ -711,12 +684,12 @@ impl AgentPanel {
         &self.inline_assist_context_store
     }
 
-    pub(crate) fn thread_store(&self) -> &Entity<ThreadStore> {
-        &self.thread_store
+    pub(crate) fn thread_store(&self) -> &Entity<HistoryStore> {
+        &self.history_store
     }
 
-    pub(crate) fn text_thread_store(&self) -> &Entity<TextThreadStore> {
-        &self.context_store
+    pub(crate) fn context_server_registry(&self) -> &Entity<ContextServerRegistry> {
+        &self.context_server_registry
     }
 
     fn active_thread_view(&self) -> Option<&Entity<AcpThreadView>> {
@@ -753,18 +726,18 @@ impl AgentPanel {
         );
     }
 
-    fn new_prompt_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+    fn new_text_thread(&mut self, window: &mut Window, cx: &mut Context<Self>) {
         telemetry::event!("Agent Thread Started", agent = "zed-text");
 
         let context = self
-            .context_store
+            .text_thread_store
             .update(cx, |context_store, cx| context_store.create(cx));
         let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project, cx)
             .log_err()
             .flatten();
 
-        let context_editor = cx.new(|cx| {
-            let mut editor = TextThreadEditor::for_context(
+        let text_thread_editor = cx.new(|cx| {
+            let mut editor = TextThreadEditor::for_text_thread(
                 context,
                 self.fs.clone(),
                 self.workspace.clone(),
@@ -783,8 +756,8 @@ impl AgentPanel {
         }
 
         self.set_active_view(
-            ActiveView::prompt_editor(
-                context_editor.clone(),
+            ActiveView::text_thread(
+                text_thread_editor.clone(),
                 self.history_store.clone(),
                 self.language_registry.clone(),
                 window,
@@ -793,7 +766,7 @@ impl AgentPanel {
             window,
             cx,
         );
-        context_editor.focus_handle(cx).focus(window);
+        text_thread_editor.focus_handle(cx).focus(window);
     }
 
     fn external_thread(
@@ -921,34 +894,31 @@ impl AgentPanel {
                 self.set_active_view(previous_view, window, cx);
             }
         } else {
-            self.thread_store
-                .update(cx, |thread_store, cx| thread_store.reload(cx))
-                .detach_and_log_err(cx);
             self.set_active_view(ActiveView::History, window, cx);
         }
         cx.notify();
     }
 
-    pub(crate) fn open_saved_prompt_editor(
+    pub(crate) fn open_saved_text_thread(
         &mut self,
         path: Arc<Path>,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) -> Task<Result<()>> {
-        let context = self
-            .context_store
-            .update(cx, |store, cx| store.open_local_context(path, cx));
+        let text_thread_task = self
+            .history_store
+            .update(cx, |store, cx| store.load_text_thread(path, cx));
         cx.spawn_in(window, async move |this, cx| {
-            let context = context.await?;
+            let text_thread = text_thread_task.await?;
             this.update_in(cx, |this, window, cx| {
-                this.open_prompt_editor(context, window, cx);
+                this.open_text_thread(text_thread, window, cx);
             })
         })
     }
 
-    pub(crate) fn open_prompt_editor(
+    pub(crate) fn open_text_thread(
         &mut self,
-        context: Entity<AssistantContext>,
+        text_thread: Entity<TextThread>,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
@@ -956,8 +926,8 @@ impl AgentPanel {
             .log_err()
             .flatten();
         let editor = cx.new(|cx| {
-            TextThreadEditor::for_context(
-                context,
+            TextThreadEditor::for_text_thread(
+                text_thread,
                 self.fs.clone(),
                 self.workspace.clone(),
                 self.project.clone(),
@@ -973,7 +943,7 @@ impl AgentPanel {
         }
 
         self.set_active_view(
-            ActiveView::prompt_editor(
+            ActiveView::text_thread(
                 editor,
                 self.history_store.clone(),
                 self.language_registry.clone(),
@@ -995,8 +965,10 @@ impl AgentPanel {
                         ActiveView::ExternalAgentThread { thread_view } => {
                             thread_view.focus_handle(cx).focus(window);
                         }
-                        ActiveView::TextThread { context_editor, .. } => {
-                            context_editor.focus_handle(cx).focus(window);
+                        ActiveView::TextThread {
+                            text_thread_editor, ..
+                        } => {
+                            text_thread_editor.focus_handle(cx).focus(window);
                         }
                         ActiveView::History | ActiveView::Configuration => {}
                     }
@@ -1013,7 +985,7 @@ impl AgentPanel {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        self.assistant_navigation_menu_handle.toggle(window, cx);
+        self.agent_navigation_menu_handle.toggle(window, cx);
     }
 
     pub fn toggle_options_menu(
@@ -1106,7 +1078,6 @@ impl AgentPanel {
     pub(crate) fn open_configuration(&mut self, window: &mut Window, cx: &mut Context<Self>) {
         let agent_server_store = self.project.read(cx).agent_server_store().clone();
         let context_server_store = self.project.read(cx).context_server_store();
-        let tools = self.thread_store.read(cx).tools();
         let fs = self.fs.clone();
 
         self.set_active_view(ActiveView::Configuration, window, cx);
@@ -1115,7 +1086,7 @@ impl AgentPanel {
                 fs,
                 agent_server_store,
                 context_server_store,
-                tools,
+                self.context_server_registry.clone(),
                 self.language_registry.clone(),
                 self.workspace.clone(),
                 window,
@@ -1183,7 +1154,7 @@ impl AgentPanel {
                     });
                 }
 
-                self.new_thread(&NewThread::default(), window, cx);
+                self.new_thread(&NewThread, window, cx);
                 if let Some((thread, model)) = self
                     .active_native_agent_thread(cx)
                     .zip(provider.default_model(cx))
@@ -1205,7 +1176,7 @@ impl AgentPanel {
         }
     }
 
-    pub(crate) fn active_native_agent_thread(&self, cx: &App) -> Option<Entity<agent2::Thread>> {
+    pub(crate) fn active_native_agent_thread(&self, cx: &App) -> Option<Entity<agent::Thread>> {
         match &self.active_view {
             ActiveView::ExternalAgentThread { thread_view, .. } => {
                 thread_view.read(cx).as_native_thread(cx)
@@ -1214,9 +1185,11 @@ impl AgentPanel {
         }
     }
 
-    pub(crate) fn active_context_editor(&self) -> Option<Entity<TextThreadEditor>> {
+    pub(crate) fn active_text_thread_editor(&self) -> Option<Entity<TextThreadEditor>> {
         match &self.active_view {
-            ActiveView::TextThread { context_editor, .. } => Some(context_editor.clone()),
+            ActiveView::TextThread {
+                text_thread_editor, ..
+            } => Some(text_thread_editor.clone()),
             _ => None,
         }
     }
@@ -1237,16 +1210,16 @@ impl AgentPanel {
         let new_is_special = new_is_history || new_is_config;
 
         match &new_view {
-            ActiveView::TextThread { context_editor, .. } => {
-                self.history_store.update(cx, |store, cx| {
-                    if let Some(path) = context_editor.read(cx).context().read(cx).path() {
-                        store.push_recently_opened_entry(
-                            agent2::HistoryEntryId::TextThread(path.clone()),
-                            cx,
-                        )
-                    }
-                })
-            }
+            ActiveView::TextThread {
+                text_thread_editor, ..
+            } => self.history_store.update(cx, |store, cx| {
+                if let Some(path) = text_thread_editor.read(cx).text_thread().read(cx).path() {
+                    store.push_recently_opened_entry(
+                        agent::HistoryEntryId::TextThread(path.clone()),
+                        cx,
+                    )
+                }
+            }),
             ActiveView::ExternalAgentThread { .. } => {}
             ActiveView::History | ActiveView::Configuration => {}
         }
@@ -1295,15 +1268,15 @@ impl AgentPanel {
                         let entry = entry.clone();
                         panel
                             .update(cx, move |this, cx| match &entry {
-                                agent2::HistoryEntry::AcpThread(entry) => this.external_thread(
+                                agent::HistoryEntry::AcpThread(entry) => this.external_thread(
                                     Some(ExternalAgent::NativeAgent),
                                     Some(entry.clone()),
                                     None,
                                     window,
                                     cx,
                                 ),
-                                agent2::HistoryEntry::TextThread(entry) => this
-                                    .open_saved_prompt_editor(entry.path.clone(), window, cx)
+                                agent::HistoryEntry::TextThread(entry) => this
+                                    .open_saved_text_thread(entry.path.clone(), window, cx)
                                     .detach_and_log_err(cx),
                             })
                             .ok();
@@ -1403,7 +1376,9 @@ impl Focusable for AgentPanel {
         match &self.active_view {
             ActiveView::ExternalAgentThread { thread_view, .. } => thread_view.focus_handle(cx),
             ActiveView::History => self.acp_history.focus_handle(cx),
-            ActiveView::TextThread { context_editor, .. } => context_editor.focus_handle(cx),
+            ActiveView::TextThread {
+                text_thread_editor, ..
+            } => text_thread_editor.focus_handle(cx),
             ActiveView::Configuration => {
                 if let Some(configuration) = self.configuration.as_ref() {
                     configuration.focus_handle(cx)
@@ -1426,6 +1401,10 @@ impl Panel for AgentPanel {
         "AgentPanel"
     }
 
+    fn panel_key() -> &'static str {
+        AGENT_PANEL_KEY
+    }
+
     fn position(&self, _window: &Window, cx: &App) -> DockPosition {
         agent_panel_dock_position(cx)
     }
@@ -1534,17 +1513,17 @@ impl AgentPanel {
             }
             ActiveView::TextThread {
                 title_editor,
-                context_editor,
+                text_thread_editor,
                 ..
             } => {
-                let summary = context_editor.read(cx).context().read(cx).summary();
+                let summary = text_thread_editor.read(cx).text_thread().read(cx).summary();
 
                 match summary {
-                    ContextSummary::Pending => Label::new(ContextSummary::DEFAULT)
+                    TextThreadSummary::Pending => Label::new(TextThreadSummary::DEFAULT)
                         .color(Color::Muted)
                         .truncate()
                         .into_any_element(),
-                    ContextSummary::Content(summary) => {
+                    TextThreadSummary::Content(summary) => {
                         if summary.done {
                             div()
                                 .w_full()
@@ -1557,17 +1536,17 @@ impl AgentPanel {
                                 .into_any_element()
                         }
                     }
-                    ContextSummary::Error => h_flex()
+                    TextThreadSummary::Error => h_flex()
                         .w_full()
                         .child(title_editor.clone())
                         .child(
                             IconButton::new("retry-summary-generation", IconName::RotateCcw)
                                 .icon_size(IconSize::Small)
                                 .on_click({
-                                    let context_editor = context_editor.clone();
+                                    let text_thread_editor = text_thread_editor.clone();
                                     move |_, _window, cx| {
-                                        context_editor.update(cx, |context_editor, cx| {
-                                            context_editor.regenerate_summary(cx);
+                                        text_thread_editor.update(cx, |text_thread_editor, cx| {
+                                            text_thread_editor.regenerate_summary(cx);
                                         });
                                     }
                                 })
@@ -1622,12 +1601,11 @@ impl AgentPanel {
                     .icon_size(IconSize::Small),
                 {
                     let focus_handle = focus_handle.clone();
-                    move |window, cx| {
+                    move |_window, cx| {
                         Tooltip::for_action_in(
                             "Toggle Agent Menu",
                             &ToggleOptionsMenu,
                             &focus_handle,
-                            window,
                             cx,
                         )
                     }
@@ -1691,7 +1669,7 @@ impl AgentPanel {
                             .separator();
 
                         menu = menu
-                            .action("Rules…", Box::new(OpenRulesLibrary::default()))
+                            .action("Rules", Box::new(OpenRulesLibrary::default()))
                             .action("Settings", Box::new(OpenSettings))
                             .separator()
                             .action(full_screen_label, Box::new(ToggleZoom));
@@ -1718,21 +1696,20 @@ impl AgentPanel {
             .trigger_with_tooltip(
                 IconButton::new("agent-nav-menu", icon).icon_size(IconSize::Small),
                 {
-                    move |window, cx| {
+                    move |_window, cx| {
                         Tooltip::for_action_in(
                             "Toggle Recent Threads",
                             &ToggleNavigationMenu,
                             &focus_handle,
-                            window,
                             cx,
                         )
                     }
                 },
             )
             .anchor(corner)
-            .with_handle(self.assistant_navigation_menu_handle.clone())
+            .with_handle(self.agent_navigation_menu_handle.clone())
             .menu({
-                let menu = self.assistant_navigation_menu.clone();
+                let menu = self.agent_navigation_menu.clone();
                 move |window, cx| {
                     telemetry::event!("View Thread History Clicked");
 
@@ -1757,8 +1734,8 @@ impl AgentPanel {
                 this.go_back(&workspace::GoBack, window, cx);
             }))
             .tooltip({
-                move |window, cx| {
-                    Tooltip::for_action_in("Go Back", &workspace::GoBack, &focus_handle, window, cx)
+                move |_window, cx| {
+                    Tooltip::for_action_in("Go Back", &workspace::GoBack, &focus_handle, cx)
                 }
             })
     }
@@ -1779,12 +1756,11 @@ impl AgentPanel {
                 IconButton::new("new_thread_menu_btn", IconName::Plus).icon_size(IconSize::Small),
                 {
                     let focus_handle = focus_handle.clone();
-                    move |window, cx| {
+                    move |_window, cx| {
                         Tooltip::for_action_in(
                             "New…",
                             &ToggleNewThreadMenu,
                             &focus_handle,
-                            window,
                             cx,
                         )
                     }
@@ -1832,7 +1808,7 @@ impl AgentPanel {
                             })
                             .item(
                                 ContextMenuEntry::new("New Thread")
-                                    .action(NewThread::default().boxed_clone())
+                                    .action(NewThread.boxed_clone())
                                     .icon(IconName::Thread)
                                     .icon_color(Color::Muted)
                                     .handler({
@@ -2030,14 +2006,8 @@ impl AgentPanel {
             .when_some(self.selected_agent.icon(), |this, icon| {
                 this.px(DynamicSpacing::Base02.rems(cx))
                     .child(Icon::new(icon).color(Color::Muted))
-                    .tooltip(move |window, cx| {
-                        Tooltip::with_meta(
-                            selected_agent_label.clone(),
-                            None,
-                            "Selected Agent",
-                            window,
-                            cx,
-                        )
+                    .tooltip(move |_window, cx| {
+                        Tooltip::with_meta(selected_agent_label.clone(), None, "Selected Agent", cx)
                     })
             })
             .into_any_element();
@@ -2213,7 +2183,6 @@ impl AgentPanel {
         border_bottom: bool,
         configuration_error: &ConfigurationError,
         focus_handle: &FocusHandle,
-        window: &mut Window,
         cx: &mut App,
     ) -> impl IntoElement {
         let zed_provider_configured = AgentSettings::get_global(cx)
@@ -2262,7 +2231,7 @@ impl AgentPanel {
                         .style(ButtonStyle::Tinted(ui::TintColor::Warning))
                         .label_size(LabelSize::Small)
                         .key_binding(
-                            KeyBinding::for_action_in(&OpenSettings, focus_handle, window, cx)
+                            KeyBinding::for_action_in(&OpenSettings, focus_handle, cx)
                                 .map(|kb| kb.size(rems_from_px(12.))),
                         )
                         .on_click(|_event, window, cx| {
@@ -2278,9 +2247,9 @@ impl AgentPanel {
         }
     }
 
-    fn render_prompt_editor(
+    fn render_text_thread(
         &self,
-        context_editor: &Entity<TextThreadEditor>,
+        text_thread_editor: &Entity<TextThreadEditor>,
         buffer_search_bar: &Entity<BufferSearchBar>,
         window: &mut Window,
         cx: &mut Context<Self>,
@@ -2314,7 +2283,7 @@ impl AgentPanel {
                     )
                 })
             })
-            .child(context_editor.clone())
+            .child(text_thread_editor.clone())
             .child(self.render_drag_target(cx))
     }
 
@@ -2390,10 +2359,12 @@ impl AgentPanel {
                     thread_view.insert_dragged_files(paths, added_worktrees, window, cx);
                 });
             }
-            ActiveView::TextThread { context_editor, .. } => {
-                context_editor.update(cx, |context_editor, cx| {
+            ActiveView::TextThread {
+                text_thread_editor, ..
+            } => {
+                text_thread_editor.update(cx, |text_thread_editor, cx| {
                     TextThreadEditor::insert_dragged_files(
-                        context_editor,
+                        text_thread_editor,
                         paths,
                         added_worktrees,
                         window,
@@ -2409,8 +2380,8 @@ impl AgentPanel {
         let mut key_context = KeyContext::new_with_defaults();
         key_context.add("AgentPanel");
         match &self.active_view {
-            ActiveView::ExternalAgentThread { .. } => key_context.add("external_agent_thread"),
-            ActiveView::TextThread { .. } => key_context.add("prompt_editor"),
+            ActiveView::ExternalAgentThread { .. } => key_context.add("acp_thread"),
+            ActiveView::TextThread { .. } => key_context.add("text_thread"),
             ActiveView::History | ActiveView::Configuration => {}
         }
         key_context
@@ -2464,7 +2435,7 @@ impl Render for AgentPanel {
                     .child(self.render_drag_target(cx)),
                 ActiveView::History => parent.child(self.acp_history.clone()),
                 ActiveView::TextThread {
-                    context_editor,
+                    text_thread_editor,
                     buffer_search_bar,
                     ..
                 } => {
@@ -2480,15 +2451,14 @@ impl Render for AgentPanel {
                                     true,
                                     err,
                                     &self.focus_handle(cx),
-                                    window,
                                     cx,
                                 ))
                             } else {
                                 this
                             }
                         })
-                        .child(self.render_prompt_editor(
-                            context_editor,
+                        .child(self.render_text_thread(
+                            text_thread_editor,
                             buffer_search_bar,
                             window,
                             cx,
@@ -2538,8 +2508,7 @@ impl rules_library::InlineAssistDelegate for PromptLibraryInlineAssist {
             };
             let prompt_store = None;
             let thread_store = None;
-            let text_thread_store = None;
-            let context_store = cx.new(|_| ContextStore::new(project.clone(), None));
+            let context_store = cx.new(|_| ContextStore::new(project.clone()));
             assistant.assist(
                 prompt_editor,
                 self.workspace.clone(),
@@ -2547,7 +2516,6 @@ impl rules_library::InlineAssistDelegate for PromptLibraryInlineAssist {
                 project,
                 prompt_store,
                 thread_store,
-                text_thread_store,
                 initial_prompt,
                 window,
                 cx,
@@ -2568,17 +2536,17 @@ impl rules_library::InlineAssistDelegate for PromptLibraryInlineAssist {
 pub struct ConcreteAssistantPanelDelegate;
 
 impl AgentPanelDelegate for ConcreteAssistantPanelDelegate {
-    fn active_context_editor(
+    fn active_text_thread_editor(
         &self,
         workspace: &mut Workspace,
         _window: &mut Window,
         cx: &mut Context<Workspace>,
     ) -> Option<Entity<TextThreadEditor>> {
         let panel = workspace.panel::<AgentPanel>(cx)?;
-        panel.read(cx).active_context_editor()
+        panel.read(cx).active_text_thread_editor()
     }
 
-    fn open_saved_context(
+    fn open_local_text_thread(
         &self,
         workspace: &mut Workspace,
         path: Arc<Path>,
@@ -2590,14 +2558,14 @@ impl AgentPanelDelegate for ConcreteAssistantPanelDelegate {
         };
 
         panel.update(cx, |panel, cx| {
-            panel.open_saved_prompt_editor(path, window, cx)
+            panel.open_saved_text_thread(path, window, cx)
         })
     }
 
-    fn open_remote_context(
+    fn open_remote_text_thread(
         &self,
         _workspace: &mut Workspace,
-        _context_id: assistant_context::ContextId,
+        _text_thread_id: assistant_text_thread::TextThreadId,
         _window: &mut Window,
         _cx: &mut Context<Workspace>,
     ) -> Task<Result<Entity<TextThreadEditor>>> {
@@ -2628,15 +2596,15 @@ impl AgentPanelDelegate for ConcreteAssistantPanelDelegate {
                     thread_view.update(cx, |thread_view, cx| {
                         thread_view.insert_selections(window, cx);
                     });
-                } else if let Some(context_editor) = panel.active_context_editor() {
+                } else if let Some(text_thread_editor) = panel.active_text_thread_editor() {
                     let snapshot = buffer.read(cx).snapshot(cx);
                     let selection_ranges = selection_ranges
                         .into_iter()
                         .map(|range| range.to_point(&snapshot))
                         .collect::<Vec<_>>();
 
-                    context_editor.update(cx, |context_editor, cx| {
-                        context_editor.quote_ranges(selection_ranges, snapshot, window, cx)
+                    text_thread_editor.update(cx, |text_thread_editor, cx| {
+                        text_thread_editor.quote_ranges(selection_ranges, snapshot, window, cx)
                     });
                 }
             });

crates/agent_ui/src/agent_ui.rs 🔗

@@ -4,8 +4,10 @@ mod agent_diff;
 mod agent_model_selector;
 mod agent_panel;
 mod buffer_codegen;
+mod context;
 mod context_picker;
 mod context_server_configuration;
+mod context_store;
 mod context_strip;
 mod inline_assistant;
 mod inline_prompt_editor;
@@ -22,7 +24,6 @@ mod ui;
 use std::rc::Rc;
 use std::sync::Arc;
 
-use agent::ThreadId;
 use agent_settings::{AgentProfileId, AgentSettings};
 use assistant_slash_command::SlashCommandRegistry;
 use client::Client;
@@ -129,20 +130,11 @@ actions!(
     ]
 );
 
-#[derive(Clone, Copy, Debug, PartialEq, Eq, Action)]
-#[action(namespace = agent)]
-#[action(deprecated_aliases = ["assistant::QuoteSelection"])]
-/// Quotes the current selection in the agent panel's message editor.
-pub struct QuoteSelection;
-
 /// Creates a new conversation thread, optionally based on an existing thread.
 #[derive(Default, Clone, PartialEq, Deserialize, JsonSchema, Action)]
 #[action(namespace = agent)]
 #[serde(deny_unknown_fields)]
-pub struct NewThread {
-    #[serde(default)]
-    from_thread_id: Option<ThreadId>,
-}
+pub struct NewThread;
 
 /// Creates a new external agent conversation thread.
 #[derive(Default, Clone, PartialEq, Deserialize, JsonSchema, Action)]
@@ -196,13 +188,13 @@ impl ExternalAgent {
     pub fn server(
         &self,
         fs: Arc<dyn fs::Fs>,
-        history: Entity<agent2::HistoryStore>,
+        history: Entity<agent::HistoryStore>,
     ) -> Rc<dyn agent_servers::AgentServer> {
         match self {
             Self::Gemini => Rc::new(agent_servers::Gemini),
             Self::ClaudeCode => Rc::new(agent_servers::ClaudeCode),
             Self::Codex => Rc::new(agent_servers::Codex),
-            Self::NativeAgent => Rc::new(agent2::NativeAgentServer::new(fs, history)),
+            Self::NativeAgent => Rc::new(agent::NativeAgentServer::new(fs, history)),
             Self::Custom { name, command: _ } => {
                 Rc::new(agent_servers::CustomAgentServer::new(name.clone()))
             }
@@ -258,7 +250,7 @@ pub fn init(
 ) {
     AgentSettings::register(cx);
 
-    assistant_context::init(client.clone(), cx);
+    assistant_text_thread::init(client.clone(), cx);
     rules_library::init(cx);
     if !is_eval {
         // Initializing the language model from the user settings messes with the eval, so we only initialize them when
@@ -266,7 +258,6 @@ pub fn init(
         init_language_model_settings(cx);
     }
     assistant_slash_command::init(cx);
-    agent::init(fs.clone(), cx);
     agent_panel::init(cx);
     context_server_configuration::init(language_registry.clone(), fs.clone(), cx);
     TextThreadEditor::init(cx);

crates/agent_ui/src/buffer_codegen.rs 🔗

@@ -1,7 +1,5 @@
-use crate::inline_prompt_editor::CodegenStatus;
-use agent::{
-    ContextStore,
-    context::{ContextLoadResult, load_context},
+use crate::{
+    context::load_context, context_store::ContextStore, inline_prompt_editor::CodegenStatus,
 };
 use agent_settings::AgentSettings;
 use anyhow::{Context as _, Result};
@@ -434,16 +432,16 @@ impl CodegenAlternative {
             .generate_inline_transformation_prompt(user_prompt, language_name, buffer, range)
             .context("generating content prompt")?;
 
-        let context_task = self.context_store.as_ref().map(|context_store| {
+        let context_task = self.context_store.as_ref().and_then(|context_store| {
             if let Some(project) = self.project.upgrade() {
                 let context = context_store
                     .read(cx)
                     .context()
                     .cloned()
                     .collect::<Vec<_>>();
-                load_context(context, &project, &self.prompt_store, cx)
+                Some(load_context(context, &project, &self.prompt_store, cx))
             } else {
-                Task::ready(ContextLoadResult::default())
+                None
             }
         });
 
@@ -459,7 +457,6 @@ impl CodegenAlternative {
             if let Some(context_task) = context_task {
                 context_task
                     .await
-                    .loaded_context
                     .add_to_request_message(&mut request_message);
             }
 

crates/agent/src/context.rs → crates/agent_ui/src/context.rs 🔗

@@ -1,11 +1,8 @@
-use crate::thread::Thread;
-use assistant_context::AssistantContext;
-use assistant_tool::outline;
-use collections::HashSet;
+use agent::outline;
+use assistant_text_thread::TextThread;
 use futures::future;
 use futures::{FutureExt, future::Shared};
 use gpui::{App, AppContext as _, ElementId, Entity, SharedString, Task};
-use icons::IconName;
 use language::Buffer;
 use language_model::{LanguageModelImage, LanguageModelRequestMessage, MessageContent};
 use project::{Project, ProjectEntryId, ProjectPath, Worktree};
@@ -17,6 +14,7 @@ use std::hash::{Hash, Hasher};
 use std::path::PathBuf;
 use std::{ops::Range, path::Path, sync::Arc};
 use text::{Anchor, OffsetRangeExt as _};
+use ui::IconName;
 use util::markdown::MarkdownCodeBlock;
 use util::rel_path::RelPath;
 use util::{ResultExt as _, post_inc};
@@ -181,7 +179,7 @@ impl FileContextHandle {
         })
     }
 
-    fn load(self, cx: &App) -> Task<Option<(AgentContext, Vec<Entity<Buffer>>)>> {
+    fn load(self, cx: &App) -> Task<Option<AgentContext>> {
         let buffer_ref = self.buffer.read(cx);
         let Some(file) = buffer_ref.file() else {
             log::error!("file context missing path");
@@ -206,7 +204,7 @@ impl FileContextHandle {
                 text: buffer_content.text.into(),
                 is_outline: buffer_content.is_outline,
             });
-            Some((context, vec![buffer]))
+            Some(context)
         })
     }
 }
@@ -256,11 +254,7 @@ impl DirectoryContextHandle {
         self.entry_id.hash(state)
     }
 
-    fn load(
-        self,
-        project: Entity<Project>,
-        cx: &mut App,
-    ) -> Task<Option<(AgentContext, Vec<Entity<Buffer>>)>> {
+    fn load(self, project: Entity<Project>, cx: &mut App) -> Task<Option<AgentContext>> {
         let Some(worktree) = project.read(cx).worktree_for_entry(self.entry_id, cx) else {
             return Task::ready(None);
         };
@@ -307,7 +301,7 @@ impl DirectoryContextHandle {
             });
 
             cx.background_spawn(async move {
-                let (rope, buffer) = rope_task.await?;
+                let (rope, _buffer) = rope_task.await?;
                 let fenced_codeblock = MarkdownCodeBlock {
                     tag: &codeblock_tag(&full_path, None),
                     text: &rope.to_string(),
@@ -318,18 +312,22 @@ impl DirectoryContextHandle {
                     rel_path,
                     fenced_codeblock,
                 };
-                Some((descendant, buffer))
+                Some(descendant)
             })
         }));
 
         cx.background_spawn(async move {
-            let (descendants, buffers) = descendants_future.await.into_iter().flatten().unzip();
+            let descendants = descendants_future
+                .await
+                .into_iter()
+                .flatten()
+                .collect::<Vec<_>>();
             let context = AgentContext::Directory(DirectoryContext {
                 handle: self,
                 full_path: directory_full_path,
                 descendants,
             });
-            Some((context, buffers))
+            Some(context)
         })
     }
 }
@@ -397,7 +395,7 @@ impl SymbolContextHandle {
             .into()
     }
 
-    fn load(self, cx: &App) -> Task<Option<(AgentContext, Vec<Entity<Buffer>>)>> {
+    fn load(self, cx: &App) -> Task<Option<AgentContext>> {
         let buffer_ref = self.buffer.read(cx);
         let Some(file) = buffer_ref.file() else {
             log::error!("symbol context's file has no path");
@@ -406,14 +404,13 @@ impl SymbolContextHandle {
         let full_path = file.full_path(cx).to_string_lossy().into_owned();
         let line_range = self.enclosing_range.to_point(&buffer_ref.snapshot());
         let text = self.text(cx);
-        let buffer = self.buffer.clone();
         let context = AgentContext::Symbol(SymbolContext {
             handle: self,
             full_path,
             line_range,
             text,
         });
-        Task::ready(Some((context, vec![buffer])))
+        Task::ready(Some(context))
     }
 }
 
@@ -468,13 +465,12 @@ impl SelectionContextHandle {
             .into()
     }
 
-    fn load(self, cx: &App) -> Task<Option<(AgentContext, Vec<Entity<Buffer>>)>> {
+    fn load(self, cx: &App) -> Task<Option<AgentContext>> {
         let Some(full_path) = self.full_path(cx) else {
             log::error!("selection context's file has no path");
             return Task::ready(None);
         };
         let text = self.text(cx);
-        let buffer = self.buffer.clone();
         let context = AgentContext::Selection(SelectionContext {
             full_path: full_path.to_string_lossy().into_owned(),
             line_range: self.line_range(cx),
@@ -482,7 +478,7 @@ impl SelectionContextHandle {
             handle: self,
         });
 
-        Task::ready(Some((context, vec![buffer])))
+        Task::ready(Some(context))
     }
 }
 
@@ -523,8 +519,8 @@ impl FetchedUrlContext {
         }))
     }
 
-    pub fn load(self) -> Task<Option<(AgentContext, Vec<Entity<Buffer>>)>> {
-        Task::ready(Some((AgentContext::FetchedUrl(self), vec![])))
+    pub fn load(self) -> Task<Option<AgentContext>> {
+        Task::ready(Some(AgentContext::FetchedUrl(self)))
     }
 }
 
@@ -537,7 +533,7 @@ impl Display for FetchedUrlContext {
 
 #[derive(Debug, Clone)]
 pub struct ThreadContextHandle {
-    pub thread: Entity<Thread>,
+    pub thread: Entity<agent::Thread>,
     pub context_id: ContextId,
 }
 
@@ -558,22 +554,20 @@ impl ThreadContextHandle {
     }
 
     pub fn title(&self, cx: &App) -> SharedString {
-        self.thread.read(cx).summary().or_default()
+        self.thread.read(cx).title()
     }
 
-    fn load(self, cx: &App) -> Task<Option<(AgentContext, Vec<Entity<Buffer>>)>> {
-        cx.spawn(async move |cx| {
-            let text = Thread::wait_for_detailed_summary_or_text(&self.thread, cx).await?;
-            let title = self
-                .thread
-                .read_with(cx, |thread, _cx| thread.summary().or_default())
-                .ok()?;
+    fn load(self, cx: &mut App) -> Task<Option<AgentContext>> {
+        let task = self.thread.update(cx, |thread, cx| thread.summary(cx));
+        let title = self.title(cx);
+        cx.background_spawn(async move {
+            let text = task.await?;
             let context = AgentContext::Thread(ThreadContext {
                 title,
                 text,
                 handle: self,
             });
-            Some((context, vec![]))
+            Some(context)
         })
     }
 }
@@ -587,7 +581,7 @@ impl Display for ThreadContext {
 
 #[derive(Debug, Clone)]
 pub struct TextThreadContextHandle {
-    pub context: Entity<AssistantContext>,
+    pub text_thread: Entity<TextThread>,
     pub context_id: ContextId,
 }
 
@@ -601,26 +595,26 @@ pub struct TextThreadContext {
 impl TextThreadContextHandle {
     // pub fn lookup_key() ->
     pub fn eq_for_key(&self, other: &Self) -> bool {
-        self.context == other.context
+        self.text_thread == other.text_thread
     }
 
     pub fn hash_for_key<H: Hasher>(&self, state: &mut H) {
-        self.context.hash(state)
+        self.text_thread.hash(state)
     }
 
     pub fn title(&self, cx: &App) -> SharedString {
-        self.context.read(cx).summary().or_default()
+        self.text_thread.read(cx).summary().or_default()
     }
 
-    fn load(self, cx: &App) -> Task<Option<(AgentContext, Vec<Entity<Buffer>>)>> {
+    fn load(self, cx: &App) -> Task<Option<AgentContext>> {
         let title = self.title(cx);
-        let text = self.context.read(cx).to_xml(cx);
+        let text = self.text_thread.read(cx).to_xml(cx);
         let context = AgentContext::TextThread(TextThreadContext {
             title,
             text: text.into(),
             handle: self,
         });
-        Task::ready(Some((context, vec![])))
+        Task::ready(Some(context))
     }
 }
 
@@ -666,7 +660,7 @@ impl RulesContextHandle {
         self,
         prompt_store: &Option<Entity<PromptStore>>,
         cx: &App,
-    ) -> Task<Option<(AgentContext, Vec<Entity<Buffer>>)>> {
+    ) -> Task<Option<AgentContext>> {
         let Some(prompt_store) = prompt_store.as_ref() else {
             return Task::ready(None);
         };
@@ -685,7 +679,7 @@ impl RulesContextHandle {
                 title,
                 text,
             });
-            Some((context, vec![]))
+            Some(context)
         })
     }
 }
@@ -748,32 +742,21 @@ impl ImageContext {
         }
     }
 
-    pub fn load(self, cx: &App) -> Task<Option<(AgentContext, Vec<Entity<Buffer>>)>> {
+    pub fn load(self, cx: &App) -> Task<Option<AgentContext>> {
         cx.background_spawn(async move {
             self.image_task.clone().await;
-            Some((AgentContext::Image(self), vec![]))
+            Some(AgentContext::Image(self))
         })
     }
 }
 
-#[derive(Debug, Clone, Default)]
-pub struct ContextLoadResult {
-    pub loaded_context: LoadedContext,
-    pub referenced_buffers: HashSet<Entity<Buffer>>,
-}
-
 #[derive(Debug, Clone, Default)]
 pub struct LoadedContext {
-    pub contexts: Vec<AgentContext>,
     pub text: String,
     pub images: Vec<LanguageModelImage>,
 }
 
 impl LoadedContext {
-    pub fn is_empty(&self) -> bool {
-        self.text.is_empty() && self.images.is_empty()
-    }
-
     pub fn add_to_request_message(&self, request_message: &mut LanguageModelRequestMessage) {
         if !self.text.is_empty() {
             request_message
@@ -804,7 +787,7 @@ pub fn load_context(
     project: &Entity<Project>,
     prompt_store: &Option<Entity<PromptStore>>,
     cx: &mut App,
-) -> Task<ContextLoadResult> {
+) -> Task<LoadedContext> {
     let load_tasks: Vec<_> = contexts
         .into_iter()
         .map(|context| match context {
@@ -823,16 +806,7 @@ pub fn load_context(
     cx.background_spawn(async move {
         let load_results = future::join_all(load_tasks).await;
 
-        let mut contexts = Vec::new();
         let mut text = String::new();
-        let mut referenced_buffers = HashSet::default();
-        for context in load_results {
-            let Some((context, buffers)) = context else {
-                continue;
-            };
-            contexts.push(context);
-            referenced_buffers.extend(buffers);
-        }
 
         let mut file_context = Vec::new();
         let mut directory_context = Vec::new();
@@ -843,7 +817,7 @@ pub fn load_context(
         let mut text_thread_context = Vec::new();
         let mut rules_context = Vec::new();
         let mut images = Vec::new();
-        for context in &contexts {
+        for context in load_results.into_iter().flatten() {
             match context {
                 AgentContext::File(context) => file_context.push(context),
                 AgentContext::Directory(context) => directory_context.push(context),
@@ -868,14 +842,7 @@ pub fn load_context(
             && text_thread_context.is_empty()
             && rules_context.is_empty()
         {
-            return ContextLoadResult {
-                loaded_context: LoadedContext {
-                    contexts,
-                    text,
-                    images,
-                },
-                referenced_buffers,
-            };
+            return LoadedContext { text, images };
         }
 
         text.push_str(
@@ -961,14 +928,7 @@ pub fn load_context(
 
         text.push_str("</context>\n");
 
-        ContextLoadResult {
-            loaded_context: LoadedContext {
-                contexts,
-                text,
-                images,
-            },
-            referenced_buffers,
-        }
+        LoadedContext { text, images }
     })
 }
 
@@ -1131,11 +1091,13 @@ mod tests {
 
         assert!(content_len > outline::AUTO_OUTLINE_SIZE);
 
-        let file_context = file_context_for(large_content, cx).await;
+        let file_context = load_context_for("file.txt", large_content, cx).await;
 
         assert!(
-            file_context.is_outline,
-            "Large file should use outline format"
+            file_context
+                .text
+                .contains(&format!("# File outline for {}", path!("test/file.txt"))),
+            "Large files should not get an outline"
         );
 
         assert!(
@@ -1153,29 +1115,38 @@ mod tests {
 
         assert!(content_len < outline::AUTO_OUTLINE_SIZE);
 
-        let file_context = file_context_for(small_content.to_string(), cx).await;
+        let file_context = load_context_for("file.txt", small_content.to_string(), cx).await;
 
         assert!(
-            !file_context.is_outline,
+            !file_context
+                .text
+                .contains(&format!("# File outline for {}", path!("test/file.txt"))),
             "Small files should not get an outline"
         );
 
-        assert_eq!(file_context.text, small_content);
+        assert!(
+            file_context.text.contains(small_content),
+            "Small files should use full content"
+        );
     }
 
-    async fn file_context_for(content: String, cx: &mut TestAppContext) -> FileContext {
+    async fn load_context_for(
+        filename: &str,
+        content: String,
+        cx: &mut TestAppContext,
+    ) -> LoadedContext {
         // Create a test project with the file
         let project = create_test_project(
             cx,
             json!({
-                "file.txt": content,
+                filename: content,
             }),
         )
         .await;
 
         // Open the buffer
         let buffer_path = project
-            .read_with(cx, |project, cx| project.find_project_path("file.txt", cx))
+            .read_with(cx, |project, cx| project.find_project_path(filename, cx))
             .unwrap();
 
         let buffer = project
@@ -1190,16 +1161,5 @@ mod tests {
 
         cx.update(|cx| load_context(vec![context_handle], &project, &None, cx))
             .await
-            .loaded_context
-            .contexts
-            .into_iter()
-            .find_map(|ctx| {
-                if let AgentContext::File(file_ctx) = ctx {
-                    Some(file_ctx)
-                } else {
-                    None
-                }
-            })
-            .expect("Should have found a file context")
     }
 }

crates/agent_ui/src/context_picker.rs 🔗

@@ -9,6 +9,8 @@ use std::ops::Range;
 use std::path::PathBuf;
 use std::sync::Arc;
 
+use agent::{HistoryEntry, HistoryEntryId, HistoryStore};
+use agent_client_protocol as acp;
 use anyhow::{Result, anyhow};
 use collections::HashSet;
 pub use completion_provider::ContextPickerCompletionProvider;
@@ -27,9 +29,7 @@ use project::ProjectPath;
 use prompt_store::PromptStore;
 use rules_context_picker::{RulesContextEntry, RulesContextPicker};
 use symbol_context_picker::SymbolContextPicker;
-use thread_context_picker::{
-    ThreadContextEntry, ThreadContextPicker, render_thread_context_entry, unordered_thread_entries,
-};
+use thread_context_picker::render_thread_context_entry;
 use ui::{
     ButtonLike, ContextMenu, ContextMenuEntry, ContextMenuItem, Disclosure, TintColor, prelude::*,
 };
@@ -37,12 +37,8 @@ use util::paths::PathStyle;
 use util::rel_path::RelPath;
 use workspace::{Workspace, notifications::NotifyResultExt};
 
-use agent::{
-    ThreadId,
-    context::RULES_ICON,
-    context_store::ContextStore,
-    thread_store::{TextThreadStore, ThreadStore},
-};
+use crate::context_picker::thread_context_picker::ThreadContextPicker;
+use crate::{context::RULES_ICON, context_store::ContextStore};
 
 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
 pub(crate) enum ContextPickerEntry {
@@ -168,17 +164,16 @@ pub(super) struct ContextPicker {
     mode: ContextPickerState,
     workspace: WeakEntity<Workspace>,
     context_store: WeakEntity<ContextStore>,
-    thread_store: Option<WeakEntity<ThreadStore>>,
-    text_thread_store: Option<WeakEntity<TextThreadStore>>,
-    prompt_store: Option<Entity<PromptStore>>,
+    thread_store: Option<WeakEntity<HistoryStore>>,
+    prompt_store: Option<WeakEntity<PromptStore>>,
     _subscriptions: Vec<Subscription>,
 }
 
 impl ContextPicker {
     pub fn new(
         workspace: WeakEntity<Workspace>,
-        thread_store: Option<WeakEntity<ThreadStore>>,
-        text_thread_store: Option<WeakEntity<TextThreadStore>>,
+        thread_store: Option<WeakEntity<HistoryStore>>,
+        prompt_store: Option<WeakEntity<PromptStore>>,
         context_store: WeakEntity<ContextStore>,
         window: &mut Window,
         cx: &mut Context<Self>,
@@ -199,13 +194,6 @@ impl ContextPicker {
             )
             .collect::<Vec<Subscription>>();
 
-        let prompt_store = thread_store.as_ref().and_then(|thread_store| {
-            thread_store
-                .read_with(cx, |thread_store, _cx| thread_store.prompt_store().clone())
-                .ok()
-                .flatten()
-        });
-
         ContextPicker {
             mode: ContextPickerState::Default(ContextMenu::build(
                 window,
@@ -215,7 +203,6 @@ impl ContextPicker {
             workspace,
             context_store,
             thread_store,
-            text_thread_store,
             prompt_store,
             _subscriptions: subscriptions,
         }
@@ -355,17 +342,13 @@ impl ContextPicker {
                     }));
                 }
                 ContextPickerMode::Thread => {
-                    if let Some((thread_store, text_thread_store)) = self
-                        .thread_store
-                        .as_ref()
-                        .zip(self.text_thread_store.as_ref())
-                    {
+                    if let Some(thread_store) = self.thread_store.clone() {
                         self.mode = ContextPickerState::Thread(cx.new(|cx| {
                             ThreadContextPicker::new(
-                                thread_store.clone(),
-                                text_thread_store.clone(),
+                                thread_store,
                                 context_picker.clone(),
                                 self.context_store.clone(),
+                                self.workspace.clone(),
                                 window,
                                 cx,
                             )
@@ -480,16 +463,23 @@ impl ContextPicker {
 
     fn add_recent_thread(
         &self,
-        entry: ThreadContextEntry,
-        window: &mut Window,
+        entry: HistoryEntry,
+        _window: &mut Window,
         cx: &mut Context<Self>,
     ) -> Task<Result<()>> {
         let Some(context_store) = self.context_store.upgrade() else {
             return Task::ready(Err(anyhow!("context store not available")));
         };
+        let Some(project) = self
+            .workspace
+            .upgrade()
+            .map(|workspace| workspace.read(cx).project().clone())
+        else {
+            return Task::ready(Err(anyhow!("project not available")));
+        };
 
         match entry {
-            ThreadContextEntry::Thread { id, .. } => {
+            HistoryEntry::AcpThread(thread) => {
                 let Some(thread_store) = self
                     .thread_store
                     .as_ref()
@@ -497,28 +487,28 @@ impl ContextPicker {
                 else {
                     return Task::ready(Err(anyhow!("thread store not available")));
                 };
-
-                let open_thread_task =
-                    thread_store.update(cx, |this, cx| this.open_thread(&id, window, cx));
+                let load_thread_task =
+                    agent::load_agent_thread(thread.id, thread_store, project, cx);
                 cx.spawn(async move |this, cx| {
-                    let thread = open_thread_task.await?;
+                    let thread = load_thread_task.await?;
                     context_store.update(cx, |context_store, cx| {
                         context_store.add_thread(thread, true, cx);
                     })?;
                     this.update(cx, |_this, cx| cx.notify())
                 })
             }
-            ThreadContextEntry::Context { path, .. } => {
-                let Some(text_thread_store) = self
-                    .text_thread_store
+            HistoryEntry::TextThread(thread) => {
+                let Some(thread_store) = self
+                    .thread_store
                     .as_ref()
                     .and_then(|thread_store| thread_store.upgrade())
                 else {
                     return Task::ready(Err(anyhow!("text thread store not available")));
                 };
 
-                let task = text_thread_store
-                    .update(cx, |this, cx| this.open_local_context(path.clone(), cx));
+                let task = thread_store.update(cx, |this, cx| {
+                    this.load_text_thread(thread.path.clone(), cx)
+                });
                 cx.spawn(async move |this, cx| {
                     let thread = task.await?;
                     context_store.update(cx, |context_store, cx| {
@@ -542,7 +532,6 @@ impl ContextPicker {
         recent_context_picker_entries_with_store(
             context_store,
             self.thread_store.clone(),
-            self.text_thread_store.clone(),
             workspace,
             None,
             cx,
@@ -599,12 +588,12 @@ pub(crate) enum RecentEntry {
         project_path: ProjectPath,
         path_prefix: Arc<RelPath>,
     },
-    Thread(ThreadContextEntry),
+    Thread(HistoryEntry),
 }
 
 pub(crate) fn available_context_picker_entries(
-    prompt_store: &Option<Entity<PromptStore>>,
-    thread_store: &Option<WeakEntity<ThreadStore>>,
+    prompt_store: &Option<WeakEntity<PromptStore>>,
+    thread_store: &Option<WeakEntity<HistoryStore>>,
     workspace: &Entity<Workspace>,
     cx: &mut App,
 ) -> Vec<ContextPickerEntry> {
@@ -617,7 +606,11 @@ pub(crate) fn available_context_picker_entries(
         .read(cx)
         .active_item(cx)
         .and_then(|item| item.downcast::<Editor>())
-        .is_some_and(|editor| editor.update(cx, |editor, cx| editor.has_non_empty_selection(cx)));
+        .is_some_and(|editor| {
+            editor.update(cx, |editor, cx| {
+                editor.has_non_empty_selection(&editor.display_snapshot(cx))
+            })
+        });
     if has_selection {
         entries.push(ContextPickerEntry::Action(
             ContextPickerAction::AddSelections,
@@ -639,8 +632,7 @@ pub(crate) fn available_context_picker_entries(
 
 fn recent_context_picker_entries_with_store(
     context_store: Entity<ContextStore>,
-    thread_store: Option<WeakEntity<ThreadStore>>,
-    text_thread_store: Option<WeakEntity<TextThreadStore>>,
+    thread_store: Option<WeakEntity<HistoryStore>>,
     workspace: Entity<Workspace>,
     exclude_path: Option<ProjectPath>,
     cx: &App,
@@ -657,22 +649,14 @@ fn recent_context_picker_entries_with_store(
 
     let exclude_threads = context_store.read(cx).thread_ids();
 
-    recent_context_picker_entries(
-        thread_store,
-        text_thread_store,
-        workspace,
-        &exclude_paths,
-        exclude_threads,
-        cx,
-    )
+    recent_context_picker_entries(thread_store, workspace, &exclude_paths, exclude_threads, cx)
 }
 
 pub(crate) fn recent_context_picker_entries(
-    thread_store: Option<WeakEntity<ThreadStore>>,
-    text_thread_store: Option<WeakEntity<TextThreadStore>>,
+    thread_store: Option<WeakEntity<HistoryStore>>,
     workspace: Entity<Workspace>,
     exclude_paths: &HashSet<PathBuf>,
-    _exclude_threads: &HashSet<ThreadId>,
+    exclude_threads: &HashSet<acp::SessionId>,
     cx: &App,
 ) -> Vec<RecentEntry> {
     let mut recent = Vec::with_capacity(6);
@@ -698,30 +682,21 @@ pub(crate) fn recent_context_picker_entries(
             }),
     );
 
-    if let Some((thread_store, text_thread_store)) = thread_store
-        .and_then(|store| store.upgrade())
-        .zip(text_thread_store.and_then(|store| store.upgrade()))
-    {
-        let mut threads = unordered_thread_entries(thread_store, text_thread_store, cx)
-            .filter(|(_, thread)| match thread {
-                ThreadContextEntry::Thread { .. } => false,
-                ThreadContextEntry::Context { .. } => true,
-            })
-            .collect::<Vec<_>>();
-
-        const RECENT_COUNT: usize = 2;
-        if threads.len() > RECENT_COUNT {
-            threads.select_nth_unstable_by_key(RECENT_COUNT - 1, |(updated_at, _)| {
-                std::cmp::Reverse(*updated_at)
-            });
-            threads.truncate(RECENT_COUNT);
-        }
-        threads.sort_unstable_by_key(|(updated_at, _)| std::cmp::Reverse(*updated_at));
-
+    if let Some(thread_store) = thread_store.and_then(|store| store.upgrade()) {
+        const RECENT_THREADS_COUNT: usize = 2;
         recent.extend(
-            threads
-                .into_iter()
-                .map(|(_, thread)| RecentEntry::Thread(thread)),
+            thread_store
+                .read(cx)
+                .recently_opened_entries(cx)
+                .iter()
+                .filter(|e| match e.id() {
+                    HistoryEntryId::AcpThread(session_id) => !exclude_threads.contains(&session_id),
+                    HistoryEntryId::TextThread(path) => {
+                        !exclude_paths.contains(&path.to_path_buf())
+                    }
+                })
+                .take(RECENT_THREADS_COUNT)
+                .map(|thread| RecentEntry::Thread(thread.clone())),
         );
     }
 
@@ -754,7 +729,7 @@ pub(crate) fn selection_ranges(
     };
 
     editor.update(cx, |editor, cx| {
-        let selections = editor.selections.all_adjusted(cx);
+        let selections = editor.selections.all_adjusted(&editor.display_snapshot(cx));
 
         let buffer = editor.buffer().clone().read(cx);
         let snapshot = buffer.snapshot(cx);
@@ -915,17 +890,21 @@ impl MentionLink {
         )
     }
 
-    pub fn for_thread(thread: &ThreadContextEntry) -> String {
+    pub fn for_thread(thread: &HistoryEntry) -> String {
         match thread {
-            ThreadContextEntry::Thread { id, title } => {
-                format!("[@{}]({}:{})", title, Self::THREAD, id)
+            HistoryEntry::AcpThread(thread) => {
+                format!("[@{}]({}:{})", thread.title, Self::THREAD, thread.id)
             }
-            ThreadContextEntry::Context { path, title } => {
-                let filename = path.file_name().unwrap_or_default().to_string_lossy();
+            HistoryEntry::TextThread(thread) => {
+                let filename = thread
+                    .path
+                    .file_name()
+                    .unwrap_or_default()
+                    .to_string_lossy();
                 let escaped_filename = urlencoding::encode(&filename);
                 format!(
                     "[@{}]({}:{}{})",
-                    title,
+                    thread.title,
                     Self::THREAD,
                     Self::TEXT_THREAD_URL_PREFIX,
                     escaped_filename

crates/agent_ui/src/context_picker/completion_provider.rs 🔗

@@ -3,7 +3,7 @@ use std::path::{Path, PathBuf};
 use std::sync::Arc;
 use std::sync::atomic::AtomicBool;
 
-use agent::context_store::ContextStore;
+use agent::{HistoryEntry, HistoryStore};
 use anyhow::Result;
 use editor::{CompletionProvider, Editor, ExcerptId, ToOffset as _};
 use file_icons::FileIcons;
@@ -15,8 +15,8 @@ use language::{Buffer, CodeLabel, CodeLabelBuilder, HighlightId};
 use lsp::CompletionContext;
 use project::lsp_store::SymbolLocation;
 use project::{
-    Completion, CompletionDisplayOptions, CompletionIntent, CompletionResponse, ProjectPath,
-    Symbol, WorktreeId,
+    Completion, CompletionDisplayOptions, CompletionIntent, CompletionResponse, Project,
+    ProjectPath, Symbol, WorktreeId,
 };
 use prompt_store::PromptStore;
 use rope::Point;
@@ -27,10 +27,9 @@ use util::paths::PathStyle;
 use util::rel_path::RelPath;
 use workspace::Workspace;
 
-use agent::{
-    Thread,
+use crate::{
     context::{AgentContextHandle, AgentContextKey, RULES_ICON},
-    thread_store::{TextThreadStore, ThreadStore},
+    context_store::ContextStore,
 };
 
 use super::fetch_context_picker::fetch_url_content;
@@ -38,7 +37,7 @@ use super::file_context_picker::{FileMatch, search_files};
 use super::rules_context_picker::{RulesContextEntry, search_rules};
 use super::symbol_context_picker::SymbolMatch;
 use super::symbol_context_picker::search_symbols;
-use super::thread_context_picker::{ThreadContextEntry, ThreadMatch, search_threads};
+use super::thread_context_picker::search_threads;
 use super::{
     ContextPickerAction, ContextPickerEntry, ContextPickerMode, MentionLink, RecentEntry,
     available_context_picker_entries, recent_context_picker_entries_with_store, selection_ranges,
@@ -48,7 +47,8 @@ use crate::message_editor::ContextCreasesAddon;
 pub(crate) enum Match {
     File(FileMatch),
     Symbol(SymbolMatch),
-    Thread(ThreadMatch),
+    Thread(HistoryEntry),
+    RecentThread(HistoryEntry),
     Fetch(SharedString),
     Rules(RulesContextEntry),
     Entry(EntryMatch),
@@ -65,6 +65,7 @@ impl Match {
             Match::File(file) => file.mat.score,
             Match::Entry(mode) => mode.mat.as_ref().map(|mat| mat.score).unwrap_or(1.),
             Match::Thread(_) => 1.,
+            Match::RecentThread(_) => 1.,
             Match::Symbol(_) => 1.,
             Match::Fetch(_) => 1.,
             Match::Rules(_) => 1.,
@@ -77,9 +78,8 @@ fn search(
     query: String,
     cancellation_flag: Arc<AtomicBool>,
     recent_entries: Vec<RecentEntry>,
-    prompt_store: Option<Entity<PromptStore>>,
-    thread_store: Option<WeakEntity<ThreadStore>>,
-    text_thread_context_store: Option<WeakEntity<assistant_context::ContextStore>>,
+    prompt_store: Option<WeakEntity<PromptStore>>,
+    thread_store: Option<WeakEntity<HistoryStore>>,
     workspace: Entity<Workspace>,
     cx: &mut App,
 ) -> Task<Vec<Match>> {
@@ -107,13 +107,9 @@ fn search(
         }
 
         Some(ContextPickerMode::Thread) => {
-            if let Some((thread_store, context_store)) = thread_store
-                .as_ref()
-                .and_then(|t| t.upgrade())
-                .zip(text_thread_context_store.as_ref().and_then(|t| t.upgrade()))
-            {
+            if let Some(thread_store) = thread_store.as_ref().and_then(|t| t.upgrade()) {
                 let search_threads_task =
-                    search_threads(query, cancellation_flag, thread_store, context_store, cx);
+                    search_threads(query, cancellation_flag, &thread_store, cx);
                 cx.background_spawn(async move {
                     search_threads_task
                         .await
@@ -135,8 +131,8 @@ fn search(
         }
 
         Some(ContextPickerMode::Rules) => {
-            if let Some(prompt_store) = prompt_store.as_ref() {
-                let search_rules_task = search_rules(query, cancellation_flag, prompt_store, cx);
+            if let Some(prompt_store) = prompt_store.as_ref().and_then(|p| p.upgrade()) {
+                let search_rules_task = search_rules(query, cancellation_flag, &prompt_store, cx);
                 cx.background_spawn(async move {
                     search_rules_task
                         .await
@@ -169,12 +165,7 @@ fn search(
                             },
                             is_recent: true,
                         }),
-                        super::RecentEntry::Thread(thread_context_entry) => {
-                            Match::Thread(ThreadMatch {
-                                thread: thread_context_entry,
-                                is_recent: true,
-                            })
-                        }
+                        super::RecentEntry::Thread(entry) => Match::RecentThread(entry),
                     })
                     .collect::<Vec<_>>();
 
@@ -245,8 +236,8 @@ fn search(
 pub struct ContextPickerCompletionProvider {
     workspace: WeakEntity<Workspace>,
     context_store: WeakEntity<ContextStore>,
-    thread_store: Option<WeakEntity<ThreadStore>>,
-    text_thread_store: Option<WeakEntity<TextThreadStore>>,
+    thread_store: Option<WeakEntity<HistoryStore>>,
+    prompt_store: Option<WeakEntity<PromptStore>>,
     editor: WeakEntity<Editor>,
     excluded_buffer: Option<WeakEntity<Buffer>>,
 }
@@ -255,8 +246,8 @@ impl ContextPickerCompletionProvider {
     pub fn new(
         workspace: WeakEntity<Workspace>,
         context_store: WeakEntity<ContextStore>,
-        thread_store: Option<WeakEntity<ThreadStore>>,
-        text_thread_store: Option<WeakEntity<TextThreadStore>>,
+        thread_store: Option<WeakEntity<HistoryStore>>,
+        prompt_store: Option<WeakEntity<PromptStore>>,
         editor: WeakEntity<Editor>,
         exclude_buffer: Option<WeakEntity<Buffer>>,
     ) -> Self {
@@ -264,7 +255,7 @@ impl ContextPickerCompletionProvider {
             workspace,
             context_store,
             thread_store,
-            text_thread_store,
+            prompt_store,
             editor,
             excluded_buffer: exclude_buffer,
         }
@@ -408,14 +399,14 @@ impl ContextPickerCompletionProvider {
     }
 
     fn completion_for_thread(
-        thread_entry: ThreadContextEntry,
+        thread_entry: HistoryEntry,
         excerpt_id: ExcerptId,
         source_range: Range<Anchor>,
         recent: bool,
         editor: Entity<Editor>,
         context_store: Entity<ContextStore>,
-        thread_store: Entity<ThreadStore>,
-        text_thread_store: Entity<TextThreadStore>,
+        thread_store: Entity<HistoryStore>,
+        project: Entity<Project>,
     ) -> Completion {
         let icon_for_completion = if recent {
             IconName::HistoryRerun
@@ -442,18 +433,16 @@ impl ContextPickerCompletionProvider {
                 editor,
                 context_store.clone(),
                 move |window, cx| match &thread_entry {
-                    ThreadContextEntry::Thread { id, .. } => {
-                        let thread_id = id.clone();
+                    HistoryEntry::AcpThread(thread) => {
                         let context_store = context_store.clone();
-                        let thread_store = thread_store.clone();
+                        let load_thread_task = agent::load_agent_thread(
+                            thread.id.clone(),
+                            thread_store.clone(),
+                            project.clone(),
+                            cx,
+                        );
                         window.spawn::<_, Option<_>>(cx, async move |cx| {
-                            let thread: Entity<Thread> = thread_store
-                                .update_in(cx, |thread_store, window, cx| {
-                                    thread_store.open_thread(&thread_id, window, cx)
-                                })
-                                .ok()?
-                                .await
-                                .log_err()?;
+                            let thread = load_thread_task.await.log_err()?;
                             let context = context_store
                                 .update(cx, |context_store, cx| {
                                     context_store.add_thread(thread, false, cx)
@@ -462,13 +451,13 @@ impl ContextPickerCompletionProvider {
                             Some(context)
                         })
                     }
-                    ThreadContextEntry::Context { path, .. } => {
-                        let path = path.clone();
+                    HistoryEntry::TextThread(thread) => {
+                        let path = thread.path.clone();
                         let context_store = context_store.clone();
-                        let text_thread_store = text_thread_store.clone();
+                        let thread_store = thread_store.clone();
                         cx.spawn::<_, Option<_>>(async move |cx| {
-                            let thread = text_thread_store
-                                .update(cx, |store, cx| store.open_local_context(path, cx))
+                            let thread = thread_store
+                                .update(cx, |store, cx| store.load_text_thread(path, cx))
                                 .ok()?
                                 .await
                                 .log_err()?;
@@ -782,7 +771,7 @@ impl CompletionProvider for ContextPickerCompletionProvider {
             ..snapshot.anchor_after(state.source_range.end);
 
         let thread_store = self.thread_store.clone();
-        let text_thread_store = self.text_thread_store.clone();
+        let prompt_store = self.prompt_store.clone();
         let editor = self.editor.clone();
         let http_client = workspace.read(cx).client().http_client();
         let path_style = workspace.read(cx).path_style(cx);
@@ -800,19 +789,11 @@ impl CompletionProvider for ContextPickerCompletionProvider {
         let recent_entries = recent_context_picker_entries_with_store(
             context_store.clone(),
             thread_store.clone(),
-            text_thread_store.clone(),
             workspace.clone(),
             excluded_path.clone(),
             cx,
         );
 
-        let prompt_store = thread_store.as_ref().and_then(|thread_store| {
-            thread_store
-                .read_with(cx, |thread_store, _cx| thread_store.prompt_store().clone())
-                .ok()
-                .flatten()
-        });
-
         let search_task = search(
             mode,
             query,
@@ -820,14 +801,14 @@ impl CompletionProvider for ContextPickerCompletionProvider {
             recent_entries,
             prompt_store,
             thread_store.clone(),
-            text_thread_store.clone(),
             workspace.clone(),
             cx,
         );
+        let project = workspace.read(cx).project().downgrade();
 
         cx.spawn(async move |_, cx| {
             let matches = search_task.await;
-            let Some(editor) = editor.upgrade() else {
+            let Some((editor, project)) = editor.upgrade().zip(project.upgrade()) else {
                 return Ok(Vec::new());
             };
 
@@ -868,25 +849,32 @@ impl CompletionProvider for ContextPickerCompletionProvider {
                             workspace.clone(),
                             cx,
                         ),
-
-                        Match::Thread(ThreadMatch {
-                            thread, is_recent, ..
-                        }) => {
+                        Match::Thread(thread) => {
                             let thread_store = thread_store.as_ref().and_then(|t| t.upgrade())?;
-                            let text_thread_store =
-                                text_thread_store.as_ref().and_then(|t| t.upgrade())?;
                             Some(Self::completion_for_thread(
                                 thread,
                                 excerpt_id,
                                 source_range.clone(),
-                                is_recent,
+                                false,
                                 editor.clone(),
                                 context_store.clone(),
                                 thread_store,
-                                text_thread_store,
+                                project.clone(),
+                            ))
+                        }
+                        Match::RecentThread(thread) => {
+                            let thread_store = thread_store.as_ref().and_then(|t| t.upgrade())?;
+                            Some(Self::completion_for_thread(
+                                thread,
+                                excerpt_id,
+                                source_range.clone(),
+                                true,
+                                editor.clone(),
+                                context_store.clone(),
+                                thread_store,
+                                project.clone(),
                             ))
                         }
-
                         Match::Rules(user_rules) => Some(Self::completion_for_rules(
                             user_rules,
                             excerpt_id,
@@ -1289,7 +1277,7 @@ mod tests {
             editor
         });
 
-        let context_store = cx.new(|_| ContextStore::new(project.downgrade(), None));
+        let context_store = cx.new(|_| ContextStore::new(project.downgrade()));
 
         let editor_entity = editor.downgrade();
         editor.update_in(&mut cx, |editor, window, cx| {

crates/agent_ui/src/context_picker/fetch_context_picker.rs 🔗

@@ -2,7 +2,6 @@ use std::cell::RefCell;
 use std::rc::Rc;
 use std::sync::Arc;
 
-use agent::context_store::ContextStore;
 use anyhow::{Context as _, Result, bail};
 use futures::AsyncReadExt as _;
 use gpui::{App, DismissEvent, Entity, FocusHandle, Focusable, Task, WeakEntity};
@@ -12,7 +11,7 @@ use picker::{Picker, PickerDelegate};
 use ui::{Context, ListItem, Window, prelude::*};
 use workspace::Workspace;
 
-use crate::context_picker::ContextPicker;
+use crate::{context_picker::ContextPicker, context_store::ContextStore};
 
 pub struct FetchContextPicker {
     picker: Entity<Picker<FetchContextPickerDelegate>>,

crates/agent_ui/src/context_picker/file_context_picker.rs 🔗

@@ -12,8 +12,10 @@ use ui::{ListItem, Tooltip, prelude::*};
 use util::{ResultExt as _, paths::PathStyle, rel_path::RelPath};
 use workspace::Workspace;
 
-use crate::context_picker::ContextPicker;
-use agent::context_store::{ContextStore, FileInclusion};
+use crate::{
+    context_picker::ContextPicker,
+    context_store::{ContextStore, FileInclusion},
+};
 
 pub struct FileContextPicker {
     picker: Entity<Picker<FileContextPickerDelegate>>,

crates/agent_ui/src/context_picker/rules_context_picker.rs 🔗

@@ -7,9 +7,11 @@ use prompt_store::{PromptId, PromptStore, UserPromptId};
 use ui::{ListItem, prelude::*};
 use util::ResultExt as _;
 
-use crate::context_picker::ContextPicker;
-use agent::context::RULES_ICON;
-use agent::context_store::{self, ContextStore};
+use crate::{
+    context::RULES_ICON,
+    context_picker::ContextPicker,
+    context_store::{self, ContextStore},
+};
 
 pub struct RulesContextPicker {
     picker: Entity<Picker<RulesContextPickerDelegate>>,
@@ -17,7 +19,7 @@ pub struct RulesContextPicker {
 
 impl RulesContextPicker {
     pub fn new(
-        prompt_store: Entity<PromptStore>,
+        prompt_store: WeakEntity<PromptStore>,
         context_picker: WeakEntity<ContextPicker>,
         context_store: WeakEntity<context_store::ContextStore>,
         window: &mut Window,
@@ -49,7 +51,7 @@ pub struct RulesContextEntry {
 }
 
 pub struct RulesContextPickerDelegate {
-    prompt_store: Entity<PromptStore>,
+    prompt_store: WeakEntity<PromptStore>,
     context_picker: WeakEntity<ContextPicker>,
     context_store: WeakEntity<context_store::ContextStore>,
     matches: Vec<RulesContextEntry>,
@@ -58,7 +60,7 @@ pub struct RulesContextPickerDelegate {
 
 impl RulesContextPickerDelegate {
     pub fn new(
-        prompt_store: Entity<PromptStore>,
+        prompt_store: WeakEntity<PromptStore>,
         context_picker: WeakEntity<ContextPicker>,
         context_store: WeakEntity<context_store::ContextStore>,
     ) -> Self {
@@ -102,12 +104,10 @@ impl PickerDelegate for RulesContextPickerDelegate {
         window: &mut Window,
         cx: &mut Context<Picker<Self>>,
     ) -> Task<()> {
-        let search_task = search_rules(
-            query,
-            Arc::new(AtomicBool::default()),
-            &self.prompt_store,
-            cx,
-        );
+        let Some(prompt_store) = self.prompt_store.upgrade() else {
+            return Task::ready(());
+        };
+        let search_task = search_rules(query, Arc::new(AtomicBool::default()), &prompt_store, cx);
         cx.spawn_in(window, async move |this, cx| {
             let matches = search_task.await;
             this.update(cx, |this, cx| {

crates/agent_ui/src/context_picker/symbol_context_picker.rs 🔗

@@ -15,9 +15,9 @@ use ui::{ListItem, prelude::*};
 use util::ResultExt as _;
 use workspace::Workspace;
 
-use crate::context_picker::ContextPicker;
-use agent::context::AgentContextHandle;
-use agent::context_store::ContextStore;
+use crate::{
+    context::AgentContextHandle, context_picker::ContextPicker, context_store::ContextStore,
+};
 
 pub struct SymbolContextPicker {
     picker: Entity<Picker<SymbolContextPickerDelegate>>,

crates/agent_ui/src/context_picker/thread_context_picker.rs 🔗

@@ -1,19 +1,16 @@
-use std::path::Path;
 use std::sync::Arc;
 use std::sync::atomic::AtomicBool;
 
-use chrono::{DateTime, Utc};
+use crate::{
+    context_picker::ContextPicker,
+    context_store::{self, ContextStore},
+};
+use agent::{HistoryEntry, HistoryStore};
 use fuzzy::StringMatchCandidate;
 use gpui::{App, DismissEvent, Entity, FocusHandle, Focusable, Task, WeakEntity};
 use picker::{Picker, PickerDelegate};
 use ui::{ListItem, prelude::*};
-
-use crate::context_picker::ContextPicker;
-use agent::{
-    ThreadId,
-    context_store::{self, ContextStore},
-    thread_store::{TextThreadStore, ThreadStore},
-};
+use workspace::Workspace;
 
 pub struct ThreadContextPicker {
     picker: Entity<Picker<ThreadContextPickerDelegate>>,
@@ -21,18 +18,18 @@ pub struct ThreadContextPicker {
 
 impl ThreadContextPicker {
     pub fn new(
-        thread_store: WeakEntity<ThreadStore>,
-        text_thread_context_store: WeakEntity<TextThreadStore>,
+        thread_store: WeakEntity<HistoryStore>,
         context_picker: WeakEntity<ContextPicker>,
         context_store: WeakEntity<context_store::ContextStore>,
+        workspace: WeakEntity<Workspace>,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) -> Self {
         let delegate = ThreadContextPickerDelegate::new(
             thread_store,
-            text_thread_context_store,
             context_picker,
             context_store,
+            workspace,
         );
         let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
 
@@ -52,48 +49,27 @@ impl Render for ThreadContextPicker {
     }
 }
 
-#[derive(Debug, Clone)]
-pub enum ThreadContextEntry {
-    Thread {
-        id: ThreadId,
-        title: SharedString,
-    },
-    Context {
-        path: Arc<Path>,
-        title: SharedString,
-    },
-}
-
-impl ThreadContextEntry {
-    pub fn title(&self) -> &SharedString {
-        match self {
-            Self::Thread { title, .. } => title,
-            Self::Context { title, .. } => title,
-        }
-    }
-}
-
 pub struct ThreadContextPickerDelegate {
-    thread_store: WeakEntity<ThreadStore>,
-    text_thread_store: WeakEntity<TextThreadStore>,
+    thread_store: WeakEntity<HistoryStore>,
     context_picker: WeakEntity<ContextPicker>,
     context_store: WeakEntity<context_store::ContextStore>,
-    matches: Vec<ThreadContextEntry>,
+    workspace: WeakEntity<Workspace>,
+    matches: Vec<HistoryEntry>,
     selected_index: usize,
 }
 
 impl ThreadContextPickerDelegate {
     pub fn new(
-        thread_store: WeakEntity<ThreadStore>,
-        text_thread_store: WeakEntity<TextThreadStore>,
+        thread_store: WeakEntity<HistoryStore>,
         context_picker: WeakEntity<ContextPicker>,
         context_store: WeakEntity<context_store::ContextStore>,
+        workspace: WeakEntity<Workspace>,
     ) -> Self {
         ThreadContextPickerDelegate {
             thread_store,
             context_picker,
             context_store,
-            text_thread_store,
+            workspace,
             matches: Vec::new(),
             selected_index: 0,
         }
@@ -130,25 +106,15 @@ impl PickerDelegate for ThreadContextPickerDelegate {
         window: &mut Window,
         cx: &mut Context<Picker<Self>>,
     ) -> Task<()> {
-        let Some((thread_store, text_thread_context_store)) = self
-            .thread_store
-            .upgrade()
-            .zip(self.text_thread_store.upgrade())
-        else {
+        let Some(thread_store) = self.thread_store.upgrade() else {
             return Task::ready(());
         };
 
-        let search_task = search_threads(
-            query,
-            Arc::new(AtomicBool::default()),
-            thread_store,
-            text_thread_context_store,
-            cx,
-        );
+        let search_task = search_threads(query, Arc::new(AtomicBool::default()), &thread_store, cx);
         cx.spawn_in(window, async move |this, cx| {
             let matches = search_task.await;
             this.update(cx, |this, cx| {
-                this.delegate.matches = matches.into_iter().map(|mat| mat.thread).collect();
+                this.delegate.matches = matches;
                 this.delegate.selected_index = 0;
                 cx.notify();
             })
@@ -156,21 +122,29 @@ impl PickerDelegate for ThreadContextPickerDelegate {
         })
     }
 
-    fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
-        let Some(entry) = self.matches.get(self.selected_index) else {
+    fn confirm(&mut self, _secondary: bool, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
+        let Some(project) = self
+            .workspace
+            .upgrade()
+            .map(|w| w.read(cx).project().clone())
+        else {
+            return;
+        };
+        let Some((entry, thread_store)) = self
+            .matches
+            .get(self.selected_index)
+            .zip(self.thread_store.upgrade())
+        else {
             return;
         };
 
         match entry {
-            ThreadContextEntry::Thread { id, .. } => {
-                let Some(thread_store) = self.thread_store.upgrade() else {
-                    return;
-                };
-                let open_thread_task =
-                    thread_store.update(cx, |this, cx| this.open_thread(id, window, cx));
+            HistoryEntry::AcpThread(thread) => {
+                let load_thread_task =
+                    agent::load_agent_thread(thread.id.clone(), thread_store, project, cx);
 
                 cx.spawn(async move |this, cx| {
-                    let thread = open_thread_task.await?;
+                    let thread = load_thread_task.await?;
                     this.update(cx, |this, cx| {
                         this.delegate
                             .context_store
@@ -182,12 +156,10 @@ impl PickerDelegate for ThreadContextPickerDelegate {
                 })
                 .detach_and_log_err(cx);
             }
-            ThreadContextEntry::Context { path, .. } => {
-                let Some(text_thread_store) = self.text_thread_store.upgrade() else {
-                    return;
-                };
-                let task = text_thread_store
-                    .update(cx, |this, cx| this.open_local_context(path.clone(), cx));
+            HistoryEntry::TextThread(thread) => {
+                let task = thread_store.update(cx, |this, cx| {
+                    this.load_text_thread(thread.path.clone(), cx)
+                });
 
                 cx.spawn(async move |this, cx| {
                     let thread = task.await?;
@@ -229,17 +201,17 @@ impl PickerDelegate for ThreadContextPickerDelegate {
 }
 
 pub fn render_thread_context_entry(
-    entry: &ThreadContextEntry,
+    entry: &HistoryEntry,
     context_store: WeakEntity<ContextStore>,
     cx: &mut App,
 ) -> Div {
     let is_added = match entry {
-        ThreadContextEntry::Thread { id, .. } => context_store
+        HistoryEntry::AcpThread(thread) => context_store
             .upgrade()
-            .is_some_and(|ctx_store| ctx_store.read(cx).includes_thread(id)),
-        ThreadContextEntry::Context { path, .. } => context_store
+            .is_some_and(|ctx_store| ctx_store.read(cx).includes_thread(&thread.id)),
+        HistoryEntry::TextThread(thread) => context_store
             .upgrade()
-            .is_some_and(|ctx_store| ctx_store.read(cx).includes_text_thread(path)),
+            .is_some_and(|ctx_store| ctx_store.read(cx).includes_text_thread(&thread.path)),
     };
 
     h_flex()
@@ -271,91 +243,38 @@ pub fn render_thread_context_entry(
         })
 }
 
-#[derive(Clone)]
-pub struct ThreadMatch {
-    pub thread: ThreadContextEntry,
-    pub is_recent: bool,
-}
-
-pub fn unordered_thread_entries(
-    thread_store: Entity<ThreadStore>,
-    text_thread_store: Entity<TextThreadStore>,
-    cx: &App,
-) -> impl Iterator<Item = (DateTime<Utc>, ThreadContextEntry)> {
-    let threads = thread_store
-        .read(cx)
-        .reverse_chronological_threads()
-        .map(|thread| {
-            (
-                thread.updated_at,
-                ThreadContextEntry::Thread {
-                    id: thread.id.clone(),
-                    title: thread.summary.clone(),
-                },
-            )
-        });
-
-    let text_threads = text_thread_store
-        .read(cx)
-        .unordered_contexts()
-        .map(|context| {
-            (
-                context.mtime.to_utc(),
-                ThreadContextEntry::Context {
-                    path: context.path.clone(),
-                    title: context.title.clone(),
-                },
-            )
-        });
-
-    threads.chain(text_threads)
-}
-
 pub(crate) fn search_threads(
     query: String,
     cancellation_flag: Arc<AtomicBool>,
-    thread_store: Entity<ThreadStore>,
-    text_thread_store: Entity<TextThreadStore>,
+    thread_store: &Entity<HistoryStore>,
     cx: &mut App,
-) -> Task<Vec<ThreadMatch>> {
-    let mut threads =
-        unordered_thread_entries(thread_store, text_thread_store, cx).collect::<Vec<_>>();
-    threads.sort_unstable_by_key(|(updated_at, _)| std::cmp::Reverse(*updated_at));
+) -> Task<Vec<HistoryEntry>> {
+    let threads = thread_store.read(cx).entries().collect();
+    if query.is_empty() {
+        return Task::ready(threads);
+    }
 
     let executor = cx.background_executor().clone();
     cx.background_spawn(async move {
-        if query.is_empty() {
-            threads
-                .into_iter()
-                .map(|(_, thread)| ThreadMatch {
-                    thread,
-                    is_recent: false,
-                })
-                .collect()
-        } else {
-            let candidates = threads
-                .iter()
-                .enumerate()
-                .map(|(id, (_, thread))| StringMatchCandidate::new(id, thread.title()))
-                .collect::<Vec<_>>();
-            let matches = fuzzy::match_strings(
-                &candidates,
-                &query,
-                false,
-                true,
-                100,
-                &cancellation_flag,
-                executor,
-            )
-            .await;
+        let candidates = threads
+            .iter()
+            .enumerate()
+            .map(|(id, thread)| StringMatchCandidate::new(id, thread.title()))
+            .collect::<Vec<_>>();
+        let matches = fuzzy::match_strings(
+            &candidates,
+            &query,
+            false,
+            true,
+            100,
+            &cancellation_flag,
+            executor,
+        )
+        .await;
 
-            matches
-                .into_iter()
-                .map(|mat| ThreadMatch {
-                    thread: threads[mat.candidate_id].1.clone(),
-                    is_recent: false,
-                })
-                .collect()
-        }
+        matches
+            .into_iter()
+            .map(|mat| threads[mat.candidate_id].clone())
+            .collect()
     })
 }

crates/agent/src/context_store.rs → crates/agent_ui/src/context_store.rs 🔗

@@ -1,14 +1,11 @@
-use crate::{
-    context::{
-        AgentContextHandle, AgentContextKey, ContextId, ContextKind, DirectoryContextHandle,
-        FetchedUrlContext, FileContextHandle, ImageContext, RulesContextHandle,
-        SelectionContextHandle, SymbolContextHandle, TextThreadContextHandle, ThreadContextHandle,
-    },
-    thread::{MessageId, Thread, ThreadId},
-    thread_store::ThreadStore,
+use crate::context::{
+    AgentContextHandle, AgentContextKey, ContextId, ContextKind, DirectoryContextHandle,
+    FetchedUrlContext, FileContextHandle, ImageContext, RulesContextHandle, SelectionContextHandle,
+    SymbolContextHandle, TextThreadContextHandle, ThreadContextHandle,
 };
+use agent_client_protocol as acp;
 use anyhow::{Context as _, Result, anyhow};
-use assistant_context::AssistantContext;
+use assistant_text_thread::TextThread;
 use collections::{HashSet, IndexSet};
 use futures::{self, FutureExt};
 use gpui::{App, Context, Entity, EventEmitter, Image, SharedString, Task, WeakEntity};
@@ -29,10 +26,9 @@ use text::{Anchor, OffsetRangeExt};
 
 pub struct ContextStore {
     project: WeakEntity<Project>,
-    thread_store: Option<WeakEntity<ThreadStore>>,
     next_context_id: ContextId,
     context_set: IndexSet<AgentContextKey>,
-    context_thread_ids: HashSet<ThreadId>,
+    context_thread_ids: HashSet<acp::SessionId>,
     context_text_thread_paths: HashSet<Arc<Path>>,
 }
 
@@ -43,13 +39,9 @@ pub enum ContextStoreEvent {
 impl EventEmitter<ContextStoreEvent> for ContextStore {}
 
 impl ContextStore {
-    pub fn new(
-        project: WeakEntity<Project>,
-        thread_store: Option<WeakEntity<ThreadStore>>,
-    ) -> Self {
+    pub fn new(project: WeakEntity<Project>) -> Self {
         Self {
             project,
-            thread_store,
             next_context_id: ContextId::zero(),
             context_set: IndexSet::default(),
             context_thread_ids: HashSet::default(),
@@ -67,29 +59,6 @@ impl ContextStore {
         cx.notify();
     }
 
-    pub fn new_context_for_thread(
-        &self,
-        thread: &Thread,
-        exclude_messages_from_id: Option<MessageId>,
-    ) -> Vec<AgentContextHandle> {
-        let existing_context = thread
-            .messages()
-            .take_while(|message| exclude_messages_from_id.is_none_or(|id| message.id != id))
-            .flat_map(|message| {
-                message
-                    .loaded_context
-                    .contexts
-                    .iter()
-                    .map(|context| AgentContextKey(context.handle()))
-            })
-            .collect::<HashSet<_>>();
-        self.context_set
-            .iter()
-            .filter(|context| !existing_context.contains(context))
-            .map(|entry| entry.0.clone())
-            .collect::<Vec<_>>()
-    }
-
     pub fn add_file_from_path(
         &mut self,
         project_path: ProjectPath,
@@ -209,7 +178,7 @@ impl ContextStore {
 
     pub fn add_thread(
         &mut self,
-        thread: Entity<Thread>,
+        thread: Entity<agent::Thread>,
         remove_if_exists: bool,
         cx: &mut Context<Self>,
     ) -> Option<AgentContextHandle> {
@@ -231,13 +200,13 @@ impl ContextStore {
 
     pub fn add_text_thread(
         &mut self,
-        context: Entity<AssistantContext>,
+        text_thread: Entity<TextThread>,
         remove_if_exists: bool,
         cx: &mut Context<Self>,
     ) -> Option<AgentContextHandle> {
         let context_id = self.next_context_id.post_inc();
         let context = AgentContextHandle::TextThread(TextThreadContextHandle {
-            context,
+            text_thread,
             context_id,
         });
 
@@ -384,21 +353,15 @@ impl ContextStore {
                     );
                 };
             }
-            SuggestedContext::Thread { thread, name: _ } => {
-                if let Some(thread) = thread.upgrade() {
-                    let context_id = self.next_context_id.post_inc();
-                    self.insert_context(
-                        AgentContextHandle::Thread(ThreadContextHandle { thread, context_id }),
-                        cx,
-                    );
-                }
-            }
-            SuggestedContext::TextThread { context, name: _ } => {
-                if let Some(context) = context.upgrade() {
+            SuggestedContext::TextThread {
+                text_thread,
+                name: _,
+            } => {
+                if let Some(text_thread) = text_thread.upgrade() {
                     let context_id = self.next_context_id.post_inc();
                     self.insert_context(
                         AgentContextHandle::TextThread(TextThreadContextHandle {
-                            context,
+                            text_thread,
                             context_id,
                         }),
                         cx,
@@ -410,20 +373,20 @@ impl ContextStore {
 
     fn insert_context(&mut self, context: AgentContextHandle, cx: &mut Context<Self>) -> bool {
         match &context {
-            AgentContextHandle::Thread(thread_context) => {
-                if let Some(thread_store) = self.thread_store.clone() {
-                    thread_context.thread.update(cx, |thread, cx| {
-                        thread.start_generating_detailed_summary_if_needed(thread_store, cx);
-                    });
-                    self.context_thread_ids
-                        .insert(thread_context.thread.read(cx).id().clone());
-                } else {
-                    return false;
-                }
-            }
+            // AgentContextHandle::Thread(thread_context) => {
+            //     if let Some(thread_store) = self.thread_store.clone() {
+            //         thread_context.thread.update(cx, |thread, cx| {
+            //             thread.start_generating_detailed_summary_if_needed(thread_store, cx);
+            //         });
+            //         self.context_thread_ids
+            //             .insert(thread_context.thread.read(cx).id().clone());
+            //     } else {
+            //         return false;
+            //     }
+            // }
             AgentContextHandle::TextThread(text_thread_context) => {
                 self.context_text_thread_paths
-                    .extend(text_thread_context.context.read(cx).path().cloned());
+                    .extend(text_thread_context.text_thread.read(cx).path().cloned());
             }
             _ => {}
         }
@@ -445,7 +408,7 @@ impl ContextStore {
                         .remove(thread_context.thread.read(cx).id());
                 }
                 AgentContextHandle::TextThread(text_thread_context) => {
-                    if let Some(path) = text_thread_context.context.read(cx).path() {
+                    if let Some(path) = text_thread_context.text_thread.read(cx).path() {
                         self.context_text_thread_paths.remove(path);
                     }
                 }
@@ -514,7 +477,7 @@ impl ContextStore {
         })
     }
 
-    pub fn includes_thread(&self, thread_id: &ThreadId) -> bool {
+    pub fn includes_thread(&self, thread_id: &acp::SessionId) -> bool {
         self.context_thread_ids.contains(thread_id)
     }
 
@@ -547,9 +510,9 @@ impl ContextStore {
                 }
                 AgentContextHandle::Directory(_)
                 | AgentContextHandle::Symbol(_)
+                | AgentContextHandle::Thread(_)
                 | AgentContextHandle::Selection(_)
                 | AgentContextHandle::FetchedUrl(_)
-                | AgentContextHandle::Thread(_)
                 | AgentContextHandle::TextThread(_)
                 | AgentContextHandle::Rules(_)
                 | AgentContextHandle::Image(_) => None,
@@ -557,7 +520,7 @@ impl ContextStore {
             .collect()
     }
 
-    pub fn thread_ids(&self) -> &HashSet<ThreadId> {
+    pub fn thread_ids(&self) -> &HashSet<acp::SessionId> {
         &self.context_thread_ids
     }
 }
@@ -569,13 +532,9 @@ pub enum SuggestedContext {
         icon_path: Option<SharedString>,
         buffer: WeakEntity<Buffer>,
     },
-    Thread {
-        name: SharedString,
-        thread: WeakEntity<Thread>,
-    },
     TextThread {
         name: SharedString,
-        context: WeakEntity<AssistantContext>,
+        text_thread: WeakEntity<TextThread>,
     },
 }
 
@@ -583,7 +542,6 @@ impl SuggestedContext {
     pub fn name(&self) -> &SharedString {
         match self {
             Self::File { name, .. } => name,
-            Self::Thread { name, .. } => name,
             Self::TextThread { name, .. } => name,
         }
     }
@@ -591,7 +549,6 @@ impl SuggestedContext {
     pub fn icon_path(&self) -> Option<SharedString> {
         match self {
             Self::File { icon_path, .. } => icon_path.clone(),
-            Self::Thread { .. } => None,
             Self::TextThread { .. } => None,
         }
     }
@@ -599,7 +556,6 @@ impl SuggestedContext {
     pub fn kind(&self) -> ContextKind {
         match self {
             Self::File { .. } => ContextKind::File,
-            Self::Thread { .. } => ContextKind::Thread,
             Self::TextThread { .. } => ContextKind::TextThread,
         }
     }

crates/agent_ui/src/context_strip.rs 🔗

@@ -4,12 +4,11 @@ use crate::{
     context_picker::ContextPicker,
     ui::{AddedContext, ContextPill},
 };
-use agent::context_store::SuggestedContext;
-use agent::{
+use crate::{
     context::AgentContextHandle,
-    context_store::ContextStore,
-    thread_store::{TextThreadStore, ThreadStore},
+    context_store::{ContextStore, SuggestedContext},
 };
+use agent::HistoryStore;
 use collections::HashSet;
 use editor::Editor;
 use gpui::{
@@ -18,6 +17,7 @@ use gpui::{
 };
 use itertools::Itertools;
 use project::ProjectItem;
+use prompt_store::PromptStore;
 use rope::Point;
 use std::rc::Rc;
 use text::ToPoint as _;
@@ -33,7 +33,7 @@ pub struct ContextStrip {
     focus_handle: FocusHandle,
     suggest_context_kind: SuggestContextKind,
     workspace: WeakEntity<Workspace>,
-    thread_store: Option<WeakEntity<ThreadStore>>,
+    prompt_store: Option<WeakEntity<PromptStore>>,
     _subscriptions: Vec<Subscription>,
     focused_index: Option<usize>,
     children_bounds: Option<Vec<Bounds<Pixels>>>,
@@ -44,8 +44,8 @@ impl ContextStrip {
     pub fn new(
         context_store: Entity<ContextStore>,
         workspace: WeakEntity<Workspace>,
-        thread_store: Option<WeakEntity<ThreadStore>>,
-        text_thread_store: Option<WeakEntity<TextThreadStore>>,
+        thread_store: Option<WeakEntity<HistoryStore>>,
+        prompt_store: Option<WeakEntity<PromptStore>>,
         context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
         suggest_context_kind: SuggestContextKind,
         model_usage_context: ModelUsageContext,
@@ -56,7 +56,7 @@ impl ContextStrip {
             ContextPicker::new(
                 workspace.clone(),
                 thread_store.clone(),
-                text_thread_store,
+                prompt_store.clone(),
                 context_store.downgrade(),
                 window,
                 cx,
@@ -79,7 +79,7 @@ impl ContextStrip {
             focus_handle,
             suggest_context_kind,
             workspace,
-            thread_store,
+            prompt_store,
             _subscriptions: subscriptions,
             focused_index: None,
             children_bounds: None,
@@ -96,11 +96,7 @@ impl ContextStrip {
     fn added_contexts(&self, cx: &App) -> Vec<AddedContext> {
         if let Some(workspace) = self.workspace.upgrade() {
             let project = workspace.read(cx).project().read(cx);
-            let prompt_store = self
-                .thread_store
-                .as_ref()
-                .and_then(|thread_store| thread_store.upgrade())
-                .and_then(|thread_store| thread_store.read(cx).prompt_store().as_ref());
+            let prompt_store = self.prompt_store.as_ref().and_then(|p| p.upgrade());
 
             let current_model = self.model_usage_context.language_model(cx);
 
@@ -110,7 +106,7 @@ impl ContextStrip {
                 .flat_map(|context| {
                     AddedContext::new_pending(
                         context.clone(),
-                        prompt_store,
+                        prompt_store.as_ref(),
                         project,
                         current_model.as_ref(),
                         cx,
@@ -136,19 +132,19 @@ impl ContextStrip {
         let workspace = self.workspace.upgrade()?;
         let panel = workspace.read(cx).panel::<AgentPanel>(cx)?.read(cx);
 
-        if let Some(active_context_editor) = panel.active_context_editor() {
-            let context = active_context_editor.read(cx).context();
-            let weak_context = context.downgrade();
-            let context = context.read(cx);
-            let path = context.path()?;
+        if let Some(active_text_thread_editor) = panel.active_text_thread_editor() {
+            let text_thread = active_text_thread_editor.read(cx).text_thread();
+            let weak_text_thread = text_thread.downgrade();
+            let text_thread = text_thread.read(cx);
+            let path = text_thread.path()?;
 
             if self.context_store.read(cx).includes_text_thread(path) {
                 return None;
             }
 
             Some(SuggestedContext::TextThread {
-                name: context.summary().or_default(),
-                context: weak_context,
+                name: text_thread.summary().or_default(),
+                text_thread: weak_text_thread,
             })
         } else {
             None
@@ -336,10 +332,10 @@ impl ContextStrip {
             AgentContextHandle::TextThread(text_thread_context) => {
                 workspace.update(cx, |workspace, cx| {
                     if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
-                        let context = text_thread_context.context.clone();
+                        let context = text_thread_context.text_thread.clone();
                         window.defer(cx, move |window, cx| {
                             panel.update(cx, |panel, cx| {
-                                panel.open_prompt_editor(context, window, cx)
+                                panel.open_text_thread(context, window, cx)
                             });
                         });
                     }
@@ -487,12 +483,11 @@ impl Render for ContextStrip {
                             .style(ui::ButtonStyle::Filled),
                         {
                             let focus_handle = focus_handle.clone();
-                            move |window, cx| {
+                            move |_window, cx| {
                                 Tooltip::for_action_in(
                                     "Add Context",
                                     &ToggleContextPicker,
                                     &focus_handle,
-                                    window,
                                     cx,
                                 )
                             }
@@ -562,12 +557,11 @@ impl Render for ContextStrip {
                             .icon_size(IconSize::Small)
                             .tooltip({
                                 let focus_handle = focus_handle.clone();
-                                move |window, cx| {
+                                move |_window, cx| {
                                     Tooltip::for_action_in(
                                         "Remove All Context",
                                         &RemoveAllContext,
                                         &focus_handle,
-                                        window,
                                         cx,
                                     )
                                 }

crates/agent_ui/src/inline_assistant.rs 🔗

@@ -7,13 +7,11 @@ use std::sync::Arc;
 use crate::{
     AgentPanel,
     buffer_codegen::{BufferCodegen, CodegenAlternative, CodegenEvent},
+    context_store::ContextStore,
     inline_prompt_editor::{CodegenStatus, InlineAssistId, PromptEditor, PromptEditorEvent},
     terminal_inline_assistant::TerminalInlineAssistant,
 };
-use agent::{
-    context_store::ContextStore,
-    thread_store::{TextThreadStore, ThreadStore},
-};
+use agent::HistoryStore;
 use agent_settings::AgentSettings;
 use anyhow::{Context as _, Result};
 use client::telemetry::Telemetry;
@@ -209,24 +207,21 @@ impl InlineAssistant {
         window: &mut Window,
         cx: &mut App,
     ) {
-        let is_assistant2_enabled = !DisableAiSettings::get_global(cx).disable_ai;
+        let is_ai_enabled = !DisableAiSettings::get_global(cx).disable_ai;
 
         if let Some(editor) = item.act_as::<Editor>(cx) {
             editor.update(cx, |editor, cx| {
-                if is_assistant2_enabled {
+                if is_ai_enabled {
                     let panel = workspace.read(cx).panel::<AgentPanel>(cx);
                     let thread_store = panel
                         .as_ref()
                         .map(|agent_panel| agent_panel.read(cx).thread_store().downgrade());
-                    let text_thread_store = panel
-                        .map(|agent_panel| agent_panel.read(cx).text_thread_store().downgrade());
 
                     editor.add_code_action_provider(
                         Rc::new(AssistantCodeActionProvider {
                             editor: cx.entity().downgrade(),
                             workspace: workspace.downgrade(),
                             thread_store,
-                            text_thread_store,
                         }),
                         window,
                         cx,
@@ -283,7 +278,6 @@ impl InlineAssistant {
 
         let prompt_store = agent_panel.prompt_store().as_ref().cloned();
         let thread_store = Some(agent_panel.thread_store().downgrade());
-        let text_thread_store = Some(agent_panel.text_thread_store().downgrade());
         let context_store = agent_panel.inline_assist_context_store().clone();
 
         let handle_assist =
@@ -297,7 +291,6 @@ impl InlineAssistant {
                             workspace.project().downgrade(),
                             prompt_store,
                             thread_store,
-                            text_thread_store,
                             action.prompt.clone(),
                             window,
                             cx,
@@ -312,7 +305,6 @@ impl InlineAssistant {
                             workspace.project().downgrade(),
                             prompt_store,
                             thread_store,
-                            text_thread_store,
                             action.prompt.clone(),
                             window,
                             cx,
@@ -365,16 +357,18 @@ impl InlineAssistant {
         context_store: Entity<ContextStore>,
         project: WeakEntity<Project>,
         prompt_store: Option<Entity<PromptStore>>,
-        thread_store: Option<WeakEntity<ThreadStore>>,
-        text_thread_store: Option<WeakEntity<TextThreadStore>>,
+        thread_store: Option<WeakEntity<HistoryStore>>,
         initial_prompt: Option<String>,
         window: &mut Window,
         cx: &mut App,
     ) {
         let (snapshot, initial_selections, newest_selection) = editor.update(cx, |editor, cx| {
-            let selections = editor.selections.all::<Point>(cx);
-            let newest_selection = editor.selections.newest::<Point>(cx);
-            (editor.snapshot(window, cx), selections, newest_selection)
+            let snapshot = editor.snapshot(window, cx);
+            let selections = editor.selections.all::<Point>(&snapshot.display_snapshot);
+            let newest_selection = editor
+                .selections
+                .newest::<Point>(&snapshot.display_snapshot);
+            (snapshot, selections, newest_selection)
         });
 
         // Check if there is already an inline assistant that contains the
@@ -517,7 +511,7 @@ impl InlineAssistant {
                     context_store.clone(),
                     workspace.clone(),
                     thread_store.clone(),
-                    text_thread_store.clone(),
+                    prompt_store.as_ref().map(|s| s.downgrade()),
                     window,
                     cx,
                 )
@@ -589,8 +583,7 @@ impl InlineAssistant {
         focus: bool,
         workspace: Entity<Workspace>,
         prompt_store: Option<Entity<PromptStore>>,
-        thread_store: Option<WeakEntity<ThreadStore>>,
-        text_thread_store: Option<WeakEntity<TextThreadStore>>,
+        thread_store: Option<WeakEntity<HistoryStore>>,
         window: &mut Window,
         cx: &mut App,
     ) -> InlineAssistId {
@@ -608,7 +601,7 @@ impl InlineAssistant {
         }
 
         let project = workspace.read(cx).project().downgrade();
-        let context_store = cx.new(|_cx| ContextStore::new(project.clone(), thread_store.clone()));
+        let context_store = cx.new(|_cx| ContextStore::new(project.clone()));
 
         let codegen = cx.new(|cx| {
             BufferCodegen::new(
@@ -617,7 +610,7 @@ impl InlineAssistant {
                 initial_transaction_id,
                 context_store.clone(),
                 project,
-                prompt_store,
+                prompt_store.clone(),
                 self.telemetry.clone(),
                 self.prompt_builder.clone(),
                 cx,
@@ -636,7 +629,7 @@ impl InlineAssistant {
                 context_store,
                 workspace.downgrade(),
                 thread_store,
-                text_thread_store,
+                prompt_store.map(|s| s.downgrade()),
                 window,
                 cx,
             )
@@ -808,7 +801,9 @@ impl InlineAssistant {
         if editor.read(cx).selections.count() == 1 {
             let (selection, buffer) = editor.update(cx, |editor, cx| {
                 (
-                    editor.selections.newest::<usize>(cx),
+                    editor
+                        .selections
+                        .newest::<usize>(&editor.display_snapshot(cx)),
                     editor.buffer().read(cx).snapshot(cx),
                 )
             });
@@ -839,7 +834,9 @@ impl InlineAssistant {
         if editor.read(cx).selections.count() == 1 {
             let (selection, buffer) = editor.update(cx, |editor, cx| {
                 (
-                    editor.selections.newest::<usize>(cx),
+                    editor
+                        .selections
+                        .newest::<usize>(&editor.display_snapshot(cx)),
                     editor.buffer().read(cx).snapshot(cx),
                 )
             });
@@ -1511,8 +1508,8 @@ impl InlineAssistant {
             return Some(InlineAssistTarget::Terminal(terminal_view));
         }
 
-        let context_editor = agent_panel
-            .and_then(|panel| panel.read(cx).active_context_editor())
+        let text_thread_editor = agent_panel
+            .and_then(|panel| panel.read(cx).active_text_thread_editor())
             .and_then(|editor| {
                 let editor = &editor.read(cx).editor().clone();
                 if editor.read(cx).is_focused(window) {
@@ -1522,8 +1519,8 @@ impl InlineAssistant {
                 }
             });
 
-        if let Some(context_editor) = context_editor {
-            Some(InlineAssistTarget::Editor(context_editor))
+        if let Some(text_thread_editor) = text_thread_editor {
+            Some(InlineAssistTarget::Editor(text_thread_editor))
         } else if let Some(workspace_editor) = workspace
             .active_item(cx)
             .and_then(|item| item.act_as::<Editor>(cx))
@@ -1773,8 +1770,7 @@ struct InlineAssistDecorations {
 struct AssistantCodeActionProvider {
     editor: WeakEntity<Editor>,
     workspace: WeakEntity<Workspace>,
-    thread_store: Option<WeakEntity<ThreadStore>>,
-    text_thread_store: Option<WeakEntity<TextThreadStore>>,
+    thread_store: Option<WeakEntity<HistoryStore>>,
 }
 
 const ASSISTANT_CODE_ACTION_PROVIDER_ID: &str = "assistant2";
@@ -1846,7 +1842,6 @@ impl CodeActionProvider for AssistantCodeActionProvider {
         let editor = self.editor.clone();
         let workspace = self.workspace.clone();
         let thread_store = self.thread_store.clone();
-        let text_thread_store = self.text_thread_store.clone();
         let prompt_store = PromptStore::global(cx);
         window.spawn(cx, async move |cx| {
             let workspace = workspace.upgrade().context("workspace was released")?;
@@ -1894,7 +1889,6 @@ impl CodeActionProvider for AssistantCodeActionProvider {
                     workspace,
                     prompt_store,
                     thread_store,
-                    text_thread_store,
                     window,
                     cx,
                 );

crates/agent_ui/src/inline_prompt_editor.rs 🔗

@@ -1,7 +1,5 @@
-use agent::{
-    context_store::ContextStore,
-    thread_store::{TextThreadStore, ThreadStore},
-};
+use crate::context_store::ContextStore;
+use agent::HistoryStore;
 use collections::VecDeque;
 use editor::actions::Paste;
 use editor::display_map::EditorMargins;
@@ -16,6 +14,7 @@ use gpui::{
 };
 use language_model::{LanguageModel, LanguageModelRegistry};
 use parking_lot::Mutex;
+use prompt_store::PromptStore;
 use settings::Settings;
 use std::cmp;
 use std::rc::Rc;
@@ -469,12 +468,11 @@ impl<T: 'static> PromptEditor<T> {
                 IconButton::new("stop", IconName::Stop)
                     .icon_color(Color::Error)
                     .shape(IconButtonShape::Square)
-                    .tooltip(move |window, cx| {
+                    .tooltip(move |_window, cx| {
                         Tooltip::with_meta(
                             mode.tooltip_interrupt(),
                             Some(&menu::Cancel),
                             "Changes won't be discarded",
-                            window,
                             cx,
                         )
                     })
@@ -488,12 +486,11 @@ impl<T: 'static> PromptEditor<T> {
                         IconButton::new("restart", IconName::RotateCw)
                             .icon_color(Color::Info)
                             .shape(IconButtonShape::Square)
-                            .tooltip(move |window, cx| {
+                            .tooltip(move |_window, cx| {
                                 Tooltip::with_meta(
                                     mode.tooltip_restart(),
                                     Some(&menu::Confirm),
                                     "Changes will be discarded",
-                                    window,
                                     cx,
                                 )
                             })
@@ -506,8 +503,8 @@ impl<T: 'static> PromptEditor<T> {
                     let accept = IconButton::new("accept", IconName::Check)
                         .icon_color(Color::Info)
                         .shape(IconButtonShape::Square)
-                        .tooltip(move |window, cx| {
-                            Tooltip::for_action(mode.tooltip_accept(), &menu::Confirm, window, cx)
+                        .tooltip(move |_window, cx| {
+                            Tooltip::for_action(mode.tooltip_accept(), &menu::Confirm, cx)
                         })
                         .on_click(cx.listener(|_, _, _, cx| {
                             cx.emit(PromptEditorEvent::ConfirmRequested { execute: false });
@@ -520,11 +517,10 @@ impl<T: 'static> PromptEditor<T> {
                             IconButton::new("confirm", IconName::PlayFilled)
                                 .icon_color(Color::Info)
                                 .shape(IconButtonShape::Square)
-                                .tooltip(|window, cx| {
+                                .tooltip(|_window, cx| {
                                     Tooltip::for_action(
                                         "Execute Generated Command",
                                         &menu::SecondaryConfirm,
-                                        window,
                                         cx,
                                     )
                                 })
@@ -616,13 +612,12 @@ impl<T: 'static> PromptEditor<T> {
                     .shape(IconButtonShape::Square)
                     .tooltip({
                         let focus_handle = self.editor.focus_handle(cx);
-                        move |window, cx| {
+                        move |_window, cx| {
                             cx.new(|cx| {
                                 let mut tooltip = Tooltip::new("Previous Alternative").key_binding(
                                     KeyBinding::for_action_in(
                                         &CyclePreviousInlineAssist,
                                         &focus_handle,
-                                        window,
                                         cx,
                                     ),
                                 );
@@ -658,13 +653,12 @@ impl<T: 'static> PromptEditor<T> {
                     .shape(IconButtonShape::Square)
                     .tooltip({
                         let focus_handle = self.editor.focus_handle(cx);
-                        move |window, cx| {
+                        move |_window, cx| {
                             cx.new(|cx| {
                                 let mut tooltip = Tooltip::new("Next Alternative").key_binding(
                                     KeyBinding::for_action_in(
                                         &CycleNextInlineAssist,
                                         &focus_handle,
-                                        window,
                                         cx,
                                     ),
                                 );
@@ -777,8 +771,8 @@ impl PromptEditor<BufferCodegen> {
         fs: Arc<dyn Fs>,
         context_store: Entity<ContextStore>,
         workspace: WeakEntity<Workspace>,
-        thread_store: Option<WeakEntity<ThreadStore>>,
-        text_thread_store: Option<WeakEntity<TextThreadStore>>,
+        thread_store: Option<WeakEntity<HistoryStore>>,
+        prompt_store: Option<WeakEntity<PromptStore>>,
         window: &mut Window,
         cx: &mut Context<PromptEditor<BufferCodegen>>,
     ) -> PromptEditor<BufferCodegen> {
@@ -823,7 +817,7 @@ impl PromptEditor<BufferCodegen> {
                 workspace.clone(),
                 context_store.downgrade(),
                 thread_store.clone(),
-                text_thread_store.clone(),
+                prompt_store.clone(),
                 prompt_editor_entity,
                 codegen_buffer.as_ref().map(Entity::downgrade),
             ))));
@@ -837,7 +831,7 @@ impl PromptEditor<BufferCodegen> {
                 context_store.clone(),
                 workspace.clone(),
                 thread_store.clone(),
-                text_thread_store.clone(),
+                prompt_store,
                 context_picker_menu_handle.clone(),
                 SuggestContextKind::Thread,
                 ModelUsageContext::InlineAssistant,
@@ -949,8 +943,8 @@ impl PromptEditor<TerminalCodegen> {
         fs: Arc<dyn Fs>,
         context_store: Entity<ContextStore>,
         workspace: WeakEntity<Workspace>,
-        thread_store: Option<WeakEntity<ThreadStore>>,
-        text_thread_store: Option<WeakEntity<TextThreadStore>>,
+        thread_store: Option<WeakEntity<HistoryStore>>,
+        prompt_store: Option<WeakEntity<PromptStore>>,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) -> Self {
@@ -988,7 +982,7 @@ impl PromptEditor<TerminalCodegen> {
                 workspace.clone(),
                 context_store.downgrade(),
                 thread_store.clone(),
-                text_thread_store.clone(),
+                prompt_store.clone(),
                 prompt_editor_entity,
                 None,
             ))));
@@ -1002,7 +996,7 @@ impl PromptEditor<TerminalCodegen> {
                 context_store.clone(),
                 workspace.clone(),
                 thread_store.clone(),
-                text_thread_store.clone(),
+                prompt_store.clone(),
                 context_picker_menu_handle.clone(),
                 SuggestContextKind::Thread,
                 ModelUsageContext::InlineAssistant,

crates/agent_ui/src/message_editor.rs 🔗

@@ -1,31 +1,25 @@
-use agent::{context::AgentContextKey, context_store::ContextStoreEvent};
-use agent_settings::AgentProfileId;
+use std::ops::Range;
+
 use collections::HashMap;
 use editor::display_map::CreaseId;
 use editor::{Addon, AnchorRangeExt, Editor};
-use gpui::{App, Entity, Subscription};
+use gpui::{Entity, Subscription};
 use ui::prelude::*;
 
-use crate::context_picker::crease_for_mention;
-use crate::profile_selector::ProfileProvider;
-use agent::{MessageCrease, Thread, context_store::ContextStore};
-
-impl ProfileProvider for Entity<Thread> {
-    fn profiles_supported(&self, cx: &App) -> bool {
-        self.read(cx)
-            .configured_model()
-            .is_some_and(|model| model.model.supports_tools())
-    }
-
-    fn profile_id(&self, cx: &App) -> AgentProfileId {
-        self.read(cx).profile().id().clone()
-    }
+use crate::{
+    context::{AgentContextHandle, AgentContextKey},
+    context_picker::crease_for_mention,
+    context_store::{ContextStore, ContextStoreEvent},
+};
 
-    fn set_profile(&self, profile_id: AgentProfileId, cx: &mut App) {
-        self.update(cx, |this, cx| {
-            this.set_profile(profile_id, cx);
-        });
-    }
+/// Stored information that can be used to resurrect a context crease when creating an editor for a past message.
+#[derive(Clone, Debug)]
+pub struct MessageCrease {
+    pub range: Range<usize>,
+    pub icon_path: SharedString,
+    pub label: SharedString,
+    /// None for a deserialized message, Some otherwise.
+    pub context: Option<AgentContextHandle>,
 }
 
 #[derive(Default)]

crates/agent_ui/src/profile_selector.rs 🔗

@@ -162,12 +162,11 @@ impl Render for ProfileSelector {
         PickerPopoverMenu::new(
             picker,
             trigger_button,
-            move |window, cx| {
+            move |_window, cx| {
                 Tooltip::for_action_in(
                     "Toggle Profile Menu",
                     &ToggleProfileSelector,
                     &focus_handle,
-                    window,
                     cx,
                 )
             },

crates/agent_ui/src/slash_command_picker.rs 🔗

@@ -155,8 +155,8 @@ impl PickerDelegate for SlashCommandDelegate {
             match command {
                 SlashCommandEntry::Info(info) => {
                     self.active_context_editor
-                        .update(cx, |context_editor, cx| {
-                            context_editor.insert_command(&info.name, window, cx)
+                        .update(cx, |text_thread_editor, cx| {
+                            text_thread_editor.insert_command(&info.name, window, cx)
                         })
                         .ok();
                 }

crates/agent_ui/src/terminal_inline_assistant.rs 🔗

@@ -1,12 +1,12 @@
-use crate::inline_prompt_editor::{
-    CodegenStatus, PromptEditor, PromptEditorEvent, TerminalInlineAssistId,
-};
-use crate::terminal_codegen::{CLEAR_INPUT, CodegenEvent, TerminalCodegen};
-use agent::{
+use crate::{
     context::load_context,
     context_store::ContextStore,
-    thread_store::{TextThreadStore, ThreadStore},
+    inline_prompt_editor::{
+        CodegenStatus, PromptEditor, PromptEditorEvent, TerminalInlineAssistId,
+    },
+    terminal_codegen::{CLEAR_INPUT, CodegenEvent, TerminalCodegen},
 };
+use agent::HistoryStore;
 use agent_settings::AgentSettings;
 use anyhow::{Context as _, Result};
 use client::telemetry::Telemetry;
@@ -74,8 +74,7 @@ impl TerminalInlineAssistant {
         workspace: WeakEntity<Workspace>,
         project: WeakEntity<Project>,
         prompt_store: Option<Entity<PromptStore>>,
-        thread_store: Option<WeakEntity<ThreadStore>>,
-        text_thread_store: Option<WeakEntity<TextThreadStore>>,
+        thread_store: Option<WeakEntity<HistoryStore>>,
         initial_prompt: Option<String>,
         window: &mut Window,
         cx: &mut App,
@@ -88,7 +87,7 @@ impl TerminalInlineAssistant {
                 cx,
             )
         });
-        let context_store = cx.new(|_cx| ContextStore::new(project, thread_store.clone()));
+        let context_store = cx.new(|_cx| ContextStore::new(project));
         let codegen = cx.new(|_| TerminalCodegen::new(terminal, self.telemetry.clone()));
 
         let prompt_editor = cx.new(|cx| {
@@ -101,7 +100,7 @@ impl TerminalInlineAssistant {
                 context_store.clone(),
                 workspace.clone(),
                 thread_store.clone(),
-                text_thread_store.clone(),
+                prompt_store.as_ref().map(|s| s.downgrade()),
                 window,
                 cx,
             )
@@ -282,7 +281,6 @@ impl TerminalInlineAssistant {
 
             context_load_task
                 .await
-                .loaded_context
                 .add_to_request_message(&mut request_message);
 
             request_message.content.push(prompt.into());

crates/agent_ui/src/text_thread_editor.rs 🔗

@@ -1,5 +1,4 @@
 use crate::{
-    QuoteSelection,
     language_model_selector::{LanguageModelSelector, language_model_selector},
     ui::BurnModeTooltip,
 };
@@ -72,13 +71,13 @@ use workspace::{
     pane,
     searchable::{SearchEvent, SearchableItem},
 };
-use zed_actions::agent::ToggleModelSelector;
+use zed_actions::agent::{AddSelectionToThread, ToggleModelSelector};
 
 use crate::{slash_command::SlashCommandCompletionProvider, slash_command_picker};
-use assistant_context::{
-    AssistantContext, CacheStatus, Content, ContextEvent, ContextId, InvokedSlashCommandId,
-    InvokedSlashCommandStatus, Message, MessageId, MessageMetadata, MessageStatus,
-    PendingSlashCommandStatus, ThoughtProcessOutputSection,
+use assistant_text_thread::{
+    CacheStatus, Content, InvokedSlashCommandId, InvokedSlashCommandStatus, Message, MessageId,
+    MessageMetadata, MessageStatus, PendingSlashCommandStatus, TextThread, TextThreadEvent,
+    TextThreadId, ThoughtProcessOutputSection,
 };
 
 actions!(
@@ -127,14 +126,14 @@ pub enum ThoughtProcessStatus {
 }
 
 pub trait AgentPanelDelegate {
-    fn active_context_editor(
+    fn active_text_thread_editor(
         &self,
         workspace: &mut Workspace,
         window: &mut Window,
         cx: &mut Context<Workspace>,
     ) -> Option<Entity<TextThreadEditor>>;
 
-    fn open_saved_context(
+    fn open_local_text_thread(
         &self,
         workspace: &mut Workspace,
         path: Arc<Path>,
@@ -142,10 +141,10 @@ pub trait AgentPanelDelegate {
         cx: &mut Context<Workspace>,
     ) -> Task<Result<()>>;
 
-    fn open_remote_context(
+    fn open_remote_text_thread(
         &self,
         workspace: &mut Workspace,
-        context_id: ContextId,
+        text_thread_id: TextThreadId,
         window: &mut Window,
         cx: &mut Context<Workspace>,
     ) -> Task<Result<Entity<TextThreadEditor>>>;
@@ -178,7 +177,7 @@ struct GlobalAssistantPanelDelegate(Arc<dyn AgentPanelDelegate>);
 impl Global for GlobalAssistantPanelDelegate {}
 
 pub struct TextThreadEditor {
-    context: Entity<AssistantContext>,
+    text_thread: Entity<TextThread>,
     fs: Arc<dyn Fs>,
     slash_commands: Arc<SlashCommandWorkingSet>,
     workspace: WeakEntity<Workspace>,
@@ -224,8 +223,8 @@ impl TextThreadEditor {
         .detach();
     }
 
-    pub fn for_context(
-        context: Entity<AssistantContext>,
+    pub fn for_text_thread(
+        text_thread: Entity<TextThread>,
         fs: Arc<dyn Fs>,
         workspace: WeakEntity<Workspace>,
         project: Entity<Project>,
@@ -234,14 +233,14 @@ impl TextThreadEditor {
         cx: &mut Context<Self>,
     ) -> Self {
         let completion_provider = SlashCommandCompletionProvider::new(
-            context.read(cx).slash_commands().clone(),
+            text_thread.read(cx).slash_commands().clone(),
             Some(cx.entity().downgrade()),
             Some(workspace.clone()),
         );
 
         let editor = cx.new(|cx| {
             let mut editor =
-                Editor::for_buffer(context.read(cx).buffer().clone(), None, window, cx);
+                Editor::for_buffer(text_thread.read(cx).buffer().clone(), None, window, cx);
             editor.disable_scrollbars_and_minimap(window, cx);
             editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
             editor.set_show_line_numbers(false, cx);
@@ -265,18 +264,24 @@ impl TextThreadEditor {
         });
 
         let _subscriptions = vec![
-            cx.observe(&context, |_, _, cx| cx.notify()),
-            cx.subscribe_in(&context, window, Self::handle_context_event),
+            cx.observe(&text_thread, |_, _, cx| cx.notify()),
+            cx.subscribe_in(&text_thread, window, Self::handle_text_thread_event),
             cx.subscribe_in(&editor, window, Self::handle_editor_event),
             cx.subscribe_in(&editor, window, Self::handle_editor_search_event),
             cx.observe_global_in::<SettingsStore>(window, Self::settings_changed),
         ];
 
-        let slash_command_sections = context.read(cx).slash_command_output_sections().to_vec();
-        let thought_process_sections = context.read(cx).thought_process_output_sections().to_vec();
-        let slash_commands = context.read(cx).slash_commands().clone();
+        let slash_command_sections = text_thread
+            .read(cx)
+            .slash_command_output_sections()
+            .to_vec();
+        let thought_process_sections = text_thread
+            .read(cx)
+            .thought_process_output_sections()
+            .to_vec();
+        let slash_commands = text_thread.read(cx).slash_commands().clone();
         let mut this = Self {
-            context,
+            text_thread,
             slash_commands,
             editor,
             lsp_adapter_delegate,
@@ -338,8 +343,8 @@ impl TextThreadEditor {
         });
     }
 
-    pub fn context(&self) -> &Entity<AssistantContext> {
-        &self.context
+    pub fn text_thread(&self) -> &Entity<TextThread> {
+        &self.text_thread
     }
 
     pub fn editor(&self) -> &Entity<Editor> {
@@ -351,9 +356,9 @@ impl TextThreadEditor {
         self.editor.update(cx, |editor, cx| {
             editor.insert(&format!("/{command_name}\n\n"), window, cx)
         });
-        let command = self.context.update(cx, |context, cx| {
-            context.reparse(cx);
-            context.parsed_slash_commands()[0].clone()
+        let command = self.text_thread.update(cx, |text_thread, cx| {
+            text_thread.reparse(cx);
+            text_thread.parsed_slash_commands()[0].clone()
         });
         self.run_command(
             command.source_range,
@@ -376,11 +381,14 @@ impl TextThreadEditor {
 
     fn send_to_model(&mut self, window: &mut Window, cx: &mut Context<Self>) {
         self.last_error = None;
-        if let Some(user_message) = self.context.update(cx, |context, cx| context.assist(cx)) {
+        if let Some(user_message) = self
+            .text_thread
+            .update(cx, |text_thread, cx| text_thread.assist(cx))
+        {
             let new_selection = {
                 let cursor = user_message
                     .start
-                    .to_offset(self.context.read(cx).buffer().read(cx));
+                    .to_offset(self.text_thread.read(cx).buffer().read(cx));
                 cursor..cursor
             };
             self.editor.update(cx, |editor, cx| {
@@ -404,8 +412,8 @@ impl TextThreadEditor {
         self.last_error = None;
 
         if self
-            .context
-            .update(cx, |context, cx| context.cancel_last_assist(cx))
+            .text_thread
+            .update(cx, |text_thread, cx| text_thread.cancel_last_assist(cx))
         {
             return;
         }
@@ -420,20 +428,20 @@ impl TextThreadEditor {
         cx: &mut Context<Self>,
     ) {
         let cursors = self.cursors(cx);
-        self.context.update(cx, |context, cx| {
-            let messages = context
+        self.text_thread.update(cx, |text_thread, cx| {
+            let messages = text_thread
                 .messages_for_offsets(cursors, cx)
                 .into_iter()
                 .map(|message| message.id)
                 .collect();
-            context.cycle_message_roles(messages, cx)
+            text_thread.cycle_message_roles(messages, cx)
         });
     }
 
     fn cursors(&self, cx: &mut App) -> Vec<usize> {
-        let selections = self
-            .editor
-            .update(cx, |editor, cx| editor.selections.all::<usize>(cx));
+        let selections = self.editor.update(cx, |editor, cx| {
+            editor.selections.all::<usize>(&editor.display_snapshot(cx))
+        });
         selections
             .into_iter()
             .map(|selection| selection.head())
@@ -446,7 +454,10 @@ impl TextThreadEditor {
                 editor.transact(window, cx, |editor, window, cx| {
                     editor.change_selections(Default::default(), window, cx, |s| s.try_cancel());
                     let snapshot = editor.buffer().read(cx).snapshot(cx);
-                    let newest_cursor = editor.selections.newest::<Point>(cx).head();
+                    let newest_cursor = editor
+                        .selections
+                        .newest::<Point>(&editor.display_snapshot(cx))
+                        .head();
                     if newest_cursor.column > 0
                         || snapshot
                             .chars_at(newest_cursor)
@@ -489,11 +500,11 @@ impl TextThreadEditor {
         let selections = self.editor.read(cx).selections.disjoint_anchors_arc();
         let mut commands_by_range = HashMap::default();
         let workspace = self.workspace.clone();
-        self.context.update(cx, |context, cx| {
-            context.reparse(cx);
+        self.text_thread.update(cx, |text_thread, cx| {
+            text_thread.reparse(cx);
             for selection in selections.iter() {
                 if let Some(command) =
-                    context.pending_command_for_position(selection.head().text_anchor, cx)
+                    text_thread.pending_command_for_position(selection.head().text_anchor, cx)
                 {
                     commands_by_range
                         .entry(command.source_range.clone())
@@ -531,14 +542,14 @@ impl TextThreadEditor {
         cx: &mut Context<Self>,
     ) {
         if let Some(command) = self.slash_commands.command(name, cx) {
-            let context = self.context.read(cx);
-            let sections = context
+            let text_thread = self.text_thread.read(cx);
+            let sections = text_thread
                 .slash_command_output_sections()
                 .iter()
-                .filter(|section| section.is_valid(context.buffer().read(cx)))
+                .filter(|section| section.is_valid(text_thread.buffer().read(cx)))
                 .cloned()
                 .collect::<Vec<_>>();
-            let snapshot = context.buffer().read(cx).snapshot();
+            let snapshot = text_thread.buffer().read(cx).snapshot();
             let output = command.run(
                 arguments,
                 &sections,
@@ -548,8 +559,8 @@ impl TextThreadEditor {
                 window,
                 cx,
             );
-            self.context.update(cx, |context, cx| {
-                context.insert_command_output(
+            self.text_thread.update(cx, |text_thread, cx| {
+                text_thread.insert_command_output(
                     command_range,
                     name,
                     output,
@@ -560,32 +571,32 @@ impl TextThreadEditor {
         }
     }
 
-    fn handle_context_event(
+    fn handle_text_thread_event(
         &mut self,
-        _: &Entity<AssistantContext>,
-        event: &ContextEvent,
+        _: &Entity<TextThread>,
+        event: &TextThreadEvent,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        let context_editor = cx.entity().downgrade();
+        let text_thread_editor = cx.entity().downgrade();
 
         match event {
-            ContextEvent::MessagesEdited => {
+            TextThreadEvent::MessagesEdited => {
                 self.update_message_headers(cx);
                 self.update_image_blocks(cx);
-                self.context.update(cx, |context, cx| {
-                    context.save(Some(Duration::from_millis(500)), self.fs.clone(), cx);
+                self.text_thread.update(cx, |text_thread, cx| {
+                    text_thread.save(Some(Duration::from_millis(500)), self.fs.clone(), cx);
                 });
             }
-            ContextEvent::SummaryChanged => {
+            TextThreadEvent::SummaryChanged => {
                 cx.emit(EditorEvent::TitleChanged);
-                self.context.update(cx, |context, cx| {
-                    context.save(Some(Duration::from_millis(500)), self.fs.clone(), cx);
+                self.text_thread.update(cx, |text_thread, cx| {
+                    text_thread.save(Some(Duration::from_millis(500)), self.fs.clone(), cx);
                 });
             }
-            ContextEvent::SummaryGenerated => {}
-            ContextEvent::PathChanged { .. } => {}
-            ContextEvent::StartedThoughtProcess(range) => {
+            TextThreadEvent::SummaryGenerated => {}
+            TextThreadEvent::PathChanged { .. } => {}
+            TextThreadEvent::StartedThoughtProcess(range) => {
                 let creases = self.insert_thought_process_output_sections(
                     [(
                         ThoughtProcessOutputSection {
@@ -598,7 +609,7 @@ impl TextThreadEditor {
                 );
                 self.pending_thought_process = Some((creases[0], range.start));
             }
-            ContextEvent::EndedThoughtProcess(end) => {
+            TextThreadEvent::EndedThoughtProcess(end) => {
                 if let Some((crease_id, start)) = self.pending_thought_process.take() {
                     self.editor.update(cx, |editor, cx| {
                         let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
@@ -624,7 +635,7 @@ impl TextThreadEditor {
                     );
                 }
             }
-            ContextEvent::StreamedCompletion => {
+            TextThreadEvent::StreamedCompletion => {
                 self.editor.update(cx, |editor, cx| {
                     if let Some(scroll_position) = self.scroll_position {
                         let snapshot = editor.snapshot(window, cx);
@@ -639,7 +650,7 @@ impl TextThreadEditor {
                     }
                 });
             }
-            ContextEvent::ParsedSlashCommandsUpdated { removed, updated } => {
+            TextThreadEvent::ParsedSlashCommandsUpdated { removed, updated } => {
                 self.editor.update(cx, |editor, cx| {
                     let buffer = editor.buffer().read(cx).snapshot(cx);
                     let (&excerpt_id, _, _) = buffer.as_singleton().unwrap();
@@ -655,12 +666,12 @@ impl TextThreadEditor {
                         updated.iter().map(|command| {
                             let workspace = self.workspace.clone();
                             let confirm_command = Arc::new({
-                                let context_editor = context_editor.clone();
+                                let text_thread_editor = text_thread_editor.clone();
                                 let command = command.clone();
                                 move |window: &mut Window, cx: &mut App| {
-                                    context_editor
-                                        .update(cx, |context_editor, cx| {
-                                            context_editor.run_command(
+                                    text_thread_editor
+                                        .update(cx, |text_thread_editor, cx| {
+                                            text_thread_editor.run_command(
                                                 command.source_range.clone(),
                                                 &command.name,
                                                 &command.arguments,
@@ -710,17 +721,17 @@ impl TextThreadEditor {
                     );
                 })
             }
-            ContextEvent::InvokedSlashCommandChanged { command_id } => {
+            TextThreadEvent::InvokedSlashCommandChanged { command_id } => {
                 self.update_invoked_slash_command(*command_id, window, cx);
             }
-            ContextEvent::SlashCommandOutputSectionAdded { section } => {
+            TextThreadEvent::SlashCommandOutputSectionAdded { section } => {
                 self.insert_slash_command_output_sections([section.clone()], false, window, cx);
             }
-            ContextEvent::Operation(_) => {}
-            ContextEvent::ShowAssistError(error_message) => {
+            TextThreadEvent::Operation(_) => {}
+            TextThreadEvent::ShowAssistError(error_message) => {
                 self.last_error = Some(AssistError::Message(error_message.clone()));
             }
-            ContextEvent::ShowPaymentRequiredError => {
+            TextThreadEvent::ShowPaymentRequiredError => {
                 self.last_error = Some(AssistError::PaymentRequired);
             }
         }
@@ -733,14 +744,14 @@ impl TextThreadEditor {
         cx: &mut Context<Self>,
     ) {
         if let Some(invoked_slash_command) =
-            self.context.read(cx).invoked_slash_command(&command_id)
+            self.text_thread.read(cx).invoked_slash_command(&command_id)
             && let InvokedSlashCommandStatus::Finished = invoked_slash_command.status
         {
             let run_commands_in_ranges = invoked_slash_command.run_commands_in_ranges.clone();
             for range in run_commands_in_ranges {
-                let commands = self.context.update(cx, |context, cx| {
-                    context.reparse(cx);
-                    context
+                let commands = self.text_thread.update(cx, |text_thread, cx| {
+                    text_thread.reparse(cx);
+                    text_thread
                         .pending_commands_for_range(range.clone(), cx)
                         .to_vec()
                 });
@@ -761,7 +772,7 @@ impl TextThreadEditor {
 
         self.editor.update(cx, |editor, cx| {
             if let Some(invoked_slash_command) =
-                self.context.read(cx).invoked_slash_command(&command_id)
+                self.text_thread.read(cx).invoked_slash_command(&command_id)
             {
                 if let InvokedSlashCommandStatus::Finished = invoked_slash_command.status {
                     let buffer = editor.buffer().read(cx).snapshot(cx);
@@ -788,7 +799,7 @@ impl TextThreadEditor {
                     let buffer = editor.buffer().read(cx).snapshot(cx);
                     let (&excerpt_id, _buffer_id, _buffer_snapshot) =
                         buffer.as_singleton().unwrap();
-                    let context = self.context.downgrade();
+                    let context = self.text_thread.downgrade();
                     let range = buffer
                         .anchor_range_in_excerpt(excerpt_id, invoked_slash_command.range.clone())
                         .unwrap();
@@ -1018,7 +1029,7 @@ impl TextThreadEditor {
 
             let render_block = |message: MessageMetadata| -> RenderBlock {
                 Arc::new({
-                    let context = self.context.clone();
+                    let text_thread = self.text_thread.clone();
 
                     move |cx| {
                         let message_id = MessageId(message.timestamp);
@@ -1082,20 +1093,19 @@ impl TextThreadEditor {
                                             .child(label)
                                             .children(spinner),
                                     )
-                                    .tooltip(|window, cx| {
+                                    .tooltip(|_window, cx| {
                                         Tooltip::with_meta(
                                             "Toggle message role",
                                             None,
                                             "Available roles: You (User), Agent, System",
-                                            window,
                                             cx,
                                         )
                                     })
                                     .on_click({
-                                        let context = context.clone();
+                                        let text_thread = text_thread.clone();
                                         move |_, _window, cx| {
-                                            context.update(cx, |context, cx| {
-                                                context.cycle_message_roles(
+                                            text_thread.update(cx, |text_thread, cx| {
+                                                text_thread.cycle_message_roles(
                                                     HashSet::from_iter(Some(message_id)),
                                                     cx,
                                                 )
@@ -1123,12 +1133,11 @@ impl TextThreadEditor {
                                                     .size(IconSize::XSmall)
                                                     .color(Color::Hint),
                                             )
-                                            .tooltip(|window, cx| {
+                                            .tooltip(|_window, cx| {
                                                 Tooltip::with_meta(
                                                     "Context Cached",
                                                     None,
                                                     "Large messages cached to optimize performance",
-                                                    window,
                                                     cx,
                                                 )
                                             })
@@ -1158,11 +1167,11 @@ impl TextThreadEditor {
                                         .icon_position(IconPosition::Start)
                                         .tooltip(Tooltip::text("View Details"))
                                         .on_click({
-                                            let context = context.clone();
+                                            let text_thread = text_thread.clone();
                                             let error = error.clone();
                                             move |_, _window, cx| {
-                                                context.update(cx, |_, cx| {
-                                                    cx.emit(ContextEvent::ShowAssistError(
+                                                text_thread.update(cx, |_, cx| {
+                                                    cx.emit(TextThreadEvent::ShowAssistError(
                                                         error.clone(),
                                                     ));
                                                 });
@@ -1205,7 +1214,7 @@ impl TextThreadEditor {
             };
             let mut new_blocks = vec![];
             let mut block_index_to_message = vec![];
-            for message in self.context.read(cx).messages(cx) {
+            for message in self.text_thread.read(cx).messages(cx) {
                 if blocks_to_remove.remove(&message.id).is_some() {
                     // This is an old message that we might modify.
                     let Some((meta, block_id)) = old_blocks.get_mut(&message.id) else {
@@ -1246,13 +1255,21 @@ impl TextThreadEditor {
     ) -> Option<(String, bool)> {
         const CODE_FENCE_DELIMITER: &str = "```";
 
-        let context_editor = context_editor_view.read(cx).editor.clone();
-        context_editor.update(cx, |context_editor, cx| {
-            if context_editor.selections.newest::<Point>(cx).is_empty() {
-                let snapshot = context_editor.buffer().read(cx).snapshot(cx);
+        let text_thread_editor = context_editor_view.read(cx).editor.clone();
+        text_thread_editor.update(cx, |text_thread_editor, cx| {
+            let display_map = text_thread_editor.display_snapshot(cx);
+            if text_thread_editor
+                .selections
+                .newest::<Point>(&display_map)
+                .is_empty()
+            {
+                let snapshot = text_thread_editor.buffer().read(cx).snapshot(cx);
                 let (_, _, snapshot) = snapshot.as_singleton()?;
 
-                let head = context_editor.selections.newest::<Point>(cx).head();
+                let head = text_thread_editor
+                    .selections
+                    .newest::<Point>(&display_map)
+                    .head();
                 let offset = snapshot.point_to_offset(head);
 
                 let surrounding_code_block_range = find_surrounding_code_block(snapshot, offset)?;
@@ -1269,8 +1286,8 @@ impl TextThreadEditor {
 
                 (!text.is_empty()).then_some((text, true))
             } else {
-                let selection = context_editor.selections.newest_adjusted(cx);
-                let buffer = context_editor.buffer().read(cx).snapshot(cx);
+                let selection = text_thread_editor.selections.newest_adjusted(&display_map);
+                let buffer = text_thread_editor.buffer().read(cx).snapshot(cx);
                 let selected_text = buffer.text_for_range(selection.range()).collect::<String>();
 
                 (!selected_text.is_empty()).then_some((selected_text, false))
@@ -1288,7 +1305,7 @@ impl TextThreadEditor {
             return;
         };
         let Some(context_editor_view) =
-            agent_panel_delegate.active_context_editor(workspace, window, cx)
+            agent_panel_delegate.active_text_thread_editor(workspace, window, cx)
         else {
             return;
         };
@@ -1316,7 +1333,7 @@ impl TextThreadEditor {
         let result = maybe!({
             let agent_panel_delegate = <dyn AgentPanelDelegate>::try_global(cx)?;
             let context_editor_view =
-                agent_panel_delegate.active_context_editor(workspace, window, cx)?;
+                agent_panel_delegate.active_text_thread_editor(workspace, window, cx)?;
             Self::get_selection_or_code_block(&context_editor_view, cx)
         });
         let Some((text, is_code_block)) = result else {
@@ -1353,7 +1370,7 @@ impl TextThreadEditor {
             return;
         };
         let Some(context_editor_view) =
-            agent_panel_delegate.active_context_editor(workspace, window, cx)
+            agent_panel_delegate.active_text_thread_editor(workspace, window, cx)
         else {
             return;
         };
@@ -1439,7 +1456,7 @@ impl TextThreadEditor {
 
     pub fn quote_selection(
         workspace: &mut Workspace,
-        _: &QuoteSelection,
+        _: &AddSelectionToThread,
         window: &mut Window,
         cx: &mut Context<Workspace>,
     ) {
@@ -1457,7 +1474,7 @@ impl TextThreadEditor {
             let selections = editor.update(cx, |editor, cx| {
                 editor
                     .selections
-                    .all_adjusted(cx)
+                    .all_adjusted(&editor.display_snapshot(cx))
                     .into_iter()
                     .filter_map(|s| {
                         (!s.is_empty())
@@ -1489,7 +1506,10 @@ impl TextThreadEditor {
         self.editor.update(cx, |editor, cx| {
             editor.insert("\n", window, cx);
             for (text, crease_title) in creases {
-                let point = editor.selections.newest::<Point>(cx).head();
+                let point = editor
+                    .selections
+                    .newest::<Point>(&editor.display_snapshot(cx))
+                    .head();
                 let start_row = MultiBufferRow(point.row);
 
                 editor.insert(&text, window, cx);
@@ -1561,7 +1581,9 @@ impl TextThreadEditor {
         cx: &mut Context<Self>,
     ) -> (String, CopyMetadata, Vec<text::Selection<usize>>) {
         let (mut selection, creases) = self.editor.update(cx, |editor, cx| {
-            let mut selection = editor.selections.newest_adjusted(cx);
+            let mut selection = editor
+                .selections
+                .newest_adjusted(&editor.display_snapshot(cx));
             let snapshot = editor.buffer().read(cx).snapshot(cx);
 
             selection.goal = SelectionGoal::None;
@@ -1609,29 +1631,33 @@ impl TextThreadEditor {
             )
         });
 
-        let context = self.context.read(cx);
+        let text_thread = self.text_thread.read(cx);
 
         let mut text = String::new();
 
         // If selection is empty, we want to copy the entire line
         if selection.range().is_empty() {
-            let snapshot = context.buffer().read(cx).snapshot();
+            let snapshot = text_thread.buffer().read(cx).snapshot();
             let point = snapshot.offset_to_point(selection.range().start);
             selection.start = snapshot.point_to_offset(Point::new(point.row, 0));
             selection.end = snapshot
                 .point_to_offset(cmp::min(Point::new(point.row + 1, 0), snapshot.max_point()));
-            for chunk in context.buffer().read(cx).text_for_range(selection.range()) {
+            for chunk in text_thread
+                .buffer()
+                .read(cx)
+                .text_for_range(selection.range())
+            {
                 text.push_str(chunk);
             }
         } else {
-            for message in context.messages(cx) {
+            for message in text_thread.messages(cx) {
                 if message.offset_range.start >= selection.range().end {
                     break;
                 } else if message.offset_range.end >= selection.range().start {
                     let range = cmp::max(message.offset_range.start, selection.range().start)
                         ..cmp::min(message.offset_range.end, selection.range().end);
                     if !range.is_empty() {
-                        for chunk in context.buffer().read(cx).text_for_range(range) {
+                        for chunk in text_thread.buffer().read(cx).text_for_range(range) {
                             text.push_str(chunk);
                         }
                         if message.offset_range.end < selection.range().end {
@@ -1680,7 +1706,10 @@ impl TextThreadEditor {
 
         if images.is_empty() {
             self.editor.update(cx, |editor, cx| {
-                let paste_position = editor.selections.newest::<usize>(cx).head();
+                let paste_position = editor
+                    .selections
+                    .newest::<usize>(&editor.display_snapshot(cx))
+                    .head();
                 editor.paste(action, window, cx);
 
                 if let Some(metadata) = metadata {
@@ -1727,19 +1756,19 @@ impl TextThreadEditor {
                 editor.transact(window, cx, |editor, _window, cx| {
                     let edits = editor
                         .selections
-                        .all::<usize>(cx)
+                        .all::<usize>(&editor.display_snapshot(cx))
                         .into_iter()
                         .map(|selection| (selection.start..selection.end, "\n"));
                     editor.edit(edits, cx);
 
                     let snapshot = editor.buffer().read(cx).snapshot(cx);
-                    for selection in editor.selections.all::<usize>(cx) {
+                    for selection in editor.selections.all::<usize>(&editor.display_snapshot(cx)) {
                         image_positions.push(snapshot.anchor_before(selection.end));
                     }
                 });
             });
 
-            self.context.update(cx, |context, cx| {
+            self.text_thread.update(cx, |text_thread, cx| {
                 for image in images {
                     let Some(render_image) = image.to_image_data(cx.svg_renderer()).log_err()
                     else {
@@ -1749,7 +1778,7 @@ impl TextThreadEditor {
                     let image_task = LanguageModelImage::from_image(Arc::new(image), cx).shared();
 
                     for image_position in image_positions.iter() {
-                        context.insert_content(
+                        text_thread.insert_content(
                             Content::Image {
                                 anchor: image_position.text_anchor,
                                 image_id,
@@ -1770,7 +1799,7 @@ impl TextThreadEditor {
             let excerpt_id = *buffer.as_singleton().unwrap().0;
             let old_blocks = std::mem::take(&mut self.image_blocks);
             let new_blocks = self
-                .context
+                .text_thread
                 .read(cx)
                 .contents(cx)
                 .map(
@@ -1818,36 +1847,36 @@ impl TextThreadEditor {
     }
 
     fn split(&mut self, _: &Split, _window: &mut Window, cx: &mut Context<Self>) {
-        self.context.update(cx, |context, cx| {
+        self.text_thread.update(cx, |text_thread, cx| {
             let selections = self.editor.read(cx).selections.disjoint_anchors_arc();
             for selection in selections.as_ref() {
                 let buffer = self.editor.read(cx).buffer().read(cx).snapshot(cx);
                 let range = selection
                     .map(|endpoint| endpoint.to_offset(&buffer))
                     .range();
-                context.split_message(range, cx);
+                text_thread.split_message(range, cx);
             }
         });
     }
 
     fn save(&mut self, _: &Save, _window: &mut Window, cx: &mut Context<Self>) {
-        self.context.update(cx, |context, cx| {
-            context.save(Some(Duration::from_millis(500)), self.fs.clone(), cx)
+        self.text_thread.update(cx, |text_thread, cx| {
+            text_thread.save(Some(Duration::from_millis(500)), self.fs.clone(), cx)
         });
     }
 
     pub fn title(&self, cx: &App) -> SharedString {
-        self.context.read(cx).summary().or_default()
+        self.text_thread.read(cx).summary().or_default()
     }
 
     pub fn regenerate_summary(&mut self, cx: &mut Context<Self>) {
-        self.context
-            .update(cx, |context, cx| context.summarize(true, cx));
+        self.text_thread
+            .update(cx, |text_thread, cx| text_thread.summarize(true, cx));
     }
 
     fn render_remaining_tokens(&self, cx: &App) -> Option<impl IntoElement + use<>> {
         let (token_count_color, token_count, max_token_count, tooltip) =
-            match token_state(&self.context, cx)? {
+            match token_state(&self.text_thread, cx)? {
                 TokenState::NoTokensLeft {
                     max_token_count,
                     token_count,
@@ -1895,7 +1924,7 @@ impl TextThreadEditor {
     fn render_send_button(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
         let focus_handle = self.focus_handle(cx);
 
-        let (style, tooltip) = match token_state(&self.context, cx) {
+        let (style, tooltip) = match token_state(&self.text_thread, cx) {
             Some(TokenState::NoTokensLeft { .. }) => (
                 ButtonStyle::Tinted(TintColor::Error),
                 Some(Tooltip::text("Token limit reached")(window, cx)),
@@ -1928,7 +1957,7 @@ impl TextThreadEditor {
             })
             .layer(ElevationIndex::ModalSurface)
             .key_binding(
-                KeyBinding::for_action_in(&Assist, &focus_handle, window, cx)
+                KeyBinding::for_action_in(&Assist, &focus_handle, cx)
                     .map(|kb| kb.size(rems_from_px(12.))),
             )
             .on_click(move |_event, window, cx| {
@@ -1963,20 +1992,14 @@ impl TextThreadEditor {
                 .icon_color(Color::Muted)
                 .selected_icon_color(Color::Accent)
                 .selected_style(ButtonStyle::Filled),
-            move |window, cx| {
-                Tooltip::with_meta(
-                    "Add Context",
-                    None,
-                    "Type / to insert via keyboard",
-                    window,
-                    cx,
-                )
+            move |_window, cx| {
+                Tooltip::with_meta("Add Context", None, "Type / to insert via keyboard", cx)
             },
         )
     }
 
     fn render_burn_mode_toggle(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
-        let context = self.context().read(cx);
+        let text_thread = self.text_thread().read(cx);
         let active_model = LanguageModelRegistry::read_global(cx)
             .default_model()
             .map(|default| default.model)?;
@@ -1984,7 +2007,7 @@ impl TextThreadEditor {
             return None;
         }
 
-        let active_completion_mode = context.completion_mode();
+        let active_completion_mode = text_thread.completion_mode();
         let burn_mode_enabled = active_completion_mode == CompletionMode::Burn;
         let icon = if burn_mode_enabled {
             IconName::ZedBurnModeOn
@@ -1999,8 +2022,8 @@ impl TextThreadEditor {
                 .toggle_state(burn_mode_enabled)
                 .selected_icon_color(Color::Error)
                 .on_click(cx.listener(move |this, _event, _window, cx| {
-                    this.context().update(cx, |context, _cx| {
-                        context.set_completion_mode(match active_completion_mode {
+                    this.text_thread().update(cx, |text_thread, _cx| {
+                        text_thread.set_completion_mode(match active_completion_mode {
                             CompletionMode::Burn => CompletionMode::Normal,
                             CompletionMode::Normal => CompletionMode::Burn,
                         });
@@ -2059,14 +2082,8 @@ impl TextThreadEditor {
                         )
                         .child(Icon::new(icon).color(color).size(IconSize::XSmall)),
                 ),
-            move |window, cx| {
-                Tooltip::for_action_in(
-                    "Change Model",
-                    &ToggleModelSelector,
-                    &focus_handle,
-                    window,
-                    cx,
-                )
+            move |_window, cx| {
+                Tooltip::for_action_in("Change Model", &ToggleModelSelector, &focus_handle, cx)
             },
             gpui::Corner::BottomRight,
             cx,
@@ -2633,10 +2650,10 @@ impl FollowableItem for TextThreadEditor {
     }
 
     fn to_state_proto(&self, window: &Window, cx: &App) -> Option<proto::view::Variant> {
-        let context = self.context.read(cx);
+        let text_thread = self.text_thread.read(cx);
         Some(proto::view::Variant::ContextEditor(
             proto::view::ContextEditor {
-                context_id: context.id().to_proto(),
+                context_id: text_thread.id().to_proto(),
                 editor: if let Some(proto::view::Variant::Editor(proto)) =
                     self.editor.read(cx).to_state_proto(window, cx)
                 {
@@ -2662,22 +2679,22 @@ impl FollowableItem for TextThreadEditor {
             unreachable!()
         };
 
-        let context_id = ContextId::from_proto(state.context_id);
+        let text_thread_id = TextThreadId::from_proto(state.context_id);
         let editor_state = state.editor?;
 
         let project = workspace.read(cx).project().clone();
         let agent_panel_delegate = <dyn AgentPanelDelegate>::try_global(cx)?;
 
-        let context_editor_task = workspace.update(cx, |workspace, cx| {
-            agent_panel_delegate.open_remote_context(workspace, context_id, window, cx)
+        let text_thread_editor_task = workspace.update(cx, |workspace, cx| {
+            agent_panel_delegate.open_remote_text_thread(workspace, text_thread_id, window, cx)
         });
 
         Some(window.spawn(cx, async move |cx| {
-            let context_editor = context_editor_task.await?;
-            context_editor
-                .update_in(cx, |context_editor, window, cx| {
-                    context_editor.remote_id = Some(id);
-                    context_editor.editor.update(cx, |editor, cx| {
+            let text_thread_editor = text_thread_editor_task.await?;
+            text_thread_editor
+                .update_in(cx, |text_thread_editor, window, cx| {
+                    text_thread_editor.remote_id = Some(id);
+                    text_thread_editor.editor.update(cx, |editor, cx| {
                         editor.apply_update_proto(
                             &project,
                             proto::update_view::Variant::Editor(proto::update_view::Editor {
@@ -2694,7 +2711,7 @@ impl FollowableItem for TextThreadEditor {
                     })
                 })?
                 .await?;
-            Ok(context_editor)
+            Ok(text_thread_editor)
         }))
     }
 
@@ -2741,7 +2758,7 @@ impl FollowableItem for TextThreadEditor {
     }
 
     fn dedup(&self, existing: &Self, _window: &Window, cx: &App) -> Option<item::Dedup> {
-        if existing.context.read(cx).id() == self.context.read(cx).id() {
+        if existing.text_thread.read(cx).id() == self.text_thread.read(cx).id() {
             Some(item::Dedup::KeepExisting)
         } else {
             None
@@ -2753,17 +2770,17 @@ enum PendingSlashCommand {}
 
 fn invoked_slash_command_fold_placeholder(
     command_id: InvokedSlashCommandId,
-    context: WeakEntity<AssistantContext>,
+    text_thread: WeakEntity<TextThread>,
 ) -> FoldPlaceholder {
     FoldPlaceholder {
         constrain_width: false,
         merge_adjacent: false,
         render: Arc::new(move |fold_id, _, cx| {
-            let Some(context) = context.upgrade() else {
+            let Some(text_thread) = text_thread.upgrade() else {
                 return Empty.into_any();
             };
 
-            let Some(command) = context.read(cx).invoked_slash_command(&command_id) else {
+            let Some(command) = text_thread.read(cx).invoked_slash_command(&command_id) else {
                 return Empty.into_any();
             };
 
@@ -2804,14 +2821,15 @@ enum TokenState {
     },
 }
 
-fn token_state(context: &Entity<AssistantContext>, cx: &App) -> Option<TokenState> {
+fn token_state(text_thread: &Entity<TextThread>, cx: &App) -> Option<TokenState> {
     const WARNING_TOKEN_THRESHOLD: f32 = 0.8;
 
     let model = LanguageModelRegistry::read_global(cx)
         .default_model()?
         .model;
-    let token_count = context.read(cx).token_count()?;
-    let max_token_count = model.max_token_count_for_mode(context.read(cx).completion_mode().into());
+    let token_count = text_thread.read(cx).token_count()?;
+    let max_token_count =
+        model.max_token_count_for_mode(text_thread.read(cx).completion_mode().into());
     let token_state = if max_token_count.saturating_sub(token_count) == 0 {
         TokenState::NoTokensLeft {
             max_token_count,
@@ -2923,7 +2941,7 @@ mod tests {
 
     #[gpui::test]
     async fn test_copy_paste_whole_message(cx: &mut TestAppContext) {
-        let (context, context_editor, mut cx) = setup_context_editor_text(vec![
+        let (context, text_thread_editor, mut cx) = setup_text_thread_editor_text(vec![
             (Role::User, "What is the Zed editor?"),
             (
                 Role::Assistant,
@@ -2933,8 +2951,8 @@ mod tests {
         ],cx).await;
 
         // Select & Copy whole user message
-        assert_copy_paste_context_editor(
-            &context_editor,
+        assert_copy_paste_text_thread_editor(
+            &text_thread_editor,
             message_range(&context, 0, &mut cx),
             indoc! {"
                 What is the Zed editor?
@@ -2945,8 +2963,8 @@ mod tests {
         );
 
         // Select & Copy whole assistant message
-        assert_copy_paste_context_editor(
-            &context_editor,
+        assert_copy_paste_text_thread_editor(
+            &text_thread_editor,
             message_range(&context, 1, &mut cx),
             indoc! {"
                 What is the Zed editor?
@@ -2960,7 +2978,7 @@ mod tests {
 
     #[gpui::test]
     async fn test_copy_paste_no_selection(cx: &mut TestAppContext) {
-        let (context, context_editor, mut cx) = setup_context_editor_text(
+        let (context, text_thread_editor, mut cx) = setup_text_thread_editor_text(
             vec![
                 (Role::User, "user1"),
                 (Role::Assistant, "assistant1"),
@@ -2973,8 +2991,8 @@ mod tests {
 
         // Copy and paste first assistant message
         let message_2_range = message_range(&context, 1, &mut cx);
-        assert_copy_paste_context_editor(
-            &context_editor,
+        assert_copy_paste_text_thread_editor(
+            &text_thread_editor,
             message_2_range.start..message_2_range.start,
             indoc! {"
                 user1
@@ -2987,8 +3005,8 @@ mod tests {
 
         // Copy and cut second assistant message
         let message_3_range = message_range(&context, 2, &mut cx);
-        assert_copy_paste_context_editor(
-            &context_editor,
+        assert_copy_paste_text_thread_editor(
+            &text_thread_editor,
             message_3_range.start..message_3_range.start,
             indoc! {"
                 user1
@@ -3075,29 +3093,29 @@ mod tests {
         }
     }
 
-    async fn setup_context_editor_text(
+    async fn setup_text_thread_editor_text(
         messages: Vec<(Role, &str)>,
         cx: &mut TestAppContext,
     ) -> (
-        Entity<AssistantContext>,
+        Entity<TextThread>,
         Entity<TextThreadEditor>,
         VisualTestContext,
     ) {
         cx.update(init_test);
 
         let fs = FakeFs::new(cx.executor());
-        let context = create_context_with_messages(messages, cx);
+        let text_thread = create_text_thread_with_messages(messages, cx);
 
         let project = Project::test(fs.clone(), [path!("/test").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 context_editor = window
+        let text_thread_editor = window
             .update(&mut cx, |_, window, cx| {
                 cx.new(|cx| {
-                    TextThreadEditor::for_context(
-                        context.clone(),
+                    TextThreadEditor::for_text_thread(
+                        text_thread.clone(),
                         fs,
                         workspace.downgrade(),
                         project,

crates/agent_ui/src/ui/burn_mode_tooltip.rs 🔗

@@ -18,7 +18,7 @@ impl BurnModeTooltip {
 }
 
 impl Render for BurnModeTooltip {
-    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
         let (icon, color) = if self.selected {
             (IconName::ZedBurnModeOn, Color::Error)
         } else {
@@ -45,8 +45,7 @@ impl Render for BurnModeTooltip {
             .child(Label::new("Burn Mode"))
             .when(self.selected, |title| title.child(turned_on));
 
-        let keybinding = KeyBinding::for_action(&ToggleBurnMode, window, cx)
-            .map(|kb| kb.size(rems_from_px(12.)));
+        let keybinding = KeyBinding::for_action(&ToggleBurnMode, cx).size(rems_from_px(12.));
 
         tooltip_container(cx, |this, _| {
             this
@@ -54,7 +53,7 @@ impl Render for BurnModeTooltip {
                     h_flex()
                         .justify_between()
                         .child(title)
-                        .children(keybinding)
+                        .child(keybinding)
                 )
                 .child(
                     div()

crates/agent_ui/src/ui/context_pill.rs 🔗

@@ -11,13 +11,13 @@ use project::Project;
 use prompt_store::PromptStore;
 use rope::Point;
 use ui::{IconButtonShape, Tooltip, prelude::*, tooltip_container};
+use util::paths::PathStyle;
 
-use agent::context::{
+use crate::context::{
     AgentContextHandle, ContextId, ContextKind, DirectoryContextHandle, FetchedUrlContext,
     FileContextHandle, ImageContext, ImageStatus, RulesContextHandle, SelectionContextHandle,
     SymbolContextHandle, TextThreadContextHandle, ThreadContextHandle,
 };
-use util::paths::PathStyle;
 
 #[derive(IntoElement)]
 pub enum ContextPill {
@@ -244,8 +244,8 @@ impl RenderOnce for ContextPill {
                             .truncate(),
                     ),
                 )
-                .tooltip(|window, cx| {
-                    Tooltip::with_meta("Suggested Context", None, "Click to add it", window, cx)
+                .tooltip(|_window, cx| {
+                    Tooltip::with_meta("Suggested Context", None, "Click to add it", cx)
                 })
                 .when_some(on_click.as_ref(), |element, on_click| {
                     let on_click = on_click.clone();
@@ -466,7 +466,7 @@ impl AddedContext {
             parent: None,
             tooltip: None,
             icon_path: None,
-            status: if handle.thread.read(cx).is_generating_detailed_summary() {
+            status: if handle.thread.read(cx).is_generating_summary() {
                 ContextStatus::Loading {
                     message: "Summarizing…".into(),
                 }
@@ -476,7 +476,11 @@ impl AddedContext {
             render_hover: {
                 let thread = handle.thread.clone();
                 Some(Rc::new(move |_, cx| {
-                    let text = thread.read(cx).latest_detailed_summary_or_text();
+                    let text = thread
+                        .update(cx, |thread, cx| thread.summary(cx))
+                        .now_or_never()
+                        .flatten()
+                        .unwrap_or_else(|| SharedString::from(thread.read(cx).to_markdown()));
                     ContextPillHover::new_text(text, cx).into()
                 }))
             },
@@ -493,9 +497,9 @@ impl AddedContext {
             icon_path: None,
             status: ContextStatus::Ready,
             render_hover: {
-                let context = handle.context.clone();
+                let text_thread = handle.text_thread.clone();
                 Some(Rc::new(move |_, cx| {
-                    let text = context.read(cx).to_xml(cx);
+                    let text = text_thread.read(cx).to_xml(cx);
                     ContextPillHover::new_text(text.into(), cx).into()
                 }))
             },

crates/ai_onboarding/Cargo.toml 🔗

@@ -24,5 +24,4 @@ serde.workspace = true
 smallvec.workspace = true
 telemetry.workspace = true
 ui.workspace = true
-workspace-hack.workspace = true
 zed_actions.workspace = true

crates/ai_onboarding/src/ai_onboarding.rs 🔗

@@ -84,10 +84,32 @@ impl ZedAiOnboarding {
         self
     }
 
+    fn render_dismiss_button(&self) -> Option<AnyElement> {
+        self.dismiss_onboarding.as_ref().map(|dismiss_callback| {
+            let callback = dismiss_callback.clone();
+
+            h_flex()
+                .absolute()
+                .top_0()
+                .right_0()
+                .child(
+                    IconButton::new("dismiss_onboarding", IconName::Close)
+                        .icon_size(IconSize::Small)
+                        .tooltip(Tooltip::text("Dismiss"))
+                        .on_click(move |_, window, cx| {
+                            telemetry::event!("Banner Dismissed", source = "AI Onboarding",);
+                            callback(window, cx)
+                        }),
+                )
+                .into_any_element()
+        })
+    }
+
     fn render_sign_in_disclaimer(&self, _cx: &mut App) -> AnyElement {
         let signing_in = matches!(self.sign_in_status, SignInStatus::SigningIn);
 
         v_flex()
+            .relative()
             .gap_1()
             .child(Headline::new("Welcome to Zed AI"))
             .child(
@@ -109,6 +131,7 @@ impl ZedAiOnboarding {
                         }
                     }),
             )
+            .children(self.render_dismiss_button())
             .into_any_element()
     }
 
@@ -180,27 +203,7 @@ impl ZedAiOnboarding {
                         )
                         .child(PlanDefinitions.free_plan(is_v2)),
                 )
-                .when_some(
-                    self.dismiss_onboarding.as_ref(),
-                    |this, dismiss_callback| {
-                        let callback = dismiss_callback.clone();
-
-                        this.child(
-                            h_flex().absolute().top_0().right_0().child(
-                                IconButton::new("dismiss_onboarding", IconName::Close)
-                                    .icon_size(IconSize::Small)
-                                    .tooltip(Tooltip::text("Dismiss"))
-                                    .on_click(move |_, window, cx| {
-                                        telemetry::event!(
-                                            "Banner Dismissed",
-                                            source = "AI Onboarding",
-                                        );
-                                        callback(window, cx)
-                                    }),
-                            ),
-                        )
-                    },
-                )
+                .children(self.render_dismiss_button())
                 .child(
                     v_flex()
                         .mt_2()
@@ -245,26 +248,7 @@ impl ZedAiOnboarding {
                     .mb_2(),
             )
             .child(PlanDefinitions.pro_trial(is_v2, false))
-            .when_some(
-                self.dismiss_onboarding.as_ref(),
-                |this, dismiss_callback| {
-                    let callback = dismiss_callback.clone();
-                    this.child(
-                        h_flex().absolute().top_0().right_0().child(
-                            IconButton::new("dismiss_onboarding", IconName::Close)
-                                .icon_size(IconSize::Small)
-                                .tooltip(Tooltip::text("Dismiss"))
-                                .on_click(move |_, window, cx| {
-                                    telemetry::event!(
-                                        "Banner Dismissed",
-                                        source = "AI Onboarding",
-                                    );
-                                    callback(window, cx)
-                                }),
-                        ),
-                    )
-                },
-            )
+            .children(self.render_dismiss_button())
             .into_any_element()
     }
 
@@ -278,26 +262,7 @@ impl ZedAiOnboarding {
                     .mb_2(),
             )
             .child(PlanDefinitions.pro_plan(is_v2, false))
-            .when_some(
-                self.dismiss_onboarding.as_ref(),
-                |this, dismiss_callback| {
-                    let callback = dismiss_callback.clone();
-                    this.child(
-                        h_flex().absolute().top_0().right_0().child(
-                            IconButton::new("dismiss_onboarding", IconName::Close)
-                                .icon_size(IconSize::Small)
-                                .tooltip(Tooltip::text("Dismiss"))
-                                .on_click(move |_, window, cx| {
-                                    telemetry::event!(
-                                        "Banner Dismissed",
-                                        source = "AI Onboarding",
-                                    );
-                                    callback(window, cx)
-                                }),
-                        ),
-                    )
-                },
-            )
+            .children(self.render_dismiss_button())
             .into_any_element()
     }
 }

crates/anthropic/Cargo.toml 🔗

@@ -26,4 +26,3 @@ serde_json.workspace = true
 settings.workspace = true
 strum.workspace = true
 thiserror.workspace = true
-workspace-hack.workspace = true

crates/askpass/Cargo.toml 🔗

@@ -20,7 +20,6 @@ smol.workspace = true
 log.workspace = true
 tempfile.workspace = true
 util.workspace = true
-workspace-hack.workspace = true
 zeroize.workspace = true
 
 [target.'cfg(target_os = "windows")'.dependencies]

crates/assets/Cargo.toml 🔗

@@ -15,4 +15,3 @@ workspace = true
 anyhow.workspace = true
 gpui.workspace = true
 rust-embed.workspace = true
-workspace-hack.workspace = true

crates/assistant_slash_command/Cargo.toml 🔗

@@ -27,7 +27,6 @@ serde_json.workspace = true
 ui.workspace = true
 util.workspace = true
 workspace.workspace = true
-workspace-hack.workspace = true
 
 [dev-dependencies]
 gpui = { workspace = true, features = ["test-support"] }

crates/assistant_slash_commands/Cargo.toml 🔗

@@ -38,7 +38,6 @@ ui.workspace = true
 util.workspace = true
 workspace.workspace = true
 worktree.workspace = true
-workspace-hack.workspace = true
 
 [dev-dependencies]
 fs = { workspace = true, features = ["test-support"] }

crates/assistant_slash_commands/src/selection_command.rs 🔗

@@ -79,7 +79,7 @@ impl SlashCommand for SelectionCommand {
                 editor.update(cx, |editor, cx| {
                     let selection_ranges = editor
                         .selections
-                        .all_adjusted(cx)
+                        .all_adjusted(&editor.display_snapshot(cx))
                         .iter()
                         .map(|selection| selection.range())
                         .collect::<Vec<_>>();

crates/assistant_context/Cargo.toml → crates/assistant_text_thread/Cargo.toml 🔗

@@ -1,5 +1,5 @@
 [package]
-name = "assistant_context"
+name = "assistant_text_thread"
 version = "0.1.0"
 edition.workspace = true
 publish.workspace = true
@@ -9,7 +9,7 @@ license = "GPL-3.0-or-later"
 workspace = true
 
 [lib]
-path = "src/assistant_context.rs"
+path = "src/assistant_text_thread.rs"
 
 [features]
 test-support = []
@@ -51,7 +51,6 @@ ui.workspace = true
 util.workspace = true
 uuid.workspace = true
 workspace.workspace = true
-workspace-hack.workspace = true
 zed_env_vars.workspace = true
 
 [dev-dependencies]

crates/assistant_text_thread/src/assistant_text_thread.rs 🔗

@@ -0,0 +1,15 @@
+#[cfg(test)]
+mod assistant_text_thread_tests;
+mod text_thread;
+mod text_thread_store;
+
+pub use crate::text_thread::*;
+pub use crate::text_thread_store::*;
+
+use client::Client;
+use gpui::App;
+use std::sync::Arc;
+
+pub fn init(client: Arc<Client>, _: &mut App) {
+    text_thread_store::init(&client.into());
+}

crates/assistant_context/src/assistant_context_tests.rs → crates/assistant_text_thread/src/assistant_text_thread_tests.rs 🔗

@@ -1,6 +1,6 @@
 use crate::{
-    AssistantContext, CacheStatus, ContextEvent, ContextId, ContextOperation, ContextSummary,
-    InvokedSlashCommandId, MessageCacheMetadata, MessageId, MessageStatus,
+    CacheStatus, InvokedSlashCommandId, MessageCacheMetadata, MessageId, MessageStatus, TextThread,
+    TextThreadEvent, TextThreadId, TextThreadOperation, TextThreadSummary,
 };
 use anyhow::Result;
 use assistant_slash_command::{
@@ -47,8 +47,8 @@ fn test_inserting_and_removing_messages(cx: &mut App) {
 
     let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
     let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
-    let context = cx.new(|cx| {
-        AssistantContext::local(
+    let text_thread = cx.new(|cx| {
+        TextThread::local(
             registry,
             None,
             None,
@@ -57,21 +57,21 @@ fn test_inserting_and_removing_messages(cx: &mut App) {
             cx,
         )
     });
-    let buffer = context.read(cx).buffer.clone();
+    let buffer = text_thread.read(cx).buffer().clone();
 
-    let message_1 = context.read(cx).message_anchors[0].clone();
+    let message_1 = text_thread.read(cx).message_anchors[0].clone();
     assert_eq!(
-        messages(&context, cx),
+        messages(&text_thread, cx),
         vec![(message_1.id, Role::User, 0..0)]
     );
 
-    let message_2 = context.update(cx, |context, cx| {
+    let message_2 = text_thread.update(cx, |context, cx| {
         context
             .insert_message_after(message_1.id, Role::Assistant, MessageStatus::Done, cx)
             .unwrap()
     });
     assert_eq!(
-        messages(&context, cx),
+        messages(&text_thread, cx),
         vec![
             (message_1.id, Role::User, 0..1),
             (message_2.id, Role::Assistant, 1..1)
@@ -82,20 +82,20 @@ fn test_inserting_and_removing_messages(cx: &mut App) {
         buffer.edit([(0..0, "1"), (1..1, "2")], None, cx)
     });
     assert_eq!(
-        messages(&context, cx),
+        messages(&text_thread, cx),
         vec![
             (message_1.id, Role::User, 0..2),
             (message_2.id, Role::Assistant, 2..3)
         ]
     );
 
-    let message_3 = context.update(cx, |context, cx| {
+    let message_3 = text_thread.update(cx, |context, cx| {
         context
             .insert_message_after(message_2.id, Role::User, MessageStatus::Done, cx)
             .unwrap()
     });
     assert_eq!(
-        messages(&context, cx),
+        messages(&text_thread, cx),
         vec![
             (message_1.id, Role::User, 0..2),
             (message_2.id, Role::Assistant, 2..4),
@@ -103,13 +103,13 @@ fn test_inserting_and_removing_messages(cx: &mut App) {
         ]
     );
 
-    let message_4 = context.update(cx, |context, cx| {
+    let message_4 = text_thread.update(cx, |context, cx| {
         context
             .insert_message_after(message_2.id, Role::User, MessageStatus::Done, cx)
             .unwrap()
     });
     assert_eq!(
-        messages(&context, cx),
+        messages(&text_thread, cx),
         vec![
             (message_1.id, Role::User, 0..2),
             (message_2.id, Role::Assistant, 2..4),
@@ -122,7 +122,7 @@ fn test_inserting_and_removing_messages(cx: &mut App) {
         buffer.edit([(4..4, "C"), (5..5, "D")], None, cx)
     });
     assert_eq!(
-        messages(&context, cx),
+        messages(&text_thread, cx),
         vec![
             (message_1.id, Role::User, 0..2),
             (message_2.id, Role::Assistant, 2..4),
@@ -134,7 +134,7 @@ fn test_inserting_and_removing_messages(cx: &mut App) {
     // Deleting across message boundaries merges the messages.
     buffer.update(cx, |buffer, cx| buffer.edit([(1..4, "")], None, cx));
     assert_eq!(
-        messages(&context, cx),
+        messages(&text_thread, cx),
         vec![
             (message_1.id, Role::User, 0..3),
             (message_3.id, Role::User, 3..4),
@@ -144,7 +144,7 @@ fn test_inserting_and_removing_messages(cx: &mut App) {
     // Undoing the deletion should also undo the merge.
     buffer.update(cx, |buffer, cx| buffer.undo(cx));
     assert_eq!(
-        messages(&context, cx),
+        messages(&text_thread, cx),
         vec![
             (message_1.id, Role::User, 0..2),
             (message_2.id, Role::Assistant, 2..4),
@@ -156,7 +156,7 @@ fn test_inserting_and_removing_messages(cx: &mut App) {
     // Redoing the deletion should also redo the merge.
     buffer.update(cx, |buffer, cx| buffer.redo(cx));
     assert_eq!(
-        messages(&context, cx),
+        messages(&text_thread, cx),
         vec![
             (message_1.id, Role::User, 0..3),
             (message_3.id, Role::User, 3..4),
@@ -164,13 +164,13 @@ fn test_inserting_and_removing_messages(cx: &mut App) {
     );
 
     // Ensure we can still insert after a merged message.
-    let message_5 = context.update(cx, |context, cx| {
+    let message_5 = text_thread.update(cx, |context, cx| {
         context
             .insert_message_after(message_1.id, Role::System, MessageStatus::Done, cx)
             .unwrap()
     });
     assert_eq!(
-        messages(&context, cx),
+        messages(&text_thread, cx),
         vec![
             (message_1.id, Role::User, 0..3),
             (message_5.id, Role::System, 3..4),
@@ -186,8 +186,8 @@ fn test_message_splitting(cx: &mut App) {
     let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
 
     let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
-    let context = cx.new(|cx| {
-        AssistantContext::local(
+    let text_thread = cx.new(|cx| {
+        TextThread::local(
             registry.clone(),
             None,
             None,
@@ -196,11 +196,11 @@ fn test_message_splitting(cx: &mut App) {
             cx,
         )
     });
-    let buffer = context.read(cx).buffer.clone();
+    let buffer = text_thread.read(cx).buffer().clone();
 
-    let message_1 = context.read(cx).message_anchors[0].clone();
+    let message_1 = text_thread.read(cx).message_anchors[0].clone();
     assert_eq!(
-        messages(&context, cx),
+        messages(&text_thread, cx),
         vec![(message_1.id, Role::User, 0..0)]
     );
 
@@ -208,26 +208,28 @@ fn test_message_splitting(cx: &mut App) {
         buffer.edit([(0..0, "aaa\nbbb\nccc\nddd\n")], None, cx)
     });
 
-    let (_, message_2) = context.update(cx, |context, cx| context.split_message(3..3, cx));
+    let (_, message_2) =
+        text_thread.update(cx, |text_thread, cx| text_thread.split_message(3..3, cx));
     let message_2 = message_2.unwrap();
 
     // We recycle newlines in the middle of a split message
     assert_eq!(buffer.read(cx).text(), "aaa\nbbb\nccc\nddd\n");
     assert_eq!(
-        messages(&context, cx),
+        messages(&text_thread, cx),
         vec![
             (message_1.id, Role::User, 0..4),
             (message_2.id, Role::User, 4..16),
         ]
     );
 
-    let (_, message_3) = context.update(cx, |context, cx| context.split_message(3..3, cx));
+    let (_, message_3) =
+        text_thread.update(cx, |text_thread, cx| text_thread.split_message(3..3, cx));
     let message_3 = message_3.unwrap();
 
     // We don't recycle newlines at the end of a split message
     assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\nccc\nddd\n");
     assert_eq!(
-        messages(&context, cx),
+        messages(&text_thread, cx),
         vec![
             (message_1.id, Role::User, 0..4),
             (message_3.id, Role::User, 4..5),
@@ -235,11 +237,12 @@ fn test_message_splitting(cx: &mut App) {
         ]
     );
 
-    let (_, message_4) = context.update(cx, |context, cx| context.split_message(9..9, cx));
+    let (_, message_4) =
+        text_thread.update(cx, |text_thread, cx| text_thread.split_message(9..9, cx));
     let message_4 = message_4.unwrap();
     assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\nccc\nddd\n");
     assert_eq!(
-        messages(&context, cx),
+        messages(&text_thread, cx),
         vec![
             (message_1.id, Role::User, 0..4),
             (message_3.id, Role::User, 4..5),
@@ -248,11 +251,12 @@ fn test_message_splitting(cx: &mut App) {
         ]
     );
 
-    let (_, message_5) = context.update(cx, |context, cx| context.split_message(9..9, cx));
+    let (_, message_5) =
+        text_thread.update(cx, |text_thread, cx| text_thread.split_message(9..9, cx));
     let message_5 = message_5.unwrap();
     assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\n\nccc\nddd\n");
     assert_eq!(
-        messages(&context, cx),
+        messages(&text_thread, cx),
         vec![
             (message_1.id, Role::User, 0..4),
             (message_3.id, Role::User, 4..5),
@@ -263,12 +267,12 @@ fn test_message_splitting(cx: &mut App) {
     );
 
     let (message_6, message_7) =
-        context.update(cx, |context, cx| context.split_message(14..16, cx));
+        text_thread.update(cx, |text_thread, cx| text_thread.split_message(14..16, cx));
     let message_6 = message_6.unwrap();
     let message_7 = message_7.unwrap();
     assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\n\nccc\ndd\nd\n");
     assert_eq!(
-        messages(&context, cx),
+        messages(&text_thread, cx),
         vec![
             (message_1.id, Role::User, 0..4),
             (message_3.id, Role::User, 4..5),
@@ -287,8 +291,8 @@ fn test_messages_for_offsets(cx: &mut App) {
 
     let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
     let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
-    let context = cx.new(|cx| {
-        AssistantContext::local(
+    let text_thread = cx.new(|cx| {
+        TextThread::local(
             registry,
             None,
             None,
@@ -297,32 +301,32 @@ fn test_messages_for_offsets(cx: &mut App) {
             cx,
         )
     });
-    let buffer = context.read(cx).buffer.clone();
+    let buffer = text_thread.read(cx).buffer().clone();
 
-    let message_1 = context.read(cx).message_anchors[0].clone();
+    let message_1 = text_thread.read(cx).message_anchors[0].clone();
     assert_eq!(
-        messages(&context, cx),
+        messages(&text_thread, cx),
         vec![(message_1.id, Role::User, 0..0)]
     );
 
     buffer.update(cx, |buffer, cx| buffer.edit([(0..0, "aaa")], None, cx));
-    let message_2 = context
-        .update(cx, |context, cx| {
-            context.insert_message_after(message_1.id, Role::User, MessageStatus::Done, cx)
+    let message_2 = text_thread
+        .update(cx, |text_thread, cx| {
+            text_thread.insert_message_after(message_1.id, Role::User, MessageStatus::Done, cx)
         })
         .unwrap();
     buffer.update(cx, |buffer, cx| buffer.edit([(4..4, "bbb")], None, cx));
 
-    let message_3 = context
-        .update(cx, |context, cx| {
-            context.insert_message_after(message_2.id, Role::User, MessageStatus::Done, cx)
+    let message_3 = text_thread
+        .update(cx, |text_thread, cx| {
+            text_thread.insert_message_after(message_2.id, Role::User, MessageStatus::Done, cx)
         })
         .unwrap();
     buffer.update(cx, |buffer, cx| buffer.edit([(8..8, "ccc")], None, cx));
 
     assert_eq!(buffer.read(cx).text(), "aaa\nbbb\nccc");
     assert_eq!(
-        messages(&context, cx),
+        messages(&text_thread, cx),
         vec![
             (message_1.id, Role::User, 0..4),
             (message_2.id, Role::User, 4..8),
@@ -331,22 +335,22 @@ fn test_messages_for_offsets(cx: &mut App) {
     );
 
     assert_eq!(
-        message_ids_for_offsets(&context, &[0, 4, 9], cx),
+        message_ids_for_offsets(&text_thread, &[0, 4, 9], cx),
         [message_1.id, message_2.id, message_3.id]
     );
     assert_eq!(
-        message_ids_for_offsets(&context, &[0, 1, 11], cx),
+        message_ids_for_offsets(&text_thread, &[0, 1, 11], cx),
         [message_1.id, message_3.id]
     );
 
-    let message_4 = context
-        .update(cx, |context, cx| {
-            context.insert_message_after(message_3.id, Role::User, MessageStatus::Done, cx)
+    let message_4 = text_thread
+        .update(cx, |text_thread, cx| {
+            text_thread.insert_message_after(message_3.id, Role::User, MessageStatus::Done, cx)
         })
         .unwrap();
     assert_eq!(buffer.read(cx).text(), "aaa\nbbb\nccc\n");
     assert_eq!(
-        messages(&context, cx),
+        messages(&text_thread, cx),
         vec![
             (message_1.id, Role::User, 0..4),
             (message_2.id, Role::User, 4..8),
@@ -355,12 +359,12 @@ fn test_messages_for_offsets(cx: &mut App) {
         ]
     );
     assert_eq!(
-        message_ids_for_offsets(&context, &[0, 4, 8, 12], cx),
+        message_ids_for_offsets(&text_thread, &[0, 4, 8, 12], cx),
         [message_1.id, message_2.id, message_3.id, message_4.id]
     );
 
     fn message_ids_for_offsets(
-        context: &Entity<AssistantContext>,
+        context: &Entity<TextThread>,
         offsets: &[usize],
         cx: &App,
     ) -> Vec<MessageId> {
@@ -398,8 +402,8 @@ async fn test_slash_commands(cx: &mut TestAppContext) {
 
     let registry = Arc::new(LanguageRegistry::test(cx.executor()));
     let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
-    let context = cx.new(|cx| {
-        AssistantContext::local(
+    let text_thread = cx.new(|cx| {
+        TextThread::local(
             registry.clone(),
             None,
             None,
@@ -417,19 +421,19 @@ async fn test_slash_commands(cx: &mut TestAppContext) {
     }
 
     let context_ranges = Rc::new(RefCell::new(ContextRanges::default()));
-    context.update(cx, |_, cx| {
-        cx.subscribe(&context, {
+    text_thread.update(cx, |_, cx| {
+        cx.subscribe(&text_thread, {
             let context_ranges = context_ranges.clone();
-            move |context, _, event, _| {
+            move |text_thread, _, event, _| {
                 let mut context_ranges = context_ranges.borrow_mut();
                 match event {
-                    ContextEvent::InvokedSlashCommandChanged { command_id } => {
-                        let command = context.invoked_slash_command(command_id).unwrap();
+                    TextThreadEvent::InvokedSlashCommandChanged { command_id } => {
+                        let command = text_thread.invoked_slash_command(command_id).unwrap();
                         context_ranges
                             .command_outputs
                             .insert(*command_id, command.range.clone());
                     }
-                    ContextEvent::ParsedSlashCommandsUpdated { removed, updated } => {
+                    TextThreadEvent::ParsedSlashCommandsUpdated { removed, updated } => {
                         for range in removed {
                             context_ranges.parsed_commands.remove(range);
                         }
@@ -439,7 +443,7 @@ async fn test_slash_commands(cx: &mut TestAppContext) {
                                 .insert(command.source_range.clone());
                         }
                     }
-                    ContextEvent::SlashCommandOutputSectionAdded { section } => {
+                    TextThreadEvent::SlashCommandOutputSectionAdded { section } => {
                         context_ranges.output_sections.insert(section.range.clone());
                     }
                     _ => {}
@@ -449,7 +453,7 @@ async fn test_slash_commands(cx: &mut TestAppContext) {
         .detach();
     });
 
-    let buffer = context.read_with(cx, |context, _| context.buffer.clone());
+    let buffer = text_thread.read_with(cx, |text_thread, _| text_thread.buffer().clone());
 
     // Insert a slash command
     buffer.update(cx, |buffer, cx| {
@@ -508,9 +512,9 @@ async fn test_slash_commands(cx: &mut TestAppContext) {
     );
 
     let (command_output_tx, command_output_rx) = mpsc::unbounded();
-    context.update(cx, |context, cx| {
-        let command_source_range = context.parsed_slash_commands[0].source_range.clone();
-        context.insert_command_output(
+    text_thread.update(cx, |text_thread, cx| {
+        let command_source_range = text_thread.parsed_slash_commands[0].source_range.clone();
+        text_thread.insert_command_output(
             command_source_range,
             "file",
             Task::ready(Ok(command_output_rx.boxed())),
@@ -670,8 +674,8 @@ async fn test_serialization(cx: &mut TestAppContext) {
 
     let registry = Arc::new(LanguageRegistry::test(cx.executor()));
     let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
-    let context = cx.new(|cx| {
-        AssistantContext::local(
+    let text_thread = cx.new(|cx| {
+        TextThread::local(
             registry.clone(),
             None,
             None,
@@ -680,15 +684,15 @@ async fn test_serialization(cx: &mut TestAppContext) {
             cx,
         )
     });
-    let buffer = context.read_with(cx, |context, _| context.buffer.clone());
-    let message_0 = context.read_with(cx, |context, _| context.message_anchors[0].id);
-    let message_1 = context.update(cx, |context, cx| {
-        context
+    let buffer = text_thread.read_with(cx, |text_thread, _| text_thread.buffer().clone());
+    let message_0 = text_thread.read_with(cx, |text_thread, _| text_thread.message_anchors[0].id);
+    let message_1 = text_thread.update(cx, |text_thread, cx| {
+        text_thread
             .insert_message_after(message_0, Role::Assistant, MessageStatus::Done, cx)
             .unwrap()
     });
-    let message_2 = context.update(cx, |context, cx| {
-        context
+    let message_2 = text_thread.update(cx, |text_thread, cx| {
+        text_thread
             .insert_message_after(message_1.id, Role::System, MessageStatus::Done, cx)
             .unwrap()
     });
@@ -696,15 +700,15 @@ async fn test_serialization(cx: &mut TestAppContext) {
         buffer.edit([(0..0, "a"), (1..1, "b\nc")], None, cx);
         buffer.finalize_last_transaction();
     });
-    let _message_3 = context.update(cx, |context, cx| {
-        context
+    let _message_3 = text_thread.update(cx, |text_thread, cx| {
+        text_thread
             .insert_message_after(message_2.id, Role::System, MessageStatus::Done, cx)
             .unwrap()
     });
     buffer.update(cx, |buffer, cx| buffer.undo(cx));
     assert_eq!(buffer.read_with(cx, |buffer, _| buffer.text()), "a\nb\nc\n");
     assert_eq!(
-        cx.read(|cx| messages(&context, cx)),
+        cx.read(|cx| messages(&text_thread, cx)),
         [
             (message_0, Role::User, 0..2),
             (message_1.id, Role::Assistant, 2..6),
@@ -712,9 +716,9 @@ async fn test_serialization(cx: &mut TestAppContext) {
         ]
     );
 
-    let serialized_context = context.read_with(cx, |context, cx| context.serialize(cx));
+    let serialized_context = text_thread.read_with(cx, |text_thread, cx| text_thread.serialize(cx));
     let deserialized_context = cx.new(|cx| {
-        AssistantContext::deserialize(
+        TextThread::deserialize(
             serialized_context,
             Path::new("").into(),
             registry.clone(),
@@ -726,7 +730,7 @@ async fn test_serialization(cx: &mut TestAppContext) {
         )
     });
     let deserialized_buffer =
-        deserialized_context.read_with(cx, |context, _| context.buffer.clone());
+        deserialized_context.read_with(cx, |text_thread, _| text_thread.buffer().clone());
     assert_eq!(
         deserialized_buffer.read_with(cx, |buffer, _| buffer.text()),
         "a\nb\nc\n"
@@ -741,7 +745,7 @@ async fn test_serialization(cx: &mut TestAppContext) {
     );
 }
 
-#[gpui::test(iterations = 100)]
+#[gpui::test(iterations = 25)]
 async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: StdRng) {
     cx.update(init_test);
 
@@ -762,16 +766,16 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
 
     let registry = Arc::new(LanguageRegistry::test(cx.background_executor.clone()));
     let network = Arc::new(Mutex::new(Network::new(rng.clone())));
-    let mut contexts = Vec::new();
+    let mut text_threads = Vec::new();
 
     let num_peers = rng.random_range(min_peers..=max_peers);
-    let context_id = ContextId::new();
+    let context_id = TextThreadId::new();
     let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
     for i in 0..num_peers {
         let context = cx.new(|cx| {
-            AssistantContext::new(
+            TextThread::new(
                 context_id.clone(),
-                i as ReplicaId,
+                ReplicaId::new(i as u16),
                 language::Capability::ReadWrite,
                 registry.clone(),
                 prompt_builder.clone(),
@@ -786,18 +790,18 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
             cx.subscribe(&context, {
                 let network = network.clone();
                 move |_, event, _| {
-                    if let ContextEvent::Operation(op) = event {
+                    if let TextThreadEvent::Operation(op) = event {
                         network
                             .lock()
-                            .broadcast(i as ReplicaId, vec![op.to_proto()]);
+                            .broadcast(ReplicaId::new(i as u16), vec![op.to_proto()]);
                     }
                 }
             })
             .detach();
         });
 
-        contexts.push(context);
-        network.lock().add_peer(i as ReplicaId);
+        text_threads.push(context);
+        network.lock().add_peer(ReplicaId::new(i as u16));
     }
 
     let mut mutation_count = operations;
@@ -806,30 +810,30 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
         || !network.lock().is_idle()
         || network.lock().contains_disconnected_peers()
     {
-        let context_index = rng.random_range(0..contexts.len());
-        let context = &contexts[context_index];
+        let context_index = rng.random_range(0..text_threads.len());
+        let text_thread = &text_threads[context_index];
 
         match rng.random_range(0..100) {
             0..=29 if mutation_count > 0 => {
                 log::info!("Context {}: edit buffer", context_index);
-                context.update(cx, |context, cx| {
-                    context
-                        .buffer
+                text_thread.update(cx, |text_thread, cx| {
+                    text_thread
+                        .buffer()
                         .update(cx, |buffer, cx| buffer.randomly_edit(&mut rng, 1, cx));
                 });
                 mutation_count -= 1;
             }
             30..=44 if mutation_count > 0 => {
-                context.update(cx, |context, cx| {
-                    let range = context.buffer.read(cx).random_byte_range(0, &mut rng);
+                text_thread.update(cx, |text_thread, cx| {
+                    let range = text_thread.buffer().read(cx).random_byte_range(0, &mut rng);
                     log::info!("Context {}: split message at {:?}", context_index, range);
-                    context.split_message(range, cx);
+                    text_thread.split_message(range, cx);
                 });
                 mutation_count -= 1;
             }
             45..=59 if mutation_count > 0 => {
-                context.update(cx, |context, cx| {
-                    if let Some(message) = context.messages(cx).choose(&mut rng) {
+                text_thread.update(cx, |text_thread, cx| {
+                    if let Some(message) = text_thread.messages(cx).choose(&mut rng) {
                         let role = *[Role::User, Role::Assistant, Role::System]
                             .choose(&mut rng)
                             .unwrap();
@@ -839,13 +843,13 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
                             message.id,
                             role
                         );
-                        context.insert_message_after(message.id, role, MessageStatus::Done, cx);
+                        text_thread.insert_message_after(message.id, role, MessageStatus::Done, cx);
                     }
                 });
                 mutation_count -= 1;
             }
             60..=74 if mutation_count > 0 => {
-                context.update(cx, |context, cx| {
+                text_thread.update(cx, |text_thread, cx| {
                     let command_text = "/".to_string()
                         + slash_commands
                             .command_names()
@@ -854,7 +858,7 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
                             .clone()
                             .as_ref();
 
-                    let command_range = context.buffer.update(cx, |buffer, cx| {
+                    let command_range = text_thread.buffer().update(cx, |buffer, cx| {
                         let offset = buffer.random_byte_range(0, &mut rng).start;
                         buffer.edit(
                             [(offset..offset, format!("\n{}\n", command_text))],
@@ -908,9 +912,15 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
                         events.len()
                     );
 
-                    let command_range = context.buffer.read(cx).anchor_after(command_range.start)
-                        ..context.buffer.read(cx).anchor_after(command_range.end);
-                    context.insert_command_output(
+                    let command_range = text_thread
+                        .buffer()
+                        .read(cx)
+                        .anchor_after(command_range.start)
+                        ..text_thread
+                            .buffer()
+                            .read(cx)
+                            .anchor_after(command_range.end);
+                    text_thread.insert_command_output(
                         command_range,
                         "/command",
                         Task::ready(Ok(stream::iter(events).boxed())),
@@ -922,8 +932,8 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
                 mutation_count -= 1;
             }
             75..=84 if mutation_count > 0 => {
-                context.update(cx, |context, cx| {
-                    if let Some(message) = context.messages(cx).choose(&mut rng) {
+                text_thread.update(cx, |text_thread, cx| {
+                    if let Some(message) = text_thread.messages(cx).choose(&mut rng) {
                         let new_status = match rng.random_range(0..3) {
                             0 => MessageStatus::Done,
                             1 => MessageStatus::Pending,
@@ -935,7 +945,7 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
                             message.id,
                             new_status
                         );
-                        context.update_metadata(message.id, cx, |metadata| {
+                        text_thread.update_metadata(message.id, cx, |metadata| {
                             metadata.status = new_status;
                         });
                     }
@@ -943,13 +953,13 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
                 mutation_count -= 1;
             }
             _ => {
-                let replica_id = context_index as ReplicaId;
+                let replica_id = ReplicaId::new(context_index as u16);
                 if network.lock().is_disconnected(replica_id) {
-                    network.lock().reconnect_peer(replica_id, 0);
+                    network.lock().reconnect_peer(replica_id, ReplicaId::new(0));
 
                     let (ops_to_send, ops_to_receive) = cx.read(|cx| {
-                        let host_context = &contexts[0].read(cx);
-                        let guest_context = context.read(cx);
+                        let host_context = &text_threads[0].read(cx);
+                        let guest_context = text_thread.read(cx);
                         (
                             guest_context.serialize_ops(&host_context.version(cx), cx),
                             host_context.serialize_ops(&guest_context.version(cx), cx),
@@ -959,7 +969,7 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
                     let ops_to_receive = ops_to_receive
                         .await
                         .into_iter()
-                        .map(ContextOperation::from_proto)
+                        .map(TextThreadOperation::from_proto)
                         .collect::<Result<Vec<_>>>()
                         .unwrap();
                     log::info!(
@@ -970,8 +980,10 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
                     );
 
                     network.lock().broadcast(replica_id, ops_to_send);
-                    context.update(cx, |context, cx| context.apply_ops(ops_to_receive, cx));
-                } else if rng.random_bool(0.1) && replica_id != 0 {
+                    text_thread.update(cx, |text_thread, cx| {
+                        text_thread.apply_ops(ops_to_receive, cx)
+                    });
+                } else if rng.random_bool(0.1) && replica_id != ReplicaId::new(0) {
                     log::info!("Context {}: disconnecting", context_index);
                     network.lock().disconnect_peer(replica_id);
                 } else if network.lock().has_unreceived(replica_id) {
@@ -979,43 +991,43 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
                     let ops = network.lock().receive(replica_id);
                     let ops = ops
                         .into_iter()
-                        .map(ContextOperation::from_proto)
+                        .map(TextThreadOperation::from_proto)
                         .collect::<Result<Vec<_>>>()
                         .unwrap();
-                    context.update(cx, |context, cx| context.apply_ops(ops, cx));
+                    text_thread.update(cx, |text_thread, cx| text_thread.apply_ops(ops, cx));
                 }
             }
         }
     }
 
     cx.read(|cx| {
-        let first_context = contexts[0].read(cx);
-        for context in &contexts[1..] {
-            let context = context.read(cx);
-            assert!(context.pending_ops.is_empty(), "pending ops: {:?}", context.pending_ops);
+        let first_context = text_threads[0].read(cx);
+        for text_thread in &text_threads[1..] {
+            let text_thread = text_thread.read(cx);
+            assert!(text_thread.pending_ops.is_empty(), "pending ops: {:?}", text_thread.pending_ops);
             assert_eq!(
-                context.buffer.read(cx).text(),
-                first_context.buffer.read(cx).text(),
-                "Context {} text != Context 0 text",
-                context.buffer.read(cx).replica_id()
+                text_thread.buffer().read(cx).text(),
+                first_context.buffer().read(cx).text(),
+                "Context {:?} text != Context 0 text",
+                text_thread.buffer().read(cx).replica_id()
             );
             assert_eq!(
-                context.message_anchors,
+                text_thread.message_anchors,
                 first_context.message_anchors,
-                "Context {} messages != Context 0 messages",
-                context.buffer.read(cx).replica_id()
+                "Context {:?} messages != Context 0 messages",
+                text_thread.buffer().read(cx).replica_id()
             );
             assert_eq!(
-                context.messages_metadata,
+                text_thread.messages_metadata,
                 first_context.messages_metadata,
-                "Context {} message metadata != Context 0 message metadata",
-                context.buffer.read(cx).replica_id()
+                "Context {:?} message metadata != Context 0 message metadata",
+                text_thread.buffer().read(cx).replica_id()
             );
             assert_eq!(
-                context.slash_command_output_sections,
+                text_thread.slash_command_output_sections,
                 first_context.slash_command_output_sections,
-                "Context {} slash command output sections != Context 0 slash command output sections",
-                context.buffer.read(cx).replica_id()
+                "Context {:?} slash command output sections != Context 0 slash command output sections",
+                text_thread.buffer().read(cx).replica_id()
             );
         }
     });
@@ -1027,8 +1039,8 @@ fn test_mark_cache_anchors(cx: &mut App) {
 
     let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
     let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
-    let context = cx.new(|cx| {
-        AssistantContext::local(
+    let text_thread = cx.new(|cx| {
+        TextThread::local(
             registry,
             None,
             None,
@@ -1037,7 +1049,7 @@ fn test_mark_cache_anchors(cx: &mut App) {
             cx,
         )
     });
-    let buffer = context.read(cx).buffer.clone();
+    let buffer = text_thread.read(cx).buffer().clone();
 
     // Create a test cache configuration
     let cache_configuration = &Some(LanguageModelCacheConfiguration {
@@ -1046,14 +1058,14 @@ fn test_mark_cache_anchors(cx: &mut App) {
         min_total_token: 10,
     });
 
-    let message_1 = context.read(cx).message_anchors[0].clone();
+    let message_1 = text_thread.read(cx).message_anchors[0].clone();
 
-    context.update(cx, |context, cx| {
-        context.mark_cache_anchors(cache_configuration, false, cx)
+    text_thread.update(cx, |text_thread, cx| {
+        text_thread.mark_cache_anchors(cache_configuration, false, cx)
     });
 
     assert_eq!(
-        messages_cache(&context, cx)
+        messages_cache(&text_thread, cx)
             .iter()
             .filter(|(_, cache)| cache.as_ref().is_some_and(|cache| cache.is_anchor))
             .count(),
@@ -1062,41 +1074,41 @@ fn test_mark_cache_anchors(cx: &mut App) {
     );
 
     buffer.update(cx, |buffer, cx| buffer.edit([(0..0, "aaa")], None, cx));
-    let message_2 = context
-        .update(cx, |context, cx| {
-            context.insert_message_after(message_1.id, Role::User, MessageStatus::Pending, cx)
+    let message_2 = text_thread
+        .update(cx, |text_thread, cx| {
+            text_thread.insert_message_after(message_1.id, Role::User, MessageStatus::Pending, cx)
         })
         .unwrap();
 
     buffer.update(cx, |buffer, cx| buffer.edit([(4..4, "bbbbbbb")], None, cx));
-    let message_3 = context
-        .update(cx, |context, cx| {
-            context.insert_message_after(message_2.id, Role::User, MessageStatus::Pending, cx)
+    let message_3 = text_thread
+        .update(cx, |text_thread, cx| {
+            text_thread.insert_message_after(message_2.id, Role::User, MessageStatus::Pending, cx)
         })
         .unwrap();
     buffer.update(cx, |buffer, cx| buffer.edit([(12..12, "cccccc")], None, cx));
 
-    context.update(cx, |context, cx| {
-        context.mark_cache_anchors(cache_configuration, false, cx)
+    text_thread.update(cx, |text_thread, cx| {
+        text_thread.mark_cache_anchors(cache_configuration, false, cx)
     });
     assert_eq!(buffer.read(cx).text(), "aaa\nbbbbbbb\ncccccc");
     assert_eq!(
-        messages_cache(&context, cx)
+        messages_cache(&text_thread, cx)
             .iter()
             .filter(|(_, cache)| cache.as_ref().is_some_and(|cache| cache.is_anchor))
             .count(),
         0,
         "Messages should not be marked for cache before going over the token minimum."
     );
-    context.update(cx, |context, _| {
-        context.token_count = Some(20);
+    text_thread.update(cx, |text_thread, _| {
+        text_thread.token_count = Some(20);
     });
 
-    context.update(cx, |context, cx| {
-        context.mark_cache_anchors(cache_configuration, true, cx)
+    text_thread.update(cx, |text_thread, cx| {
+        text_thread.mark_cache_anchors(cache_configuration, true, cx)
     });
     assert_eq!(
-        messages_cache(&context, cx)
+        messages_cache(&text_thread, cx)
             .iter()
             .map(|(_, cache)| cache.as_ref().is_some_and(|cache| cache.is_anchor))
             .collect::<Vec<bool>>(),
@@ -1104,28 +1116,33 @@ fn test_mark_cache_anchors(cx: &mut App) {
         "Last message should not be an anchor on speculative request."
     );
 
-    context
-        .update(cx, |context, cx| {
-            context.insert_message_after(message_3.id, Role::Assistant, MessageStatus::Pending, cx)
+    text_thread
+        .update(cx, |text_thread, cx| {
+            text_thread.insert_message_after(
+                message_3.id,
+                Role::Assistant,
+                MessageStatus::Pending,
+                cx,
+            )
         })
         .unwrap();
 
-    context.update(cx, |context, cx| {
-        context.mark_cache_anchors(cache_configuration, false, cx)
+    text_thread.update(cx, |text_thread, cx| {
+        text_thread.mark_cache_anchors(cache_configuration, false, cx)
     });
     assert_eq!(
-        messages_cache(&context, cx)
+        messages_cache(&text_thread, cx)
             .iter()
             .map(|(_, cache)| cache.as_ref().is_some_and(|cache| cache.is_anchor))
             .collect::<Vec<bool>>(),
         vec![false, true, true, false],
         "Most recent message should also be cached if not a speculative request."
     );
-    context.update(cx, |context, cx| {
-        context.update_cache_status_for_completion(cx)
+    text_thread.update(cx, |text_thread, cx| {
+        text_thread.update_cache_status_for_completion(cx)
     });
     assert_eq!(
-        messages_cache(&context, cx)
+        messages_cache(&text_thread, cx)
             .iter()
             .map(|(_, cache)| cache
                 .as_ref()
@@ -1141,11 +1158,11 @@ fn test_mark_cache_anchors(cx: &mut App) {
     );
 
     buffer.update(cx, |buffer, cx| buffer.edit([(14..14, "d")], None, cx));
-    context.update(cx, |context, cx| {
-        context.mark_cache_anchors(cache_configuration, false, cx)
+    text_thread.update(cx, |text_thread, cx| {
+        text_thread.mark_cache_anchors(cache_configuration, false, cx)
     });
     assert_eq!(
-        messages_cache(&context, cx)
+        messages_cache(&text_thread, cx)
             .iter()
             .map(|(_, cache)| cache
                 .as_ref()
@@ -1160,11 +1177,11 @@ fn test_mark_cache_anchors(cx: &mut App) {
         "Modifying a message should invalidate it's cache but leave previous messages."
     );
     buffer.update(cx, |buffer, cx| buffer.edit([(2..2, "e")], None, cx));
-    context.update(cx, |context, cx| {
-        context.mark_cache_anchors(cache_configuration, false, cx)
+    text_thread.update(cx, |text_thread, cx| {
+        text_thread.mark_cache_anchors(cache_configuration, false, cx)
     });
     assert_eq!(
-        messages_cache(&context, cx)
+        messages_cache(&text_thread, cx)
             .iter()
             .map(|(_, cache)| cache
                 .as_ref()
@@ -1182,31 +1199,36 @@ fn test_mark_cache_anchors(cx: &mut App) {
 
 #[gpui::test]
 async fn test_summarization(cx: &mut TestAppContext) {
-    let (context, fake_model) = setup_context_editor_with_fake_model(cx);
+    let (text_thread, fake_model) = setup_context_editor_with_fake_model(cx);
 
     // Initial state should be pending
-    context.read_with(cx, |context, _| {
-        assert!(matches!(context.summary(), ContextSummary::Pending));
-        assert_eq!(context.summary().or_default(), ContextSummary::DEFAULT);
+    text_thread.read_with(cx, |text_thread, _| {
+        assert!(matches!(text_thread.summary(), TextThreadSummary::Pending));
+        assert_eq!(
+            text_thread.summary().or_default(),
+            TextThreadSummary::DEFAULT
+        );
     });
 
-    let message_1 = context.read_with(cx, |context, _cx| context.message_anchors[0].clone());
-    context.update(cx, |context, cx| {
+    let message_1 = text_thread.read_with(cx, |text_thread, _cx| {
+        text_thread.message_anchors[0].clone()
+    });
+    text_thread.update(cx, |context, cx| {
         context
             .insert_message_after(message_1.id, Role::Assistant, MessageStatus::Done, cx)
             .unwrap();
     });
 
     // Send a message
-    context.update(cx, |context, cx| {
-        context.assist(cx);
+    text_thread.update(cx, |text_thread, cx| {
+        text_thread.assist(cx);
     });
 
     simulate_successful_response(&fake_model, cx);
 
     // Should start generating summary when there are >= 2 messages
-    context.read_with(cx, |context, _| {
-        assert!(!context.summary().content().unwrap().done);
+    text_thread.read_with(cx, |text_thread, _| {
+        assert!(!text_thread.summary().content().unwrap().done);
     });
 
     cx.run_until_parked();
@@ -1216,61 +1238,61 @@ async fn test_summarization(cx: &mut TestAppContext) {
     cx.run_until_parked();
 
     // Summary should be set
-    context.read_with(cx, |context, _| {
-        assert_eq!(context.summary().or_default(), "Brief Introduction");
+    text_thread.read_with(cx, |text_thread, _| {
+        assert_eq!(text_thread.summary().or_default(), "Brief Introduction");
     });
 
     // We should be able to manually set a summary
-    context.update(cx, |context, cx| {
-        context.set_custom_summary("Brief Intro".into(), cx);
+    text_thread.update(cx, |text_thread, cx| {
+        text_thread.set_custom_summary("Brief Intro".into(), cx);
     });
 
-    context.read_with(cx, |context, _| {
-        assert_eq!(context.summary().or_default(), "Brief Intro");
+    text_thread.read_with(cx, |text_thread, _| {
+        assert_eq!(text_thread.summary().or_default(), "Brief Intro");
     });
 }
 
 #[gpui::test]
 async fn test_thread_summary_error_set_manually(cx: &mut TestAppContext) {
-    let (context, fake_model) = setup_context_editor_with_fake_model(cx);
+    let (text_thread, fake_model) = setup_context_editor_with_fake_model(cx);
 
-    test_summarize_error(&fake_model, &context, cx);
+    test_summarize_error(&fake_model, &text_thread, cx);
 
     // Now we should be able to set a summary
-    context.update(cx, |context, cx| {
-        context.set_custom_summary("Brief Intro".into(), cx);
+    text_thread.update(cx, |text_thread, cx| {
+        text_thread.set_custom_summary("Brief Intro".into(), cx);
     });
 
-    context.read_with(cx, |context, _| {
-        assert_eq!(context.summary().or_default(), "Brief Intro");
+    text_thread.read_with(cx, |text_thread, _| {
+        assert_eq!(text_thread.summary().or_default(), "Brief Intro");
     });
 }
 
 #[gpui::test]
 async fn test_thread_summary_error_retry(cx: &mut TestAppContext) {
-    let (context, fake_model) = setup_context_editor_with_fake_model(cx);
+    let (text_thread, fake_model) = setup_context_editor_with_fake_model(cx);
 
-    test_summarize_error(&fake_model, &context, cx);
+    test_summarize_error(&fake_model, &text_thread, cx);
 
     // Sending another message should not trigger another summarize request
-    context.update(cx, |context, cx| {
-        context.assist(cx);
+    text_thread.update(cx, |text_thread, cx| {
+        text_thread.assist(cx);
     });
 
     simulate_successful_response(&fake_model, cx);
 
-    context.read_with(cx, |context, _| {
+    text_thread.read_with(cx, |text_thread, _| {
         // State is still Error, not Generating
-        assert!(matches!(context.summary(), ContextSummary::Error));
+        assert!(matches!(text_thread.summary(), TextThreadSummary::Error));
     });
 
     // But the summarize request can be invoked manually
-    context.update(cx, |context, cx| {
-        context.summarize(true, cx);
+    text_thread.update(cx, |text_thread, cx| {
+        text_thread.summarize(true, cx);
     });
 
-    context.read_with(cx, |context, _| {
-        assert!(!context.summary().content().unwrap().done);
+    text_thread.read_with(cx, |text_thread, _| {
+        assert!(!text_thread.summary().content().unwrap().done);
     });
 
     cx.run_until_parked();

crates/assistant_context/src/assistant_context.rs → crates/assistant_text_thread/src/text_thread.rs 🔗

@@ -1,7 +1,3 @@
-#[cfg(test)]
-mod assistant_context_tests;
-mod context_store;
-
 use agent_settings::{AgentSettings, SUMMARIZE_THREAD_PROMPT};
 use anyhow::{Context as _, Result, bail};
 use assistant_slash_command::{
@@ -9,7 +5,7 @@ use assistant_slash_command::{
     SlashCommandResult, SlashCommandWorkingSet,
 };
 use assistant_slash_commands::FileCommandMetadata;
-use client::{self, Client, ModelRequestUsage, RequestUsage, proto, telemetry::Telemetry};
+use client::{self, ModelRequestUsage, RequestUsage, proto, telemetry::Telemetry};
 use clock::ReplicaId;
 use cloud_llm_client::{CompletionIntent, CompletionRequestStatus, UsageLimit};
 use collections::{HashMap, HashSet};
@@ -27,7 +23,7 @@ use language_model::{
     report_assistant_event,
 };
 use open_ai::Model as OpenAiModel;
-use paths::contexts_dir;
+use paths::text_threads_dir;
 use project::Project;
 use prompt_store::PromptBuilder;
 use serde::{Deserialize, Serialize};
@@ -48,16 +44,10 @@ use ui::IconName;
 use util::{ResultExt, TryFutureExt, post_inc};
 use uuid::Uuid;
 
-pub use crate::context_store::*;
-
-pub fn init(client: Arc<Client>, _: &mut App) {
-    context_store::init(&client.into());
-}
-
 #[derive(Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
-pub struct ContextId(String);
+pub struct TextThreadId(String);
 
-impl ContextId {
+impl TextThreadId {
     pub fn new() -> Self {
         Self(Uuid::new_v4().to_string())
     }
@@ -130,7 +120,7 @@ impl MessageStatus {
 }
 
 #[derive(Clone, Debug)]
-pub enum ContextOperation {
+pub enum TextThreadOperation {
     InsertMessage {
         anchor: MessageAnchor,
         metadata: MessageMetadata,
@@ -142,7 +132,7 @@ pub enum ContextOperation {
         version: clock::Global,
     },
     UpdateSummary {
-        summary: ContextSummaryContent,
+        summary: TextThreadSummaryContent,
         version: clock::Global,
     },
     SlashCommandStarted {
@@ -170,7 +160,7 @@ pub enum ContextOperation {
     BufferOperation(language::Operation),
 }
 
-impl ContextOperation {
+impl TextThreadOperation {
     pub fn from_proto(op: proto::ContextOperation) -> Result<Self> {
         match op.variant.context("invalid variant")? {
             proto::context_operation::Variant::InsertMessage(insert) => {
@@ -212,7 +202,7 @@ impl ContextOperation {
                 version: language::proto::deserialize_version(&update.version),
             }),
             proto::context_operation::Variant::UpdateSummary(update) => Ok(Self::UpdateSummary {
-                summary: ContextSummaryContent {
+                summary: TextThreadSummaryContent {
                     text: update.summary,
                     done: update.done,
                     timestamp: language::proto::deserialize_timestamp(
@@ -453,7 +443,7 @@ impl ContextOperation {
 }
 
 #[derive(Debug, Clone)]
-pub enum ContextEvent {
+pub enum TextThreadEvent {
     ShowAssistError(SharedString),
     ShowPaymentRequiredError,
     MessagesEdited,
@@ -476,24 +466,24 @@ pub enum ContextEvent {
     SlashCommandOutputSectionAdded {
         section: SlashCommandOutputSection<language::Anchor>,
     },
-    Operation(ContextOperation),
+    Operation(TextThreadOperation),
 }
 
 #[derive(Clone, Debug, Eq, PartialEq)]
-pub enum ContextSummary {
+pub enum TextThreadSummary {
     Pending,
-    Content(ContextSummaryContent),
+    Content(TextThreadSummaryContent),
     Error,
 }
 
-#[derive(Default, Clone, Debug, Eq, PartialEq)]
-pub struct ContextSummaryContent {
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct TextThreadSummaryContent {
     pub text: String,
     pub done: bool,
     pub timestamp: clock::Lamport,
 }
 
-impl ContextSummary {
+impl TextThreadSummary {
     pub const DEFAULT: &str = "New Text Thread";
 
     pub fn or_default(&self) -> SharedString {
@@ -505,44 +495,48 @@ impl ContextSummary {
             .map_or_else(|| message.into(), |content| content.text.clone().into())
     }
 
-    pub fn content(&self) -> Option<&ContextSummaryContent> {
+    pub fn content(&self) -> Option<&TextThreadSummaryContent> {
         match self {
-            ContextSummary::Content(content) => Some(content),
-            ContextSummary::Pending | ContextSummary::Error => None,
+            TextThreadSummary::Content(content) => Some(content),
+            TextThreadSummary::Pending | TextThreadSummary::Error => None,
         }
     }
 
-    fn content_as_mut(&mut self) -> Option<&mut ContextSummaryContent> {
+    fn content_as_mut(&mut self) -> Option<&mut TextThreadSummaryContent> {
         match self {
-            ContextSummary::Content(content) => Some(content),
-            ContextSummary::Pending | ContextSummary::Error => None,
+            TextThreadSummary::Content(content) => Some(content),
+            TextThreadSummary::Pending | TextThreadSummary::Error => None,
         }
     }
 
-    fn content_or_set_empty(&mut self) -> &mut ContextSummaryContent {
+    fn content_or_set_empty(&mut self) -> &mut TextThreadSummaryContent {
         match self {
-            ContextSummary::Content(content) => content,
-            ContextSummary::Pending | ContextSummary::Error => {
-                let content = ContextSummaryContent::default();
-                *self = ContextSummary::Content(content);
+            TextThreadSummary::Content(content) => content,
+            TextThreadSummary::Pending | TextThreadSummary::Error => {
+                let content = TextThreadSummaryContent {
+                    text: "".to_string(),
+                    done: false,
+                    timestamp: clock::Lamport::MIN,
+                };
+                *self = TextThreadSummary::Content(content);
                 self.content_as_mut().unwrap()
             }
         }
     }
 
     pub fn is_pending(&self) -> bool {
-        matches!(self, ContextSummary::Pending)
+        matches!(self, TextThreadSummary::Pending)
     }
 
     fn timestamp(&self) -> Option<clock::Lamport> {
         match self {
-            ContextSummary::Content(content) => Some(content.timestamp),
-            ContextSummary::Pending | ContextSummary::Error => None,
+            TextThreadSummary::Content(content) => Some(content.timestamp),
+            TextThreadSummary::Pending | TextThreadSummary::Error => None,
         }
     }
 }
 
-impl PartialOrd for ContextSummary {
+impl PartialOrd for TextThreadSummary {
     fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
         self.timestamp().partial_cmp(&other.timestamp())
     }
@@ -664,27 +658,27 @@ struct PendingCompletion {
 #[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)]
 pub struct InvokedSlashCommandId(clock::Lamport);
 
-pub struct AssistantContext {
-    id: ContextId,
+pub struct TextThread {
+    id: TextThreadId,
     timestamp: clock::Lamport,
     version: clock::Global,
-    pending_ops: Vec<ContextOperation>,
-    operations: Vec<ContextOperation>,
+    pub(crate) pending_ops: Vec<TextThreadOperation>,
+    operations: Vec<TextThreadOperation>,
     buffer: Entity<Buffer>,
-    parsed_slash_commands: Vec<ParsedSlashCommand>,
+    pub(crate) parsed_slash_commands: Vec<ParsedSlashCommand>,
     invoked_slash_commands: HashMap<InvokedSlashCommandId, InvokedSlashCommand>,
     edits_since_last_parse: language::Subscription,
     slash_commands: Arc<SlashCommandWorkingSet>,
-    slash_command_output_sections: Vec<SlashCommandOutputSection<language::Anchor>>,
+    pub(crate) slash_command_output_sections: Vec<SlashCommandOutputSection<language::Anchor>>,
     thought_process_output_sections: Vec<ThoughtProcessOutputSection<language::Anchor>>,
-    message_anchors: Vec<MessageAnchor>,
+    pub(crate) message_anchors: Vec<MessageAnchor>,
     contents: Vec<Content>,
-    messages_metadata: HashMap<MessageId, MessageMetadata>,
-    summary: ContextSummary,
+    pub(crate) messages_metadata: HashMap<MessageId, MessageMetadata>,
+    summary: TextThreadSummary,
     summary_task: Task<Option<()>>,
     completion_count: usize,
     pending_completions: Vec<PendingCompletion>,
-    token_count: Option<u64>,
+    pub(crate) token_count: Option<u64>,
     pending_token_count: Task<Option<()>>,
     pending_save: Task<Result<()>>,
     pending_cache_warming_task: Task<Option<()>>,
@@ -707,9 +701,9 @@ impl ContextAnnotation for ParsedSlashCommand {
     }
 }
 
-impl EventEmitter<ContextEvent> for AssistantContext {}
+impl EventEmitter<TextThreadEvent> for TextThread {}
 
-impl AssistantContext {
+impl TextThread {
     pub fn local(
         language_registry: Arc<LanguageRegistry>,
         project: Option<Entity<Project>>,
@@ -719,7 +713,7 @@ impl AssistantContext {
         cx: &mut Context<Self>,
     ) -> Self {
         Self::new(
-            ContextId::new(),
+            TextThreadId::new(),
             ReplicaId::default(),
             language::Capability::ReadWrite,
             language_registry,
@@ -740,7 +734,7 @@ impl AssistantContext {
     }
 
     pub fn new(
-        id: ContextId,
+        id: TextThreadId,
         replica_id: ReplicaId,
         capability: language::Capability,
         language_registry: Arc<LanguageRegistry>,
@@ -776,7 +770,7 @@ impl AssistantContext {
             slash_command_output_sections: Vec::new(),
             thought_process_output_sections: Vec::new(),
             edits_since_last_parse: edits_since_last_slash_command_parse,
-            summary: ContextSummary::Pending,
+            summary: TextThreadSummary::Pending,
             summary_task: Task::ready(None),
             completion_count: Default::default(),
             pending_completions: Default::default(),
@@ -796,7 +790,7 @@ impl AssistantContext {
         };
 
         let first_message_id = MessageId(clock::Lamport {
-            replica_id: 0,
+            replica_id: ReplicaId::LOCAL,
             value: 0,
         });
         let message = MessageAnchor {
@@ -819,12 +813,12 @@ impl AssistantContext {
         this
     }
 
-    pub(crate) fn serialize(&self, cx: &App) -> SavedContext {
+    pub(crate) fn serialize(&self, cx: &App) -> SavedTextThread {
         let buffer = self.buffer.read(cx);
-        SavedContext {
+        SavedTextThread {
             id: Some(self.id.clone()),
             zed: "context".into(),
-            version: SavedContext::VERSION.into(),
+            version: SavedTextThread::VERSION.into(),
             text: buffer.text(),
             messages: self
                 .messages(cx)
@@ -872,7 +866,7 @@ impl AssistantContext {
     }
 
     pub fn deserialize(
-        saved_context: SavedContext,
+        saved_context: SavedTextThread,
         path: Arc<Path>,
         language_registry: Arc<LanguageRegistry>,
         prompt_builder: Arc<PromptBuilder>,
@@ -881,7 +875,7 @@ impl AssistantContext {
         telemetry: Option<Arc<Telemetry>>,
         cx: &mut Context<Self>,
     ) -> Self {
-        let id = saved_context.id.clone().unwrap_or_else(ContextId::new);
+        let id = saved_context.id.clone().unwrap_or_else(TextThreadId::new);
         let mut this = Self::new(
             id,
             ReplicaId::default(),
@@ -902,7 +896,7 @@ impl AssistantContext {
         this
     }
 
-    pub fn id(&self) -> &ContextId {
+    pub fn id(&self) -> &TextThreadId {
         &self.id
     }
 
@@ -910,9 +904,9 @@ impl AssistantContext {
         self.timestamp.replica_id
     }
 
-    pub fn version(&self, cx: &App) -> ContextVersion {
-        ContextVersion {
-            context: self.version.clone(),
+    pub fn version(&self, cx: &App) -> TextThreadVersion {
+        TextThreadVersion {
+            text_thread: self.version.clone(),
             buffer: self.buffer.read(cx).version(),
         }
     }
@@ -934,7 +928,7 @@ impl AssistantContext {
 
     pub fn serialize_ops(
         &self,
-        since: &ContextVersion,
+        since: &TextThreadVersion,
         cx: &App,
     ) -> Task<Vec<proto::ContextOperation>> {
         let buffer_ops = self
@@ -945,7 +939,7 @@ impl AssistantContext {
         let mut context_ops = self
             .operations
             .iter()
-            .filter(|op| !since.context.observed(op.timestamp()))
+            .filter(|op| !since.text_thread.observed(op.timestamp()))
             .cloned()
             .collect::<Vec<_>>();
         context_ops.extend(self.pending_ops.iter().cloned());
@@ -969,13 +963,13 @@ impl AssistantContext {
 
     pub fn apply_ops(
         &mut self,
-        ops: impl IntoIterator<Item = ContextOperation>,
+        ops: impl IntoIterator<Item = TextThreadOperation>,
         cx: &mut Context<Self>,
     ) {
         let mut buffer_ops = Vec::new();
         for op in ops {
             match op {
-                ContextOperation::BufferOperation(buffer_op) => buffer_ops.push(buffer_op),
+                TextThreadOperation::BufferOperation(buffer_op) => buffer_ops.push(buffer_op),
                 op @ _ => self.pending_ops.push(op),
             }
         }
@@ -984,7 +978,7 @@ impl AssistantContext {
         self.flush_ops(cx);
     }
 
-    fn flush_ops(&mut self, cx: &mut Context<AssistantContext>) {
+    fn flush_ops(&mut self, cx: &mut Context<TextThread>) {
         let mut changed_messages = HashSet::default();
         let mut summary_generated = false;
 
@@ -997,7 +991,7 @@ impl AssistantContext {
 
             let timestamp = op.timestamp();
             match op.clone() {
-                ContextOperation::InsertMessage {
+                TextThreadOperation::InsertMessage {
                     anchor, metadata, ..
                 } => {
                     if self.messages_metadata.contains_key(&anchor.id) {
@@ -1007,7 +1001,7 @@ impl AssistantContext {
                         self.insert_message(anchor, metadata, cx);
                     }
                 }
-                ContextOperation::UpdateMessage {
+                TextThreadOperation::UpdateMessage {
                     message_id,
                     metadata: new_metadata,
                     ..
@@ -1018,7 +1012,7 @@ impl AssistantContext {
                         changed_messages.insert(message_id);
                     }
                 }
-                ContextOperation::UpdateSummary {
+                TextThreadOperation::UpdateSummary {
                     summary: new_summary,
                     ..
                 } => {
@@ -1027,11 +1021,11 @@ impl AssistantContext {
                         .timestamp()
                         .is_none_or(|current_timestamp| new_summary.timestamp > current_timestamp)
                     {
-                        self.summary = ContextSummary::Content(new_summary);
+                        self.summary = TextThreadSummary::Content(new_summary);
                         summary_generated = true;
                     }
                 }
-                ContextOperation::SlashCommandStarted {
+                TextThreadOperation::SlashCommandStarted {
                     id,
                     output_range,
                     name,
@@ -1048,9 +1042,9 @@ impl AssistantContext {
                             timestamp: id.0,
                         },
                     );
-                    cx.emit(ContextEvent::InvokedSlashCommandChanged { command_id: id });
+                    cx.emit(TextThreadEvent::InvokedSlashCommandChanged { command_id: id });
                 }
-                ContextOperation::SlashCommandOutputSectionAdded { section, .. } => {
+                TextThreadOperation::SlashCommandOutputSectionAdded { section, .. } => {
                     let buffer = self.buffer.read(cx);
                     if let Err(ix) = self
                         .slash_command_output_sections
@@ -1058,10 +1052,10 @@ impl AssistantContext {
                     {
                         self.slash_command_output_sections
                             .insert(ix, section.clone());
-                        cx.emit(ContextEvent::SlashCommandOutputSectionAdded { section });
+                        cx.emit(TextThreadEvent::SlashCommandOutputSectionAdded { section });
                     }
                 }
-                ContextOperation::ThoughtProcessOutputSectionAdded { section, .. } => {
+                TextThreadOperation::ThoughtProcessOutputSectionAdded { section, .. } => {
                     let buffer = self.buffer.read(cx);
                     if let Err(ix) = self
                         .thought_process_output_sections
@@ -1071,7 +1065,7 @@ impl AssistantContext {
                             .insert(ix, section.clone());
                     }
                 }
-                ContextOperation::SlashCommandFinished {
+                TextThreadOperation::SlashCommandFinished {
                     id,
                     error_message,
                     timestamp,
@@ -1090,10 +1084,10 @@ impl AssistantContext {
                                 slash_command.status = InvokedSlashCommandStatus::Finished;
                             }
                         }
-                        cx.emit(ContextEvent::InvokedSlashCommandChanged { command_id: id });
+                        cx.emit(TextThreadEvent::InvokedSlashCommandChanged { command_id: id });
                     }
                 }
-                ContextOperation::BufferOperation(_) => unreachable!(),
+                TextThreadOperation::BufferOperation(_) => unreachable!(),
             }
 
             self.version.observe(timestamp);
@@ -1103,43 +1097,43 @@ impl AssistantContext {
 
         if !changed_messages.is_empty() {
             self.message_roles_updated(changed_messages, cx);
-            cx.emit(ContextEvent::MessagesEdited);
+            cx.emit(TextThreadEvent::MessagesEdited);
             cx.notify();
         }
 
         if summary_generated {
-            cx.emit(ContextEvent::SummaryChanged);
-            cx.emit(ContextEvent::SummaryGenerated);
+            cx.emit(TextThreadEvent::SummaryChanged);
+            cx.emit(TextThreadEvent::SummaryGenerated);
             cx.notify();
         }
     }
 
-    fn can_apply_op(&self, op: &ContextOperation, cx: &App) -> bool {
+    fn can_apply_op(&self, op: &TextThreadOperation, cx: &App) -> bool {
         if !self.version.observed_all(op.version()) {
             return false;
         }
 
         match op {
-            ContextOperation::InsertMessage { anchor, .. } => self
+            TextThreadOperation::InsertMessage { anchor, .. } => self
                 .buffer
                 .read(cx)
                 .version
                 .observed(anchor.start.timestamp),
-            ContextOperation::UpdateMessage { message_id, .. } => {
+            TextThreadOperation::UpdateMessage { message_id, .. } => {
                 self.messages_metadata.contains_key(message_id)
             }
-            ContextOperation::UpdateSummary { .. } => true,
-            ContextOperation::SlashCommandStarted { output_range, .. } => {
+            TextThreadOperation::UpdateSummary { .. } => true,
+            TextThreadOperation::SlashCommandStarted { output_range, .. } => {
                 self.has_received_operations_for_anchor_range(output_range.clone(), cx)
             }
-            ContextOperation::SlashCommandOutputSectionAdded { section, .. } => {
+            TextThreadOperation::SlashCommandOutputSectionAdded { section, .. } => {
                 self.has_received_operations_for_anchor_range(section.range.clone(), cx)
             }
-            ContextOperation::ThoughtProcessOutputSectionAdded { section, .. } => {
+            TextThreadOperation::ThoughtProcessOutputSectionAdded { section, .. } => {
                 self.has_received_operations_for_anchor_range(section.range.clone(), cx)
             }
-            ContextOperation::SlashCommandFinished { .. } => true,
-            ContextOperation::BufferOperation(_) => {
+            TextThreadOperation::SlashCommandFinished { .. } => true,
+            TextThreadOperation::BufferOperation(_) => {
                 panic!("buffer operations should always be applied")
             }
         }
@@ -1160,9 +1154,9 @@ impl AssistantContext {
         observed_start && observed_end
     }
 
-    fn push_op(&mut self, op: ContextOperation, cx: &mut Context<Self>) {
+    fn push_op(&mut self, op: TextThreadOperation, cx: &mut Context<Self>) {
         self.operations.push(op.clone());
-        cx.emit(ContextEvent::Operation(op));
+        cx.emit(TextThreadEvent::Operation(op));
     }
 
     pub fn buffer(&self) -> &Entity<Buffer> {
@@ -1185,7 +1179,7 @@ impl AssistantContext {
         self.path.as_ref()
     }
 
-    pub fn summary(&self) -> &ContextSummary {
+    pub fn summary(&self) -> &TextThreadSummary {
         &self.summary
     }
 
@@ -1246,13 +1240,13 @@ impl AssistantContext {
             language::BufferEvent::Operation {
                 operation,
                 is_local: true,
-            } => cx.emit(ContextEvent::Operation(ContextOperation::BufferOperation(
-                operation.clone(),
-            ))),
+            } => cx.emit(TextThreadEvent::Operation(
+                TextThreadOperation::BufferOperation(operation.clone()),
+            )),
             language::BufferEvent::Edited => {
                 self.count_remaining_tokens(cx);
                 self.reparse(cx);
-                cx.emit(ContextEvent::MessagesEdited);
+                cx.emit(TextThreadEvent::MessagesEdited);
             }
             _ => {}
         }
@@ -1518,7 +1512,7 @@ impl AssistantContext {
         if !updated_parsed_slash_commands.is_empty()
             || !removed_parsed_slash_command_ranges.is_empty()
         {
-            cx.emit(ContextEvent::ParsedSlashCommandsUpdated {
+            cx.emit(TextThreadEvent::ParsedSlashCommandsUpdated {
                 removed: removed_parsed_slash_command_ranges,
                 updated: updated_parsed_slash_commands,
             });
@@ -1592,7 +1586,7 @@ impl AssistantContext {
                 && (!command.range.start.is_valid(buffer) || !command.range.end.is_valid(buffer))
             {
                 command.status = InvokedSlashCommandStatus::Finished;
-                cx.emit(ContextEvent::InvokedSlashCommandChanged { command_id });
+                cx.emit(TextThreadEvent::InvokedSlashCommandChanged { command_id });
                 invalidated_command_ids.push(command_id);
             }
         }
@@ -1601,7 +1595,7 @@ impl AssistantContext {
             let version = self.version.clone();
             let timestamp = self.next_timestamp();
             self.push_op(
-                ContextOperation::SlashCommandFinished {
+                TextThreadOperation::SlashCommandFinished {
                     id: command_id,
                     timestamp,
                     error_message: None,
@@ -1906,9 +1900,9 @@ impl AssistantContext {
                     }
                 }
 
-                cx.emit(ContextEvent::InvokedSlashCommandChanged { command_id });
+                cx.emit(TextThreadEvent::InvokedSlashCommandChanged { command_id });
                 this.push_op(
-                    ContextOperation::SlashCommandFinished {
+                    TextThreadOperation::SlashCommandFinished {
                         id: command_id,
                         timestamp,
                         error_message,
@@ -1931,9 +1925,9 @@ impl AssistantContext {
                 timestamp: command_id.0,
             },
         );
-        cx.emit(ContextEvent::InvokedSlashCommandChanged { command_id });
+        cx.emit(TextThreadEvent::InvokedSlashCommandChanged { command_id });
         self.push_op(
-            ContextOperation::SlashCommandStarted {
+            TextThreadOperation::SlashCommandStarted {
                 id: command_id,
                 output_range: command_range,
                 name: name.to_string(),
@@ -1957,13 +1951,13 @@ impl AssistantContext {
         };
         self.slash_command_output_sections
             .insert(insertion_ix, section.clone());
-        cx.emit(ContextEvent::SlashCommandOutputSectionAdded {
+        cx.emit(TextThreadEvent::SlashCommandOutputSectionAdded {
             section: section.clone(),
         });
         let version = self.version.clone();
         let timestamp = self.next_timestamp();
         self.push_op(
-            ContextOperation::SlashCommandOutputSectionAdded {
+            TextThreadOperation::SlashCommandOutputSectionAdded {
                 timestamp,
                 section,
                 version,
@@ -1992,7 +1986,7 @@ impl AssistantContext {
         let version = self.version.clone();
         let timestamp = self.next_timestamp();
         self.push_op(
-            ContextOperation::ThoughtProcessOutputSectionAdded {
+            TextThreadOperation::ThoughtProcessOutputSectionAdded {
                 timestamp,
                 section,
                 version,
@@ -2111,7 +2105,7 @@ impl AssistantContext {
                                             let end = buffer
                                                 .anchor_before(message_old_end_offset + chunk_len);
                                             context_event = Some(
-                                                ContextEvent::StartedThoughtProcess(start..end),
+                                                TextThreadEvent::StartedThoughtProcess(start..end),
                                             );
                                         } else {
                                             // This ensures that all the thinking chunks are inserted inside the thinking tag
@@ -2129,7 +2123,7 @@ impl AssistantContext {
                                         if let Some(start) = thought_process_stack.pop() {
                                             let end = buffer.anchor_before(message_old_end_offset);
                                             context_event =
-                                                Some(ContextEvent::EndedThoughtProcess(end));
+                                                Some(TextThreadEvent::EndedThoughtProcess(end));
                                             thought_process_output_section =
                                                 Some(ThoughtProcessOutputSection {
                                                     range: start..end,
@@ -2159,7 +2153,7 @@ impl AssistantContext {
                                 cx.emit(context_event);
                             }
 
-                            cx.emit(ContextEvent::StreamedCompletion);
+                            cx.emit(TextThreadEvent::StreamedCompletion);
 
                             Some(())
                         })?;
@@ -2180,7 +2174,7 @@ impl AssistantContext {
                 this.update(cx, |this, cx| {
                     let error_message = if let Some(error) = result.as_ref().err() {
                         if error.is::<PaymentRequiredError>() {
-                            cx.emit(ContextEvent::ShowPaymentRequiredError);
+                            cx.emit(TextThreadEvent::ShowPaymentRequiredError);
                             this.update_metadata(assistant_message_id, cx, |metadata| {
                                 metadata.status = MessageStatus::Canceled;
                             });
@@ -2191,7 +2185,7 @@ impl AssistantContext {
                                 .map(|err| err.to_string())
                                 .collect::<Vec<_>>()
                                 .join("\n");
-                            cx.emit(ContextEvent::ShowAssistError(SharedString::from(
+                            cx.emit(TextThreadEvent::ShowAssistError(SharedString::from(
                                 error_message.clone(),
                             )));
                             this.update_metadata(assistant_message_id, cx, |metadata| {
@@ -2408,13 +2402,13 @@ impl AssistantContext {
         if let Some(metadata) = self.messages_metadata.get_mut(&id) {
             f(metadata);
             metadata.timestamp = timestamp;
-            let operation = ContextOperation::UpdateMessage {
+            let operation = TextThreadOperation::UpdateMessage {
                 message_id: id,
                 metadata: metadata.clone(),
                 version,
             };
             self.push_op(operation, cx);
-            cx.emit(ContextEvent::MessagesEdited);
+            cx.emit(TextThreadEvent::MessagesEdited);
             cx.notify();
         }
     }
@@ -2478,7 +2472,7 @@ impl AssistantContext {
         };
         self.insert_message(anchor.clone(), metadata.clone(), cx);
         self.push_op(
-            ContextOperation::InsertMessage {
+            TextThreadOperation::InsertMessage {
                 anchor: anchor.clone(),
                 metadata,
                 version,
@@ -2501,7 +2495,7 @@ impl AssistantContext {
             Err(ix) => ix,
         };
         self.contents.insert(insertion_ix, content);
-        cx.emit(ContextEvent::MessagesEdited);
+        cx.emit(TextThreadEvent::MessagesEdited);
     }
 
     pub fn contents<'a>(&'a self, cx: &'a App) -> impl 'a + Iterator<Item = Content> {
@@ -2576,7 +2570,7 @@ impl AssistantContext {
             };
             self.insert_message(suffix.clone(), suffix_metadata.clone(), cx);
             self.push_op(
-                ContextOperation::InsertMessage {
+                TextThreadOperation::InsertMessage {
                     anchor: suffix.clone(),
                     metadata: suffix_metadata,
                     version,
@@ -2626,7 +2620,7 @@ impl AssistantContext {
                     };
                     self.insert_message(selection.clone(), selection_metadata.clone(), cx);
                     self.push_op(
-                        ContextOperation::InsertMessage {
+                        TextThreadOperation::InsertMessage {
                             anchor: selection.clone(),
                             metadata: selection_metadata,
                             version,
@@ -2638,7 +2632,7 @@ impl AssistantContext {
                 };
 
             if !edited_buffer {
-                cx.emit(ContextEvent::MessagesEdited);
+                cx.emit(TextThreadEvent::MessagesEdited);
             }
             new_messages
         } else {
@@ -2652,7 +2646,7 @@ impl AssistantContext {
         new_metadata: MessageMetadata,
         cx: &mut Context<Self>,
     ) {
-        cx.emit(ContextEvent::MessagesEdited);
+        cx.emit(TextThreadEvent::MessagesEdited);
 
         self.messages_metadata.insert(new_anchor.id, new_metadata);
 
@@ -2688,15 +2682,15 @@ impl AssistantContext {
             // If there is no summary, it is set with `done: false` so that "Loading Summary…" can
             // be displayed.
             match self.summary {
-                ContextSummary::Pending | ContextSummary::Error => {
-                    self.summary = ContextSummary::Content(ContextSummaryContent {
+                TextThreadSummary::Pending | TextThreadSummary::Error => {
+                    self.summary = TextThreadSummary::Content(TextThreadSummaryContent {
                         text: "".to_string(),
                         done: false,
-                        timestamp: clock::Lamport::default(),
+                        timestamp: clock::Lamport::MIN,
                     });
                     replace_old = true;
                 }
-                ContextSummary::Content(_) => {}
+                TextThreadSummary::Content(_) => {}
             }
 
             self.summary_task = cx.spawn(async move |this, cx| {
@@ -2718,13 +2712,13 @@ impl AssistantContext {
                             }
                             summary.text.extend(lines.next());
                             summary.timestamp = timestamp;
-                            let operation = ContextOperation::UpdateSummary {
+                            let operation = TextThreadOperation::UpdateSummary {
                                 summary: summary.clone(),
                                 version,
                             };
                             this.push_op(operation, cx);
-                            cx.emit(ContextEvent::SummaryChanged);
-                            cx.emit(ContextEvent::SummaryGenerated);
+                            cx.emit(TextThreadEvent::SummaryChanged);
+                            cx.emit(TextThreadEvent::SummaryGenerated);
                         })?;
 
                         // Stop if the LLM generated multiple lines.
@@ -2748,13 +2742,13 @@ impl AssistantContext {
                         if let Some(summary) = this.summary.content_as_mut() {
                             summary.done = true;
                             summary.timestamp = timestamp;
-                            let operation = ContextOperation::UpdateSummary {
+                            let operation = TextThreadOperation::UpdateSummary {
                                 summary: summary.clone(),
                                 version,
                             };
                             this.push_op(operation, cx);
-                            cx.emit(ContextEvent::SummaryChanged);
-                            cx.emit(ContextEvent::SummaryGenerated);
+                            cx.emit(TextThreadEvent::SummaryChanged);
+                            cx.emit(TextThreadEvent::SummaryGenerated);
                         }
                     })?;
 
@@ -2764,8 +2758,8 @@ impl AssistantContext {
 
                 if let Err(err) = result {
                     this.update(cx, |this, cx| {
-                        this.summary = ContextSummary::Error;
-                        cx.emit(ContextEvent::SummaryChanged);
+                        this.summary = TextThreadSummary::Error;
+                        cx.emit(TextThreadEvent::SummaryChanged);
                     })
                     .log_err();
                     log::error!("Error generating context summary: {}", err);
@@ -2871,7 +2865,7 @@ impl AssistantContext {
         &mut self,
         debounce: Option<Duration>,
         fs: Arc<dyn Fs>,
-        cx: &mut Context<AssistantContext>,
+        cx: &mut Context<TextThread>,
     ) {
         if self.replica_id() != ReplicaId::default() {
             // Prevent saving a remote context for now.
@@ -2902,7 +2896,7 @@ impl AssistantContext {
                 let mut discriminant = 1;
                 let mut new_path;
                 loop {
-                    new_path = contexts_dir().join(&format!(
+                    new_path = text_threads_dir().join(&format!(
                         "{} - {}.zed.json",
                         summary.trim(),
                         discriminant
@@ -2914,7 +2908,7 @@ impl AssistantContext {
                     }
                 }
 
-                fs.create_dir(contexts_dir().as_ref()).await?;
+                fs.create_dir(text_threads_dir().as_ref()).await?;
 
                 // rename before write ensures that only one file exists
                 if let Some(old_path) = old_path.as_ref()
@@ -2936,7 +2930,7 @@ impl AssistantContext {
                     let new_path: Arc<Path> = new_path.clone().into();
                     move |this, cx| {
                         this.path = Some(new_path.clone());
-                        cx.emit(ContextEvent::PathChanged { old_path, new_path });
+                        cx.emit(TextThreadEvent::PathChanged { old_path, new_path });
                     }
                 })
                 .ok();
@@ -2955,7 +2949,7 @@ impl AssistantContext {
         summary.timestamp = timestamp;
         summary.done = true;
         summary.text = custom_summary;
-        cx.emit(ContextEvent::SummaryChanged);
+        cx.emit(TextThreadEvent::SummaryChanged);
     }
 
     fn update_model_request_usage(&self, amount: u32, limit: UsageLimit, cx: &mut App) {
@@ -2975,23 +2969,23 @@ impl AssistantContext {
 }
 
 #[derive(Debug, Default)]
-pub struct ContextVersion {
-    context: clock::Global,
+pub struct TextThreadVersion {
+    text_thread: clock::Global,
     buffer: clock::Global,
 }
 
-impl ContextVersion {
+impl TextThreadVersion {
     pub fn from_proto(proto: &proto::ContextVersion) -> Self {
         Self {
-            context: language::proto::deserialize_version(&proto.context_version),
+            text_thread: language::proto::deserialize_version(&proto.context_version),
             buffer: language::proto::deserialize_version(&proto.buffer_version),
         }
     }
 
-    pub fn to_proto(&self, context_id: ContextId) -> proto::ContextVersion {
+    pub fn to_proto(&self, context_id: TextThreadId) -> proto::ContextVersion {
         proto::ContextVersion {
             context_id: context_id.to_proto(),
-            context_version: language::proto::serialize_version(&self.context),
+            context_version: language::proto::serialize_version(&self.text_thread),
             buffer_version: language::proto::serialize_version(&self.buffer),
         }
     }
@@ -3059,8 +3053,8 @@ pub struct SavedMessage {
 }
 
 #[derive(Serialize, Deserialize)]
-pub struct SavedContext {
-    pub id: Option<ContextId>,
+pub struct SavedTextThread {
+    pub id: Option<TextThreadId>,
     pub zed: String,
     pub version: String,
     pub text: String,
@@ -3072,7 +3066,7 @@ pub struct SavedContext {
     pub thought_process_output_sections: Vec<ThoughtProcessOutputSection<usize>>,
 }
 
-impl SavedContext {
+impl SavedTextThread {
     pub const VERSION: &'static str = "0.4.0";
 
     pub fn from_json(json: &str) -> Result<Self> {
@@ -3082,9 +3076,9 @@ impl SavedContext {
             .context("version not found")?
         {
             serde_json::Value::String(version) => match version.as_str() {
-                SavedContext::VERSION => {
-                    Ok(serde_json::from_value::<SavedContext>(saved_context_json)?)
-                }
+                SavedTextThread::VERSION => Ok(serde_json::from_value::<SavedTextThread>(
+                    saved_context_json,
+                )?),
                 SavedContextV0_3_0::VERSION => {
                     let saved_context =
                         serde_json::from_value::<SavedContextV0_3_0>(saved_context_json)?;
@@ -3109,18 +3103,18 @@ impl SavedContext {
     fn into_ops(
         self,
         buffer: &Entity<Buffer>,
-        cx: &mut Context<AssistantContext>,
-    ) -> Vec<ContextOperation> {
+        cx: &mut Context<TextThread>,
+    ) -> Vec<TextThreadOperation> {
         let mut operations = Vec::new();
         let mut version = clock::Global::new();
         let mut next_timestamp = clock::Lamport::new(ReplicaId::default());
 
         let mut first_message_metadata = None;
         for message in self.messages {
-            if message.id == MessageId(clock::Lamport::default()) {
+            if message.id == MessageId(clock::Lamport::MIN) {
                 first_message_metadata = Some(message.metadata);
             } else {
-                operations.push(ContextOperation::InsertMessage {
+                operations.push(TextThreadOperation::InsertMessage {
                     anchor: MessageAnchor {
                         id: message.id,
                         start: buffer.read(cx).anchor_before(message.start),
@@ -3140,8 +3134,8 @@ impl SavedContext {
 
         if let Some(metadata) = first_message_metadata {
             let timestamp = next_timestamp.tick();
-            operations.push(ContextOperation::UpdateMessage {
-                message_id: MessageId(clock::Lamport::default()),
+            operations.push(TextThreadOperation::UpdateMessage {
+                message_id: MessageId(clock::Lamport::MIN),
                 metadata: MessageMetadata {
                     role: metadata.role,
                     status: metadata.status,
@@ -3156,7 +3150,7 @@ impl SavedContext {
         let buffer = buffer.read(cx);
         for section in self.slash_command_output_sections {
             let timestamp = next_timestamp.tick();
-            operations.push(ContextOperation::SlashCommandOutputSectionAdded {
+            operations.push(TextThreadOperation::SlashCommandOutputSectionAdded {
                 timestamp,
                 section: SlashCommandOutputSection {
                     range: buffer.anchor_after(section.range.start)
@@ -3173,7 +3167,7 @@ impl SavedContext {
 
         for section in self.thought_process_output_sections {
             let timestamp = next_timestamp.tick();
-            operations.push(ContextOperation::ThoughtProcessOutputSectionAdded {
+            operations.push(TextThreadOperation::ThoughtProcessOutputSectionAdded {
                 timestamp,
                 section: ThoughtProcessOutputSection {
                     range: buffer.anchor_after(section.range.start)
@@ -3186,8 +3180,8 @@ impl SavedContext {
         }
 
         let timestamp = next_timestamp.tick();
-        operations.push(ContextOperation::UpdateSummary {
-            summary: ContextSummaryContent {
+        operations.push(TextThreadOperation::UpdateSummary {
+            summary: TextThreadSummaryContent {
                 text: self.summary,
                 done: true,
                 timestamp,
@@ -3217,7 +3211,7 @@ struct SavedMessageMetadataPreV0_4_0 {
 
 #[derive(Serialize, Deserialize)]
 struct SavedContextV0_3_0 {
-    id: Option<ContextId>,
+    id: Option<TextThreadId>,
     zed: String,
     version: String,
     text: String,
@@ -3230,11 +3224,11 @@ struct SavedContextV0_3_0 {
 impl SavedContextV0_3_0 {
     const VERSION: &'static str = "0.3.0";
 
-    fn upgrade(self) -> SavedContext {
-        SavedContext {
+    fn upgrade(self) -> SavedTextThread {
+        SavedTextThread {
             id: self.id,
             zed: self.zed,
-            version: SavedContext::VERSION.into(),
+            version: SavedTextThread::VERSION.into(),
             text: self.text,
             messages: self
                 .messages

crates/assistant_context/src/context_store.rs → crates/assistant_text_thread/src/text_thread_store.rs 🔗

@@ -1,6 +1,6 @@
 use crate::{
-    AssistantContext, ContextEvent, ContextId, ContextOperation, ContextVersion, SavedContext,
-    SavedContextMetadata,
+    SavedTextThread, SavedTextThreadMetadata, TextThread, TextThreadEvent, TextThreadId,
+    TextThreadOperation, TextThreadVersion,
 };
 use anyhow::{Context as _, Result};
 use assistant_slash_command::{SlashCommandId, SlashCommandWorkingSet};
@@ -11,9 +11,9 @@ use context_server::ContextServerId;
 use fs::{Fs, RemoveOptions};
 use futures::StreamExt;
 use fuzzy::StringMatchCandidate;
-use gpui::{App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Task, WeakEntity};
+use gpui::{App, AppContext as _, AsyncApp, Context, Entity, Task, WeakEntity};
 use language::LanguageRegistry;
-use paths::contexts_dir;
+use paths::text_threads_dir;
 use project::{
     Project,
     context_server_store::{ContextServerStatus, ContextServerStore},
@@ -27,24 +27,24 @@ use util::{ResultExt, TryFutureExt};
 use zed_env_vars::ZED_STATELESS;
 
 pub(crate) fn init(client: &AnyProtoClient) {
-    client.add_entity_message_handler(ContextStore::handle_advertise_contexts);
-    client.add_entity_request_handler(ContextStore::handle_open_context);
-    client.add_entity_request_handler(ContextStore::handle_create_context);
-    client.add_entity_message_handler(ContextStore::handle_update_context);
-    client.add_entity_request_handler(ContextStore::handle_synchronize_contexts);
+    client.add_entity_message_handler(TextThreadStore::handle_advertise_contexts);
+    client.add_entity_request_handler(TextThreadStore::handle_open_context);
+    client.add_entity_request_handler(TextThreadStore::handle_create_context);
+    client.add_entity_message_handler(TextThreadStore::handle_update_context);
+    client.add_entity_request_handler(TextThreadStore::handle_synchronize_contexts);
 }
 
 #[derive(Clone)]
-pub struct RemoteContextMetadata {
-    pub id: ContextId,
+pub struct RemoteTextThreadMetadata {
+    pub id: TextThreadId,
     pub summary: Option<String>,
 }
 
-pub struct ContextStore {
-    contexts: Vec<ContextHandle>,
-    contexts_metadata: Vec<SavedContextMetadata>,
+pub struct TextThreadStore {
+    text_threads: Vec<TextThreadHandle>,
+    text_threads_metadata: Vec<SavedTextThreadMetadata>,
     context_server_slash_command_ids: HashMap<ContextServerId, Vec<SlashCommandId>>,
-    host_contexts: Vec<RemoteContextMetadata>,
+    host_text_threads: Vec<RemoteTextThreadMetadata>,
     fs: Arc<dyn Fs>,
     languages: Arc<LanguageRegistry>,
     slash_commands: Arc<SlashCommandWorkingSet>,
@@ -58,34 +58,28 @@ pub struct ContextStore {
     prompt_builder: Arc<PromptBuilder>,
 }
 
-pub enum ContextStoreEvent {
-    ContextCreated(ContextId),
+enum TextThreadHandle {
+    Weak(WeakEntity<TextThread>),
+    Strong(Entity<TextThread>),
 }
 
-impl EventEmitter<ContextStoreEvent> for ContextStore {}
-
-enum ContextHandle {
-    Weak(WeakEntity<AssistantContext>),
-    Strong(Entity<AssistantContext>),
-}
-
-impl ContextHandle {
-    fn upgrade(&self) -> Option<Entity<AssistantContext>> {
+impl TextThreadHandle {
+    fn upgrade(&self) -> Option<Entity<TextThread>> {
         match self {
-            ContextHandle::Weak(weak) => weak.upgrade(),
-            ContextHandle::Strong(strong) => Some(strong.clone()),
+            TextThreadHandle::Weak(weak) => weak.upgrade(),
+            TextThreadHandle::Strong(strong) => Some(strong.clone()),
         }
     }
 
-    fn downgrade(&self) -> WeakEntity<AssistantContext> {
+    fn downgrade(&self) -> WeakEntity<TextThread> {
         match self {
-            ContextHandle::Weak(weak) => weak.clone(),
-            ContextHandle::Strong(strong) => strong.downgrade(),
+            TextThreadHandle::Weak(weak) => weak.clone(),
+            TextThreadHandle::Strong(strong) => strong.downgrade(),
         }
     }
 }
 
-impl ContextStore {
+impl TextThreadStore {
     pub fn new(
         project: Entity<Project>,
         prompt_builder: Arc<PromptBuilder>,
@@ -97,14 +91,14 @@ impl ContextStore {
         let telemetry = project.read(cx).client().telemetry().clone();
         cx.spawn(async move |cx| {
             const CONTEXT_WATCH_DURATION: Duration = Duration::from_millis(100);
-            let (mut events, _) = fs.watch(contexts_dir(), CONTEXT_WATCH_DURATION).await;
+            let (mut events, _) = fs.watch(text_threads_dir(), CONTEXT_WATCH_DURATION).await;
 
             let this = cx.new(|cx: &mut Context<Self>| {
                 let mut this = Self {
-                    contexts: Vec::new(),
-                    contexts_metadata: Vec::new(),
+                    text_threads: Vec::new(),
+                    text_threads_metadata: Vec::new(),
                     context_server_slash_command_ids: HashMap::default(),
-                    host_contexts: Vec::new(),
+                    host_text_threads: Vec::new(),
                     fs,
                     languages,
                     slash_commands,
@@ -142,10 +136,10 @@ impl ContextStore {
     #[cfg(any(test, feature = "test-support"))]
     pub fn fake(project: Entity<Project>, cx: &mut Context<Self>) -> Self {
         Self {
-            contexts: Default::default(),
-            contexts_metadata: Default::default(),
+            text_threads: Default::default(),
+            text_threads_metadata: Default::default(),
             context_server_slash_command_ids: Default::default(),
-            host_contexts: Default::default(),
+            host_text_threads: Default::default(),
             fs: project.read(cx).fs().clone(),
             languages: project.read(cx).languages().clone(),
             slash_commands: Arc::default(),
@@ -166,13 +160,13 @@ impl ContextStore {
         mut cx: AsyncApp,
     ) -> Result<()> {
         this.update(&mut cx, |this, cx| {
-            this.host_contexts = envelope
+            this.host_text_threads = envelope
                 .payload
                 .contexts
                 .into_iter()
-                .map(|context| RemoteContextMetadata {
-                    id: ContextId::from_proto(context.context_id),
-                    summary: context.summary,
+                .map(|text_thread| RemoteTextThreadMetadata {
+                    id: TextThreadId::from_proto(text_thread.context_id),
+                    summary: text_thread.summary,
                 })
                 .collect();
             cx.notify();
@@ -184,25 +178,25 @@ impl ContextStore {
         envelope: TypedEnvelope<proto::OpenContext>,
         mut cx: AsyncApp,
     ) -> Result<proto::OpenContextResponse> {
-        let context_id = ContextId::from_proto(envelope.payload.context_id);
+        let context_id = TextThreadId::from_proto(envelope.payload.context_id);
         let operations = this.update(&mut cx, |this, cx| {
             anyhow::ensure!(
                 !this.project.read(cx).is_via_collab(),
                 "only the host contexts can be opened"
             );
 
-            let context = this
-                .loaded_context_for_id(&context_id, cx)
+            let text_thread = this
+                .loaded_text_thread_for_id(&context_id, cx)
                 .context("context not found")?;
             anyhow::ensure!(
-                context.read(cx).replica_id() == ReplicaId::default(),
+                text_thread.read(cx).replica_id() == ReplicaId::default(),
                 "context must be opened via the host"
             );
 
             anyhow::Ok(
-                context
+                text_thread
                     .read(cx)
-                    .serialize_ops(&ContextVersion::default(), cx),
+                    .serialize_ops(&TextThreadVersion::default(), cx),
             )
         })??;
         let operations = operations.await;
@@ -222,15 +216,14 @@ impl ContextStore {
                 "can only create contexts as the host"
             );
 
-            let context = this.create(cx);
-            let context_id = context.read(cx).id().clone();
-            cx.emit(ContextStoreEvent::ContextCreated(context_id.clone()));
+            let text_thread = this.create(cx);
+            let context_id = text_thread.read(cx).id().clone();
 
             anyhow::Ok((
                 context_id,
-                context
+                text_thread
                     .read(cx)
-                    .serialize_ops(&ContextVersion::default(), cx),
+                    .serialize_ops(&TextThreadVersion::default(), cx),
             ))
         })??;
         let operations = operations.await;
@@ -246,11 +239,11 @@ impl ContextStore {
         mut cx: AsyncApp,
     ) -> Result<()> {
         this.update(&mut cx, |this, cx| {
-            let context_id = ContextId::from_proto(envelope.payload.context_id);
-            if let Some(context) = this.loaded_context_for_id(&context_id, cx) {
+            let context_id = TextThreadId::from_proto(envelope.payload.context_id);
+            if let Some(text_thread) = this.loaded_text_thread_for_id(&context_id, cx) {
                 let operation_proto = envelope.payload.operation.context("invalid operation")?;
-                let operation = ContextOperation::from_proto(operation_proto)?;
-                context.update(cx, |context, cx| context.apply_ops([operation], cx));
+                let operation = TextThreadOperation::from_proto(operation_proto)?;
+                text_thread.update(cx, |text_thread, cx| text_thread.apply_ops([operation], cx));
             }
             Ok(())
         })?
@@ -269,12 +262,12 @@ impl ContextStore {
 
             let mut local_versions = Vec::new();
             for remote_version_proto in envelope.payload.contexts {
-                let remote_version = ContextVersion::from_proto(&remote_version_proto);
-                let context_id = ContextId::from_proto(remote_version_proto.context_id);
-                if let Some(context) = this.loaded_context_for_id(&context_id, cx) {
-                    let context = context.read(cx);
-                    let operations = context.serialize_ops(&remote_version, cx);
-                    local_versions.push(context.version(cx).to_proto(context_id.clone()));
+                let remote_version = TextThreadVersion::from_proto(&remote_version_proto);
+                let context_id = TextThreadId::from_proto(remote_version_proto.context_id);
+                if let Some(text_thread) = this.loaded_text_thread_for_id(&context_id, cx) {
+                    let text_thread = text_thread.read(cx);
+                    let operations = text_thread.serialize_ops(&remote_version, cx);
+                    local_versions.push(text_thread.version(cx).to_proto(context_id.clone()));
                     let client = this.client.clone();
                     let project_id = envelope.payload.project_id;
                     cx.background_spawn(async move {
@@ -308,9 +301,9 @@ impl ContextStore {
         }
 
         if is_shared {
-            self.contexts.retain_mut(|context| {
-                if let Some(strong_context) = context.upgrade() {
-                    *context = ContextHandle::Strong(strong_context);
+            self.text_threads.retain_mut(|text_thread| {
+                if let Some(strong_context) = text_thread.upgrade() {
+                    *text_thread = TextThreadHandle::Strong(strong_context);
                     true
                 } else {
                     false
@@ -345,12 +338,12 @@ impl ContextStore {
                 self.synchronize_contexts(cx);
             }
             project::Event::DisconnectedFromHost => {
-                self.contexts.retain_mut(|context| {
-                    if let Some(strong_context) = context.upgrade() {
-                        *context = ContextHandle::Weak(context.downgrade());
-                        strong_context.update(cx, |context, cx| {
-                            if context.replica_id() != ReplicaId::default() {
-                                context.set_capability(language::Capability::ReadOnly, cx);
+                self.text_threads.retain_mut(|text_thread| {
+                    if let Some(strong_context) = text_thread.upgrade() {
+                        *text_thread = TextThreadHandle::Weak(text_thread.downgrade());
+                        strong_context.update(cx, |text_thread, cx| {
+                            if text_thread.replica_id() != ReplicaId::default() {
+                                text_thread.set_capability(language::Capability::ReadOnly, cx);
                             }
                         });
                         true
@@ -358,20 +351,24 @@ impl ContextStore {
                         false
                     }
                 });
-                self.host_contexts.clear();
+                self.host_text_threads.clear();
                 cx.notify();
             }
             _ => {}
         }
     }
 
-    pub fn unordered_contexts(&self) -> impl Iterator<Item = &SavedContextMetadata> {
-        self.contexts_metadata.iter()
+    pub fn unordered_text_threads(&self) -> impl Iterator<Item = &SavedTextThreadMetadata> {
+        self.text_threads_metadata.iter()
     }
 
-    pub fn create(&mut self, cx: &mut Context<Self>) -> Entity<AssistantContext> {
+    pub fn host_text_threads(&self) -> impl Iterator<Item = &RemoteTextThreadMetadata> {
+        self.host_text_threads.iter()
+    }
+
+    pub fn create(&mut self, cx: &mut Context<Self>) -> Entity<TextThread> {
         let context = cx.new(|cx| {
-            AssistantContext::local(
+            TextThread::local(
                 self.languages.clone(),
                 Some(self.project.clone()),
                 Some(self.telemetry.clone()),
@@ -380,14 +377,11 @@ impl ContextStore {
                 cx,
             )
         });
-        self.register_context(&context, cx);
+        self.register_text_thread(&context, cx);
         context
     }
 
-    pub fn create_remote_context(
-        &mut self,
-        cx: &mut Context<Self>,
-    ) -> Task<Result<Entity<AssistantContext>>> {
+    pub fn create_remote(&mut self, cx: &mut Context<Self>) -> Task<Result<Entity<TextThread>>> {
         let project = self.project.read(cx);
         let Some(project_id) = project.remote_id() else {
             return Task::ready(Err(anyhow::anyhow!("project was not remote")));
@@ -403,10 +397,10 @@ impl ContextStore {
         let request = self.client.request(proto::CreateContext { project_id });
         cx.spawn(async move |this, cx| {
             let response = request.await?;
-            let context_id = ContextId::from_proto(response.context_id);
+            let context_id = TextThreadId::from_proto(response.context_id);
             let context_proto = response.context.context("invalid context")?;
-            let context = cx.new(|cx| {
-                AssistantContext::new(
+            let text_thread = cx.new(|cx| {
+                TextThread::new(
                     context_id.clone(),
                     replica_id,
                     capability,
@@ -423,29 +417,29 @@ impl ContextStore {
                     context_proto
                         .operations
                         .into_iter()
-                        .map(ContextOperation::from_proto)
+                        .map(TextThreadOperation::from_proto)
                         .collect::<Result<Vec<_>>>()
                 })
                 .await?;
-            context.update(cx, |context, cx| context.apply_ops(operations, cx))?;
+            text_thread.update(cx, |context, cx| context.apply_ops(operations, cx))?;
             this.update(cx, |this, cx| {
-                if let Some(existing_context) = this.loaded_context_for_id(&context_id, cx) {
+                if let Some(existing_context) = this.loaded_text_thread_for_id(&context_id, cx) {
                     existing_context
                 } else {
-                    this.register_context(&context, cx);
+                    this.register_text_thread(&text_thread, cx);
                     this.synchronize_contexts(cx);
-                    context
+                    text_thread
                 }
             })
         })
     }
 
-    pub fn open_local_context(
+    pub fn open_local(
         &mut self,
         path: Arc<Path>,
         cx: &Context<Self>,
-    ) -> Task<Result<Entity<AssistantContext>>> {
-        if let Some(existing_context) = self.loaded_context_for_path(&path, cx) {
+    ) -> Task<Result<Entity<TextThread>>> {
+        if let Some(existing_context) = self.loaded_text_thread_for_path(&path, cx) {
             return Task::ready(Ok(existing_context));
         }
 
@@ -457,7 +451,7 @@ impl ContextStore {
             let path = path.clone();
             async move {
                 let saved_context = fs.load(&path).await?;
-                SavedContext::from_json(&saved_context)
+                SavedTextThread::from_json(&saved_context)
             }
         });
         let prompt_builder = self.prompt_builder.clone();
@@ -466,7 +460,7 @@ impl ContextStore {
         cx.spawn(async move |this, cx| {
             let saved_context = load.await?;
             let context = cx.new(|cx| {
-                AssistantContext::deserialize(
+                TextThread::deserialize(
                     saved_context,
                     path.clone(),
                     languages,
@@ -478,21 +472,17 @@ impl ContextStore {
                 )
             })?;
             this.update(cx, |this, cx| {
-                if let Some(existing_context) = this.loaded_context_for_path(&path, cx) {
+                if let Some(existing_context) = this.loaded_text_thread_for_path(&path, cx) {
                     existing_context
                 } else {
-                    this.register_context(&context, cx);
+                    this.register_text_thread(&context, cx);
                     context
                 }
             })
         })
     }
 
-    pub fn delete_local_context(
-        &mut self,
-        path: Arc<Path>,
-        cx: &mut Context<Self>,
-    ) -> Task<Result<()>> {
+    pub fn delete_local(&mut self, path: Arc<Path>, cx: &mut Context<Self>) -> Task<Result<()>> {
         let fs = self.fs.clone();
 
         cx.spawn(async move |this, cx| {
@@ -506,57 +496,57 @@ impl ContextStore {
             .await?;
 
             this.update(cx, |this, cx| {
-                this.contexts.retain(|context| {
-                    context
+                this.text_threads.retain(|text_thread| {
+                    text_thread
                         .upgrade()
-                        .and_then(|context| context.read(cx).path())
+                        .and_then(|text_thread| text_thread.read(cx).path())
                         != Some(&path)
                 });
-                this.contexts_metadata
-                    .retain(|context| context.path.as_ref() != path.as_ref());
+                this.text_threads_metadata
+                    .retain(|text_thread| text_thread.path.as_ref() != path.as_ref());
             })?;
 
             Ok(())
         })
     }
 
-    fn loaded_context_for_path(&self, path: &Path, cx: &App) -> Option<Entity<AssistantContext>> {
-        self.contexts.iter().find_map(|context| {
-            let context = context.upgrade()?;
-            if context.read(cx).path().map(Arc::as_ref) == Some(path) {
-                Some(context)
+    fn loaded_text_thread_for_path(&self, path: &Path, cx: &App) -> Option<Entity<TextThread>> {
+        self.text_threads.iter().find_map(|text_thread| {
+            let text_thread = text_thread.upgrade()?;
+            if text_thread.read(cx).path().map(Arc::as_ref) == Some(path) {
+                Some(text_thread)
             } else {
                 None
             }
         })
     }
 
-    pub fn loaded_context_for_id(
+    pub fn loaded_text_thread_for_id(
         &self,
-        id: &ContextId,
+        id: &TextThreadId,
         cx: &App,
-    ) -> Option<Entity<AssistantContext>> {
-        self.contexts.iter().find_map(|context| {
-            let context = context.upgrade()?;
-            if context.read(cx).id() == id {
-                Some(context)
+    ) -> Option<Entity<TextThread>> {
+        self.text_threads.iter().find_map(|text_thread| {
+            let text_thread = text_thread.upgrade()?;
+            if text_thread.read(cx).id() == id {
+                Some(text_thread)
             } else {
                 None
             }
         })
     }
 
-    pub fn open_remote_context(
+    pub fn open_remote(
         &mut self,
-        context_id: ContextId,
+        text_thread_id: TextThreadId,
         cx: &mut Context<Self>,
-    ) -> Task<Result<Entity<AssistantContext>>> {
+    ) -> Task<Result<Entity<TextThread>>> {
         let project = self.project.read(cx);
         let Some(project_id) = project.remote_id() else {
             return Task::ready(Err(anyhow::anyhow!("project was not remote")));
         };
 
-        if let Some(context) = self.loaded_context_for_id(&context_id, cx) {
+        if let Some(context) = self.loaded_text_thread_for_id(&text_thread_id, cx) {
             return Task::ready(Ok(context));
         }
 
@@ -567,16 +557,16 @@ impl ContextStore {
         let telemetry = self.telemetry.clone();
         let request = self.client.request(proto::OpenContext {
             project_id,
-            context_id: context_id.to_proto(),
+            context_id: text_thread_id.to_proto(),
         });
         let prompt_builder = self.prompt_builder.clone();
         let slash_commands = self.slash_commands.clone();
         cx.spawn(async move |this, cx| {
             let response = request.await?;
             let context_proto = response.context.context("invalid context")?;
-            let context = cx.new(|cx| {
-                AssistantContext::new(
-                    context_id.clone(),
+            let text_thread = cx.new(|cx| {
+                TextThread::new(
+                    text_thread_id.clone(),
                     replica_id,
                     capability,
                     language_registry,
@@ -592,38 +582,40 @@ impl ContextStore {
                     context_proto
                         .operations
                         .into_iter()
-                        .map(ContextOperation::from_proto)
+                        .map(TextThreadOperation::from_proto)
                         .collect::<Result<Vec<_>>>()
                 })
                 .await?;
-            context.update(cx, |context, cx| context.apply_ops(operations, cx))?;
+            text_thread.update(cx, |context, cx| context.apply_ops(operations, cx))?;
             this.update(cx, |this, cx| {
-                if let Some(existing_context) = this.loaded_context_for_id(&context_id, cx) {
+                if let Some(existing_context) = this.loaded_text_thread_for_id(&text_thread_id, cx)
+                {
                     existing_context
                 } else {
-                    this.register_context(&context, cx);
+                    this.register_text_thread(&text_thread, cx);
                     this.synchronize_contexts(cx);
-                    context
+                    text_thread
                 }
             })
         })
     }
 
-    fn register_context(&mut self, context: &Entity<AssistantContext>, cx: &mut Context<Self>) {
+    fn register_text_thread(&mut self, text_thread: &Entity<TextThread>, cx: &mut Context<Self>) {
         let handle = if self.project_is_shared {
-            ContextHandle::Strong(context.clone())
+            TextThreadHandle::Strong(text_thread.clone())
         } else {
-            ContextHandle::Weak(context.downgrade())
+            TextThreadHandle::Weak(text_thread.downgrade())
         };
-        self.contexts.push(handle);
+        self.text_threads.push(handle);
         self.advertise_contexts(cx);
-        cx.subscribe(context, Self::handle_context_event).detach();
+        cx.subscribe(text_thread, Self::handle_context_event)
+            .detach();
     }
 
     fn handle_context_event(
         &mut self,
-        context: Entity<AssistantContext>,
-        event: &ContextEvent,
+        text_thread: Entity<TextThread>,
+        event: &TextThreadEvent,
         cx: &mut Context<Self>,
     ) {
         let Some(project_id) = self.project.read(cx).remote_id() else {
@@ -631,12 +623,12 @@ impl ContextStore {
         };
 
         match event {
-            ContextEvent::SummaryChanged => {
+            TextThreadEvent::SummaryChanged => {
                 self.advertise_contexts(cx);
             }
-            ContextEvent::PathChanged { old_path, new_path } => {
+            TextThreadEvent::PathChanged { old_path, new_path } => {
                 if let Some(old_path) = old_path.as_ref() {
-                    for metadata in &mut self.contexts_metadata {
+                    for metadata in &mut self.text_threads_metadata {
                         if &metadata.path == old_path {
                             metadata.path = new_path.clone();
                             break;
@@ -644,8 +636,8 @@ impl ContextStore {
                     }
                 }
             }
-            ContextEvent::Operation(operation) => {
-                let context_id = context.read(cx).id().to_proto();
+            TextThreadEvent::Operation(operation) => {
+                let context_id = text_thread.read(cx).id().to_proto();
                 let operation = operation.to_proto();
                 self.client
                     .send(proto::UpdateContext {
@@ -670,15 +662,15 @@ impl ContextStore {
         }
 
         let contexts = self
-            .contexts
+            .text_threads
             .iter()
             .rev()
-            .filter_map(|context| {
-                let context = context.upgrade()?.read(cx);
-                if context.replica_id() == ReplicaId::default() {
+            .filter_map(|text_thread| {
+                let text_thread = text_thread.upgrade()?.read(cx);
+                if text_thread.replica_id() == ReplicaId::default() {
                     Some(proto::ContextMetadata {
-                        context_id: context.id().to_proto(),
-                        summary: context
+                        context_id: text_thread.id().to_proto(),
+                        summary: text_thread
                             .summary()
                             .content()
                             .map(|summary| summary.text.clone()),
@@ -701,13 +693,13 @@ impl ContextStore {
             return;
         };
 
-        let contexts = self
-            .contexts
+        let text_threads = self
+            .text_threads
             .iter()
-            .filter_map(|context| {
-                let context = context.upgrade()?.read(cx);
-                if context.replica_id() != ReplicaId::default() {
-                    Some(context.version(cx).to_proto(context.id().clone()))
+            .filter_map(|text_thread| {
+                let text_thread = text_thread.upgrade()?.read(cx);
+                if text_thread.replica_id() != ReplicaId::default() {
+                    Some(text_thread.version(cx).to_proto(text_thread.id().clone()))
                 } else {
                     None
                 }
@@ -717,26 +709,27 @@ impl ContextStore {
         let client = self.client.clone();
         let request = self.client.request(proto::SynchronizeContexts {
             project_id,
-            contexts,
+            contexts: text_threads,
         });
         cx.spawn(async move |this, cx| {
             let response = request.await?;
 
-            let mut context_ids = Vec::new();
+            let mut text_thread_ids = Vec::new();
             let mut operations = Vec::new();
             this.read_with(cx, |this, cx| {
                 for context_version_proto in response.contexts {
-                    let context_version = ContextVersion::from_proto(&context_version_proto);
-                    let context_id = ContextId::from_proto(context_version_proto.context_id);
-                    if let Some(context) = this.loaded_context_for_id(&context_id, cx) {
-                        context_ids.push(context_id);
-                        operations.push(context.read(cx).serialize_ops(&context_version, cx));
+                    let text_thread_version = TextThreadVersion::from_proto(&context_version_proto);
+                    let text_thread_id = TextThreadId::from_proto(context_version_proto.context_id);
+                    if let Some(text_thread) = this.loaded_text_thread_for_id(&text_thread_id, cx) {
+                        text_thread_ids.push(text_thread_id);
+                        operations
+                            .push(text_thread.read(cx).serialize_ops(&text_thread_version, cx));
                     }
                 }
             })?;
 
             let operations = futures::future::join_all(operations).await;
-            for (context_id, operations) in context_ids.into_iter().zip(operations) {
+            for (context_id, operations) in text_thread_ids.into_iter().zip(operations) {
                 for operation in operations {
                     client.send(proto::UpdateContext {
                         project_id,
@@ -751,8 +744,8 @@ impl ContextStore {
         .detach_and_log_err(cx);
     }
 
-    pub fn search(&self, query: String, cx: &App) -> Task<Vec<SavedContextMetadata>> {
-        let metadata = self.contexts_metadata.clone();
+    pub fn search(&self, query: String, cx: &App) -> Task<Vec<SavedTextThreadMetadata>> {
+        let metadata = self.text_threads_metadata.clone();
         let executor = cx.background_executor().clone();
         cx.background_spawn(async move {
             if query.is_empty() {
@@ -782,20 +775,16 @@ impl ContextStore {
         })
     }
 
-    pub fn host_contexts(&self) -> &[RemoteContextMetadata] {
-        &self.host_contexts
-    }
-
     fn reload(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> {
         let fs = self.fs.clone();
         cx.spawn(async move |this, cx| {
             if *ZED_STATELESS {
                 return Ok(());
             }
-            fs.create_dir(contexts_dir()).await?;
+            fs.create_dir(text_threads_dir()).await?;
 
-            let mut paths = fs.read_dir(contexts_dir()).await?;
-            let mut contexts = Vec::<SavedContextMetadata>::new();
+            let mut paths = fs.read_dir(text_threads_dir()).await?;
+            let mut contexts = Vec::<SavedTextThreadMetadata>::new();
             while let Some(path) = paths.next().await {
                 let path = path?;
                 if path.extension() != Some(OsStr::new("json")) {
@@ -821,7 +810,7 @@ impl ContextStore {
                         .lines()
                         .next()
                     {
-                        contexts.push(SavedContextMetadata {
+                        contexts.push(SavedTextThreadMetadata {
                             title: title.to_string().into(),
                             path: path.into(),
                             mtime: metadata.mtime.timestamp_for_user().into(),
@@ -829,10 +818,10 @@ impl ContextStore {
                     }
                 }
             }
-            contexts.sort_unstable_by_key(|context| Reverse(context.mtime));
+            contexts.sort_unstable_by_key(|text_thread| Reverse(text_thread.mtime));
 
             this.update(cx, |this, cx| {
-                this.contexts_metadata = contexts;
+                this.text_threads_metadata = contexts;
                 cx.notify();
             })
         })

crates/assistant_tool/Cargo.toml 🔗

@@ -1,50 +0,0 @@
-[package]
-name = "assistant_tool"
-version = "0.1.0"
-edition.workspace = true
-publish.workspace = true
-license = "GPL-3.0-or-later"
-
-[lints]
-workspace = true
-
-[lib]
-path = "src/assistant_tool.rs"
-
-[dependencies]
-action_log.workspace = true
-anyhow.workspace = true
-collections.workspace = true
-derive_more.workspace = true
-gpui.workspace = true
-icons.workspace = true
-language.workspace = true
-language_model.workspace = true
-log.workspace = true
-parking_lot.workspace = true
-project.workspace = true
-regex.workspace = true
-serde.workspace = true
-serde_json.workspace = true
-text.workspace = true
-util.workspace = true
-workspace.workspace = true
-workspace-hack.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"] }
-language_model = { workspace = true, features = ["test-support"] }
-log.workspace = true
-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

crates/assistant_tool/src/assistant_tool.rs 🔗

@@ -1,269 +0,0 @@
-pub mod outline;
-mod tool_registry;
-mod tool_schema;
-mod tool_working_set;
-
-use std::fmt;
-use std::fmt::Debug;
-use std::fmt::Formatter;
-use std::ops::Deref;
-use std::sync::Arc;
-
-use action_log::ActionLog;
-use anyhow::Result;
-use gpui::AnyElement;
-use gpui::AnyWindowHandle;
-use gpui::Context;
-use gpui::IntoElement;
-use gpui::Window;
-use gpui::{App, Entity, SharedString, Task, WeakEntity};
-use icons::IconName;
-use language_model::LanguageModel;
-use language_model::LanguageModelImage;
-use language_model::LanguageModelRequest;
-use language_model::LanguageModelToolSchemaFormat;
-use project::Project;
-use workspace::Workspace;
-
-pub use crate::tool_registry::*;
-pub use crate::tool_schema::*;
-pub use crate::tool_working_set::*;
-
-pub fn init(cx: &mut App) {
-    ToolRegistry::default_global(cx);
-}
-
-#[derive(Debug, Clone)]
-pub enum ToolUseStatus {
-    InputStillStreaming,
-    NeedsConfirmation,
-    Pending,
-    Running,
-    Finished(SharedString),
-    Error(SharedString),
-}
-
-impl ToolUseStatus {
-    pub fn text(&self) -> SharedString {
-        match self {
-            ToolUseStatus::NeedsConfirmation => "".into(),
-            ToolUseStatus::InputStillStreaming => "".into(),
-            ToolUseStatus::Pending => "".into(),
-            ToolUseStatus::Running => "".into(),
-            ToolUseStatus::Finished(out) => out.clone(),
-            ToolUseStatus::Error(out) => out.clone(),
-        }
-    }
-
-    pub fn error(&self) -> Option<SharedString> {
-        match self {
-            ToolUseStatus::Error(out) => Some(out.clone()),
-            _ => None,
-        }
-    }
-}
-
-#[derive(Debug)]
-pub struct ToolResultOutput {
-    pub content: ToolResultContent,
-    pub output: Option<serde_json::Value>,
-}
-
-#[derive(Debug, PartialEq, Eq)]
-pub enum ToolResultContent {
-    Text(String),
-    Image(LanguageModelImage),
-}
-
-impl ToolResultContent {
-    pub fn len(&self) -> usize {
-        match self {
-            ToolResultContent::Text(str) => str.len(),
-            ToolResultContent::Image(image) => image.len(),
-        }
-    }
-
-    pub fn is_empty(&self) -> bool {
-        match self {
-            ToolResultContent::Text(str) => str.is_empty(),
-            ToolResultContent::Image(image) => image.is_empty(),
-        }
-    }
-
-    pub fn as_str(&self) -> Option<&str> {
-        match self {
-            ToolResultContent::Text(str) => Some(str),
-            ToolResultContent::Image(_) => None,
-        }
-    }
-}
-
-impl From<String> for ToolResultOutput {
-    fn from(value: String) -> Self {
-        ToolResultOutput {
-            content: ToolResultContent::Text(value),
-            output: None,
-        }
-    }
-}
-
-impl Deref for ToolResultOutput {
-    type Target = ToolResultContent;
-
-    fn deref(&self) -> &Self::Target {
-        &self.content
-    }
-}
-
-/// The result of running a tool, containing both the asynchronous output
-/// and an optional card view that can be rendered immediately.
-pub struct ToolResult {
-    /// The asynchronous task that will eventually resolve to the tool's output
-    pub output: Task<Result<ToolResultOutput>>,
-    /// An optional view to present the output of the tool.
-    pub card: Option<AnyToolCard>,
-}
-
-pub trait ToolCard: 'static + Sized {
-    fn render(
-        &mut self,
-        status: &ToolUseStatus,
-        window: &mut Window,
-        workspace: WeakEntity<Workspace>,
-        cx: &mut Context<Self>,
-    ) -> impl IntoElement;
-}
-
-#[derive(Clone)]
-pub struct AnyToolCard {
-    entity: gpui::AnyEntity,
-    render: fn(
-        entity: gpui::AnyEntity,
-        status: &ToolUseStatus,
-        window: &mut Window,
-        workspace: WeakEntity<Workspace>,
-        cx: &mut App,
-    ) -> AnyElement,
-}
-
-impl<T: ToolCard> From<Entity<T>> for AnyToolCard {
-    fn from(entity: Entity<T>) -> Self {
-        fn downcast_render<T: ToolCard>(
-            entity: gpui::AnyEntity,
-            status: &ToolUseStatus,
-            window: &mut Window,
-            workspace: WeakEntity<Workspace>,
-            cx: &mut App,
-        ) -> AnyElement {
-            let entity = entity.downcast::<T>().unwrap();
-            entity.update(cx, |entity, cx| {
-                entity
-                    .render(status, window, workspace, cx)
-                    .into_any_element()
-            })
-        }
-
-        Self {
-            entity: entity.into(),
-            render: downcast_render::<T>,
-        }
-    }
-}
-
-impl AnyToolCard {
-    pub fn render(
-        &self,
-        status: &ToolUseStatus,
-        window: &mut Window,
-        workspace: WeakEntity<Workspace>,
-        cx: &mut App,
-    ) -> AnyElement {
-        (self.render)(self.entity.clone(), status, window, workspace, cx)
-    }
-}
-
-impl From<Task<Result<ToolResultOutput>>> for ToolResult {
-    /// Convert from a task to a ToolResult with no card
-    fn from(output: Task<Result<ToolResultOutput>>) -> Self {
-        Self { output, card: None }
-    }
-}
-
-#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
-pub enum ToolSource {
-    /// A native tool built-in to Zed.
-    Native,
-    /// A tool provided by a context server.
-    ContextServer { id: SharedString },
-}
-
-/// A tool that can be used by a language model.
-pub trait Tool: 'static + Send + Sync {
-    /// Returns the name of the tool.
-    fn name(&self) -> String;
-
-    /// Returns the description of the tool.
-    fn description(&self) -> String;
-
-    /// Returns the icon for the tool.
-    fn icon(&self) -> IconName;
-
-    /// Returns the source of the tool.
-    fn source(&self) -> ToolSource {
-        ToolSource::Native
-    }
-
-    /// Returns true if the tool needs the users's confirmation
-    /// before having permission to run.
-    fn needs_confirmation(
-        &self,
-        input: &serde_json::Value,
-        project: &Entity<Project>,
-        cx: &App,
-    ) -> bool;
-
-    /// Returns true if the tool may perform edits.
-    fn may_perform_edits(&self) -> bool;
-
-    /// Returns the JSON schema that describes the tool's input.
-    fn input_schema(&self, _: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
-        Ok(serde_json::Value::Object(serde_json::Map::default()))
-    }
-
-    /// Returns markdown to be displayed in the UI for this tool.
-    fn ui_text(&self, input: &serde_json::Value) -> String;
-
-    /// Returns markdown to be displayed in the UI for this tool, while the input JSON is still streaming
-    /// (so information may be missing).
-    fn still_streaming_ui_text(&self, input: &serde_json::Value) -> String {
-        self.ui_text(input)
-    }
-
-    /// Runs the tool with the provided input.
-    fn run(
-        self: Arc<Self>,
-        input: serde_json::Value,
-        request: Arc<LanguageModelRequest>,
-        project: Entity<Project>,
-        action_log: Entity<ActionLog>,
-        model: Arc<dyn LanguageModel>,
-        window: Option<AnyWindowHandle>,
-        cx: &mut App,
-    ) -> ToolResult;
-
-    fn deserialize_card(
-        self: Arc<Self>,
-        _output: serde_json::Value,
-        _project: Entity<Project>,
-        _window: &mut Window,
-        _cx: &mut App,
-    ) -> Option<AnyToolCard> {
-        None
-    }
-}
-
-impl Debug for dyn Tool {
-    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
-        f.debug_struct("Tool").field("name", &self.name()).finish()
-    }
-}

crates/assistant_tool/src/tool_registry.rs 🔗

@@ -1,74 +0,0 @@
-use std::sync::Arc;
-
-use collections::HashMap;
-use derive_more::{Deref, DerefMut};
-use gpui::Global;
-use gpui::{App, ReadGlobal};
-use parking_lot::RwLock;
-
-use crate::Tool;
-
-#[derive(Default, Deref, DerefMut)]
-struct GlobalToolRegistry(Arc<ToolRegistry>);
-
-impl Global for GlobalToolRegistry {}
-
-#[derive(Default)]
-struct ToolRegistryState {
-    tools: HashMap<Arc<str>, Arc<dyn Tool>>,
-}
-
-#[derive(Default)]
-pub struct ToolRegistry {
-    state: RwLock<ToolRegistryState>,
-}
-
-impl ToolRegistry {
-    /// Returns the global [`ToolRegistry`].
-    pub fn global(cx: &App) -> Arc<Self> {
-        GlobalToolRegistry::global(cx).0.clone()
-    }
-
-    /// Returns the global [`ToolRegistry`].
-    ///
-    /// Inserts a default [`ToolRegistry`] if one does not yet exist.
-    pub fn default_global(cx: &mut App) -> Arc<Self> {
-        cx.default_global::<GlobalToolRegistry>().0.clone()
-    }
-
-    pub fn new() -> Arc<Self> {
-        Arc::new(Self {
-            state: RwLock::new(ToolRegistryState {
-                tools: HashMap::default(),
-            }),
-        })
-    }
-
-    /// Registers the provided [`Tool`].
-    pub fn register_tool(&self, tool: impl Tool) {
-        let mut state = self.state.write();
-        let tool_name: Arc<str> = tool.name().into();
-        state.tools.insert(tool_name, Arc::new(tool));
-    }
-
-    /// Unregisters the provided [`Tool`].
-    pub fn unregister_tool(&self, tool: impl Tool) {
-        self.unregister_tool_by_name(tool.name().as_str())
-    }
-
-    /// Unregisters the tool with the given name.
-    pub fn unregister_tool_by_name(&self, tool_name: &str) {
-        let mut state = self.state.write();
-        state.tools.remove(tool_name);
-    }
-
-    /// Returns the list of tools in the registry.
-    pub fn tools(&self) -> Vec<Arc<dyn Tool>> {
-        self.state.read().tools.values().cloned().collect()
-    }
-
-    /// Returns the [`Tool`] with the given name.
-    pub fn tool(&self, name: &str) -> Option<Arc<dyn Tool>> {
-        self.state.read().tools.get(name).cloned()
-    }
-}

crates/assistant_tool/src/tool_working_set.rs 🔗

@@ -1,415 +0,0 @@
-use std::{borrow::Borrow, sync::Arc};
-
-use crate::{Tool, ToolRegistry, ToolSource};
-use collections::{HashMap, HashSet, IndexMap};
-use gpui::{App, SharedString};
-use util::debug_panic;
-
-#[derive(Copy, Clone, PartialEq, Eq, Hash, Default)]
-pub struct ToolId(usize);
-
-/// A unique identifier for a tool within a working set.
-#[derive(Clone, PartialEq, Eq, Hash, Default)]
-pub struct UniqueToolName(SharedString);
-
-impl Borrow<str> for UniqueToolName {
-    fn borrow(&self) -> &str {
-        &self.0
-    }
-}
-
-impl From<String> for UniqueToolName {
-    fn from(value: String) -> Self {
-        UniqueToolName(SharedString::new(value))
-    }
-}
-
-impl Into<String> for UniqueToolName {
-    fn into(self) -> String {
-        self.0.into()
-    }
-}
-
-impl std::fmt::Debug for UniqueToolName {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        self.0.fmt(f)
-    }
-}
-
-impl std::fmt::Display for UniqueToolName {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        write!(f, "{}", self.0.as_ref())
-    }
-}
-
-/// A working set of tools for use in one instance of the Assistant Panel.
-#[derive(Default)]
-pub struct ToolWorkingSet {
-    context_server_tools_by_id: HashMap<ToolId, Arc<dyn Tool>>,
-    context_server_tools_by_name: HashMap<UniqueToolName, Arc<dyn Tool>>,
-    next_tool_id: ToolId,
-}
-
-impl ToolWorkingSet {
-    pub fn tool(&self, name: &str, cx: &App) -> Option<Arc<dyn Tool>> {
-        self.context_server_tools_by_name
-            .get(name)
-            .cloned()
-            .or_else(|| ToolRegistry::global(cx).tool(name))
-    }
-
-    pub fn tools(&self, cx: &App) -> Vec<(UniqueToolName, Arc<dyn Tool>)> {
-        let mut tools = ToolRegistry::global(cx)
-            .tools()
-            .into_iter()
-            .map(|tool| (UniqueToolName(tool.name().into()), tool))
-            .collect::<Vec<_>>();
-        tools.extend(self.context_server_tools_by_name.clone());
-        tools
-    }
-
-    pub fn tools_by_source(&self, cx: &App) -> IndexMap<ToolSource, Vec<Arc<dyn Tool>>> {
-        let mut tools_by_source = IndexMap::default();
-
-        for (_, tool) in self.tools(cx) {
-            tools_by_source
-                .entry(tool.source())
-                .or_insert_with(Vec::new)
-                .push(tool);
-        }
-
-        for tools in tools_by_source.values_mut() {
-            tools.sort_by_key(|tool| tool.name());
-        }
-
-        tools_by_source.sort_unstable_keys();
-
-        tools_by_source
-    }
-
-    pub fn insert(&mut self, tool: Arc<dyn Tool>, cx: &App) -> ToolId {
-        let tool_id = self.register_tool(tool);
-        self.tools_changed(cx);
-        tool_id
-    }
-
-    pub fn extend(&mut self, tools: impl Iterator<Item = Arc<dyn Tool>>, cx: &App) -> Vec<ToolId> {
-        let ids = tools.map(|tool| self.register_tool(tool)).collect();
-        self.tools_changed(cx);
-        ids
-    }
-
-    pub fn remove(&mut self, tool_ids_to_remove: &[ToolId], cx: &App) {
-        self.context_server_tools_by_id
-            .retain(|id, _| !tool_ids_to_remove.contains(id));
-        self.tools_changed(cx);
-    }
-
-    fn register_tool(&mut self, tool: Arc<dyn Tool>) -> ToolId {
-        let tool_id = self.next_tool_id;
-        self.next_tool_id.0 += 1;
-        self.context_server_tools_by_id
-            .insert(tool_id, tool.clone());
-        tool_id
-    }
-
-    fn tools_changed(&mut self, cx: &App) {
-        self.context_server_tools_by_name = resolve_context_server_tool_name_conflicts(
-            &self
-                .context_server_tools_by_id
-                .values()
-                .cloned()
-                .collect::<Vec<_>>(),
-            &ToolRegistry::global(cx).tools(),
-        );
-    }
-}
-
-fn resolve_context_server_tool_name_conflicts(
-    context_server_tools: &[Arc<dyn Tool>],
-    native_tools: &[Arc<dyn Tool>],
-) -> HashMap<UniqueToolName, Arc<dyn Tool>> {
-    fn resolve_tool_name(tool: &Arc<dyn Tool>) -> String {
-        let mut tool_name = tool.name();
-        tool_name.truncate(MAX_TOOL_NAME_LENGTH);
-        tool_name
-    }
-
-    const MAX_TOOL_NAME_LENGTH: usize = 64;
-
-    let mut duplicated_tool_names = HashSet::default();
-    let mut seen_tool_names = HashSet::default();
-    seen_tool_names.extend(native_tools.iter().map(|tool| tool.name()));
-    for tool in context_server_tools {
-        let tool_name = resolve_tool_name(tool);
-        if seen_tool_names.contains(&tool_name) {
-            debug_assert!(
-                tool.source() != ToolSource::Native,
-                "Expected MCP tool but got a native tool: {}",
-                tool_name
-            );
-            duplicated_tool_names.insert(tool_name);
-        } else {
-            seen_tool_names.insert(tool_name);
-        }
-    }
-
-    if duplicated_tool_names.is_empty() {
-        return context_server_tools
-            .iter()
-            .map(|tool| (resolve_tool_name(tool).into(), tool.clone()))
-            .collect();
-    }
-
-    context_server_tools
-        .iter()
-        .filter_map(|tool| {
-            let mut tool_name = resolve_tool_name(tool);
-            if !duplicated_tool_names.contains(&tool_name) {
-                return Some((tool_name.into(), tool.clone()));
-            }
-            match tool.source() {
-                ToolSource::Native => {
-                    debug_panic!("Expected MCP tool but got a native tool: {}", tool_name);
-                    // Built-in tools always keep their original name
-                    Some((tool_name.into(), tool.clone()))
-                }
-                ToolSource::ContextServer { id } => {
-                    // Context server tools are prefixed with the context server ID, and truncated if necessary
-                    tool_name.insert(0, '_');
-                    if tool_name.len() + id.len() > MAX_TOOL_NAME_LENGTH {
-                        let len = MAX_TOOL_NAME_LENGTH - tool_name.len();
-                        let mut id = id.to_string();
-                        id.truncate(len);
-                        tool_name.insert_str(0, &id);
-                    } else {
-                        tool_name.insert_str(0, &id);
-                    }
-
-                    tool_name.truncate(MAX_TOOL_NAME_LENGTH);
-
-                    if seen_tool_names.contains(&tool_name) {
-                        log::error!("Cannot resolve tool name conflict for tool {}", tool.name());
-                        None
-                    } else {
-                        Some((tool_name.into(), tool.clone()))
-                    }
-                }
-            }
-        })
-        .collect()
-}
-#[cfg(test)]
-mod tests {
-    use gpui::{AnyWindowHandle, Entity, Task, TestAppContext};
-    use language_model::{LanguageModel, LanguageModelRequest};
-    use project::Project;
-
-    use crate::{ActionLog, ToolResult};
-
-    use super::*;
-
-    #[gpui::test]
-    fn test_unique_tool_names(cx: &mut TestAppContext) {
-        fn assert_tool(
-            tool_working_set: &ToolWorkingSet,
-            unique_name: &str,
-            expected_name: &str,
-            expected_source: ToolSource,
-            cx: &App,
-        ) {
-            let tool = tool_working_set.tool(unique_name, cx).unwrap();
-            assert_eq!(tool.name(), expected_name);
-            assert_eq!(tool.source(), expected_source);
-        }
-
-        let tool_registry = cx.update(ToolRegistry::default_global);
-        tool_registry.register_tool(TestTool::new("tool1", ToolSource::Native));
-        tool_registry.register_tool(TestTool::new("tool2", ToolSource::Native));
-
-        let mut tool_working_set = ToolWorkingSet::default();
-        cx.update(|cx| {
-            tool_working_set.extend(
-                vec![
-                    Arc::new(TestTool::new(
-                        "tool2",
-                        ToolSource::ContextServer { id: "mcp-1".into() },
-                    )) as Arc<dyn Tool>,
-                    Arc::new(TestTool::new(
-                        "tool2",
-                        ToolSource::ContextServer { id: "mcp-2".into() },
-                    )) as Arc<dyn Tool>,
-                ]
-                .into_iter(),
-                cx,
-            );
-        });
-
-        cx.update(|cx| {
-            assert_tool(&tool_working_set, "tool1", "tool1", ToolSource::Native, cx);
-            assert_tool(&tool_working_set, "tool2", "tool2", ToolSource::Native, cx);
-            assert_tool(
-                &tool_working_set,
-                "mcp-1_tool2",
-                "tool2",
-                ToolSource::ContextServer { id: "mcp-1".into() },
-                cx,
-            );
-            assert_tool(
-                &tool_working_set,
-                "mcp-2_tool2",
-                "tool2",
-                ToolSource::ContextServer { id: "mcp-2".into() },
-                cx,
-            );
-        })
-    }
-
-    #[gpui::test]
-    fn test_resolve_context_server_tool_name_conflicts() {
-        assert_resolve_context_server_tool_name_conflicts(
-            vec![
-                TestTool::new("tool1", ToolSource::Native),
-                TestTool::new("tool2", ToolSource::Native),
-            ],
-            vec![TestTool::new(
-                "tool3",
-                ToolSource::ContextServer { id: "mcp-1".into() },
-            )],
-            vec!["tool3"],
-        );
-
-        assert_resolve_context_server_tool_name_conflicts(
-            vec![
-                TestTool::new("tool1", ToolSource::Native),
-                TestTool::new("tool2", ToolSource::Native),
-            ],
-            vec![
-                TestTool::new("tool3", ToolSource::ContextServer { id: "mcp-1".into() }),
-                TestTool::new("tool3", ToolSource::ContextServer { id: "mcp-2".into() }),
-            ],
-            vec!["mcp-1_tool3", "mcp-2_tool3"],
-        );
-
-        assert_resolve_context_server_tool_name_conflicts(
-            vec![
-                TestTool::new("tool1", ToolSource::Native),
-                TestTool::new("tool2", ToolSource::Native),
-                TestTool::new("tool3", ToolSource::Native),
-            ],
-            vec![
-                TestTool::new("tool3", ToolSource::ContextServer { id: "mcp-1".into() }),
-                TestTool::new("tool3", ToolSource::ContextServer { id: "mcp-2".into() }),
-            ],
-            vec!["mcp-1_tool3", "mcp-2_tool3"],
-        );
-
-        // Test deduplication of tools with very long names, in this case the mcp server name should be truncated
-        assert_resolve_context_server_tool_name_conflicts(
-            vec![TestTool::new(
-                "tool-with-very-very-very-long-name",
-                ToolSource::Native,
-            )],
-            vec![TestTool::new(
-                "tool-with-very-very-very-long-name",
-                ToolSource::ContextServer {
-                    id: "mcp-with-very-very-very-long-name".into(),
-                },
-            )],
-            vec!["mcp-with-very-very-very-long-_tool-with-very-very-very-long-name"],
-        );
-
-        fn assert_resolve_context_server_tool_name_conflicts(
-            builtin_tools: Vec<TestTool>,
-            context_server_tools: Vec<TestTool>,
-            expected: Vec<&'static str>,
-        ) {
-            let context_server_tools: Vec<Arc<dyn Tool>> = context_server_tools
-                .into_iter()
-                .map(|t| Arc::new(t) as Arc<dyn Tool>)
-                .collect();
-            let builtin_tools: Vec<Arc<dyn Tool>> = builtin_tools
-                .into_iter()
-                .map(|t| Arc::new(t) as Arc<dyn Tool>)
-                .collect();
-            let tools =
-                resolve_context_server_tool_name_conflicts(&context_server_tools, &builtin_tools);
-            assert_eq!(tools.len(), expected.len());
-            for (i, (name, _)) in tools.into_iter().enumerate() {
-                assert_eq!(
-                    name.0.as_ref(),
-                    expected[i],
-                    "Expected '{}' got '{}' at index {}",
-                    expected[i],
-                    name,
-                    i
-                );
-            }
-        }
-    }
-
-    struct TestTool {
-        name: String,
-        source: ToolSource,
-    }
-
-    impl TestTool {
-        fn new(name: impl Into<String>, source: ToolSource) -> Self {
-            Self {
-                name: name.into(),
-                source,
-            }
-        }
-    }
-
-    impl Tool for TestTool {
-        fn name(&self) -> String {
-            self.name.clone()
-        }
-
-        fn icon(&self) -> icons::IconName {
-            icons::IconName::Ai
-        }
-
-        fn may_perform_edits(&self) -> bool {
-            false
-        }
-
-        fn needs_confirmation(
-            &self,
-            _input: &serde_json::Value,
-            _project: &Entity<Project>,
-            _cx: &App,
-        ) -> bool {
-            true
-        }
-
-        fn source(&self) -> ToolSource {
-            self.source.clone()
-        }
-
-        fn description(&self) -> String {
-            "Test tool".to_string()
-        }
-
-        fn ui_text(&self, _input: &serde_json::Value) -> String {
-            "Test tool".to_string()
-        }
-
-        fn run(
-            self: Arc<Self>,
-            _input: serde_json::Value,
-            _request: Arc<LanguageModelRequest>,
-            _project: Entity<Project>,
-            _action_log: Entity<ActionLog>,
-            _model: Arc<dyn LanguageModel>,
-            _window: Option<AnyWindowHandle>,
-            _cx: &mut App,
-        ) -> ToolResult {
-            ToolResult {
-                output: Task::ready(Err(anyhow::anyhow!("No content"))),
-                card: None,
-            }
-        }
-    }
-}

crates/assistant_tools/Cargo.toml 🔗

@@ -1,92 +0,0 @@
-[package]
-name = "assistant_tools"
-version = "0.1.0"
-edition.workspace = true
-publish.workspace = true
-license = "GPL-3.0-or-later"
-
-[lints]
-workspace = true
-
-[lib]
-path = "src/assistant_tools.rs"
-
-[features]
-eval = []
-
-[dependencies]
-action_log.workspace = true
-agent_settings.workspace = true
-anyhow.workspace = true
-assistant_tool.workspace = true
-buffer_diff.workspace = true
-chrono.workspace = true
-client.workspace = true
-cloud_llm_client.workspace = true
-collections.workspace = true
-component.workspace = true
-derive_more.workspace = true
-diffy = "0.4.2"
-editor.workspace = true
-feature_flags.workspace = true
-futures.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
-log.workspace = true
-lsp.workspace = true
-markdown.workspace = true
-open.workspace = true
-paths.workspace = true
-portable-pty.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
-streaming_diff.workspace = true
-strsim.workspace = true
-task.workspace = true
-terminal.workspace = true
-terminal_view.workspace = true
-theme.workspace = true
-ui.workspace = true
-util.workspace = true
-watch.workspace = true
-web_search.workspace = true
-workspace-hack.workspace = true
-workspace.workspace = true
-
-[dev-dependencies]
-lsp = { workspace = true, features = ["test-support"] }
-client = { workspace = true, features = ["test-support"] }
-clock = { workspace = true, features = ["test-support"] }
-collections = { workspace = true, features = ["test-support"] }
-gpui = { workspace = true, features = ["test-support"] }
-gpui_tokio.workspace = true
-fs = { workspace = true, features = ["test-support"] }
-language = { workspace = true, features = ["test-support"] }
-language_model = { workspace = true, features = ["test-support"] }
-language_models.workspace = true
-project = { workspace = true, features = ["test-support"] }
-rand.workspace = true
-pretty_assertions.workspace = true
-reqwest_client.workspace = true
-settings = { workspace = true, features = ["test-support"] }
-smol.workspace = true
-task = { workspace = true, features = ["test-support"]}
-tempfile.workspace = true
-theme.workspace = true
-tree-sitter-rust.workspace = true
-workspace = { workspace = true, features = ["test-support"] }
-unindent.workspace = true
-zlog.workspace = true

crates/assistant_tools/src/assistant_tools.rs 🔗

@@ -1,167 +0,0 @@
-mod copy_path_tool;
-mod create_directory_tool;
-mod delete_path_tool;
-mod diagnostics_tool;
-pub mod edit_agent;
-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 project_notifications_tool;
-mod read_file_tool;
-mod schema;
-pub mod templates;
-mod terminal_tool;
-mod thinking_tool;
-mod ui;
-mod web_search_tool;
-
-use assistant_tool::ToolRegistry;
-use copy_path_tool::CopyPathTool;
-use gpui::{App, Entity};
-use http_client::HttpClientWithUrl;
-use language_model::LanguageModelRegistry;
-use move_path_tool::MovePathTool;
-use std::sync::Arc;
-use web_search_tool::WebSearchTool;
-
-pub(crate) use templates::*;
-
-use crate::create_directory_tool::CreateDirectoryTool;
-use crate::delete_path_tool::DeletePathTool;
-use crate::diagnostics_tool::DiagnosticsTool;
-use crate::edit_file_tool::EditFileTool;
-use crate::fetch_tool::FetchTool;
-use crate::list_directory_tool::ListDirectoryTool;
-use crate::now_tool::NowTool;
-use crate::thinking_tool::ThinkingTool;
-
-pub use edit_file_tool::{EditFileMode, EditFileToolInput};
-pub use find_path_tool::*;
-pub use grep_tool::{GrepTool, GrepToolInput};
-pub use open_tool::OpenTool;
-pub use project_notifications_tool::ProjectNotificationsTool;
-pub use read_file_tool::{ReadFileTool, ReadFileToolInput};
-pub use terminal_tool::TerminalTool;
-
-pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) {
-    assistant_tool::init(cx);
-
-    let registry = ToolRegistry::global(cx);
-    registry.register_tool(TerminalTool);
-    registry.register_tool(CreateDirectoryTool);
-    registry.register_tool(CopyPathTool);
-    registry.register_tool(DeletePathTool);
-    registry.register_tool(MovePathTool);
-    registry.register_tool(DiagnosticsTool);
-    registry.register_tool(ListDirectoryTool);
-    registry.register_tool(NowTool);
-    registry.register_tool(OpenTool);
-    registry.register_tool(ProjectNotificationsTool);
-    registry.register_tool(FindPathTool);
-    registry.register_tool(ReadFileTool);
-    registry.register_tool(GrepTool);
-    registry.register_tool(ThinkingTool);
-    registry.register_tool(FetchTool::new(http_client));
-    registry.register_tool(EditFileTool);
-
-    register_web_search_tool(&LanguageModelRegistry::global(cx), cx);
-    cx.subscribe(
-        &LanguageModelRegistry::global(cx),
-        move |registry, event, cx| {
-            if let language_model::Event::DefaultModelChanged = event {
-                register_web_search_tool(&registry, cx);
-            }
-        },
-    )
-    .detach();
-}
-
-fn register_web_search_tool(registry: &Entity<LanguageModelRegistry>, cx: &mut App) {
-    let using_zed_provider = registry
-        .read(cx)
-        .default_model()
-        .is_some_and(|default| default.is_provided_by_zed());
-    if using_zed_provider {
-        ToolRegistry::global(cx).register_tool(WebSearchTool);
-    } else {
-        ToolRegistry::global(cx).unregister_tool(WebSearchTool);
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use agent_settings::AgentSettings;
-    use client::Client;
-    use clock::FakeSystemClock;
-    use http_client::FakeHttpClient;
-    use schemars::JsonSchema;
-    use serde::Serialize;
-    use settings::Settings;
-
-    #[test]
-    fn test_json_schema() {
-        #[derive(Serialize, JsonSchema)]
-        struct GetWeatherTool {
-            location: String,
-        }
-
-        let schema = schema::json_schema_for::<GetWeatherTool>(
-            language_model::LanguageModelToolSchemaFormat::JsonSchema,
-        )
-        .unwrap();
-
-        assert_eq!(
-            schema,
-            serde_json::json!({
-                "type": "object",
-                "properties": {
-                    "location": {
-                        "type": "string"
-                    }
-                },
-                "required": ["location"],
-                "additionalProperties": false
-            })
-        );
-    }
-
-    #[gpui::test]
-    fn test_builtin_tool_schema_compatibility(cx: &mut App) {
-        settings::init(cx);
-        AgentSettings::register(cx);
-
-        let client = Client::new(
-            Arc::new(FakeSystemClock::new()),
-            FakeHttpClient::with_200_response(),
-            cx,
-        );
-        language_model::init(client.clone(), cx);
-        crate::init(client.http_client(), cx);
-
-        for tool in ToolRegistry::global(cx).tools() {
-            let actual_schema = tool
-                .input_schema(language_model::LanguageModelToolSchemaFormat::JsonSchemaSubset)
-                .unwrap();
-            let mut expected_schema = actual_schema.clone();
-            assistant_tool::adapt_schema_to_format(
-                &mut expected_schema,
-                language_model::LanguageModelToolSchemaFormat::JsonSchemaSubset,
-            )
-            .unwrap();
-
-            let error_message = format!(
-                "Tool schema for `{}` is not compatible with `language_model::LanguageModelToolSchemaFormat::JsonSchemaSubset` (Gemini Models).\n\
-                Are you using `schema::json_schema_for<T>(format)` to generate the schema?",
-                tool.name(),
-            );
-
-            assert_eq!(actual_schema, expected_schema, "{}", error_message)
-        }
-    }
-}

crates/assistant_tools/src/copy_path_tool.rs 🔗

@@ -1,123 +0,0 @@
-use crate::schema::json_schema_for;
-use action_log::ActionLog;
-use anyhow::{Context as _, Result, anyhow};
-use assistant_tool::{Tool, ToolResult};
-use gpui::AnyWindowHandle;
-use gpui::{App, AppContext, Entity, Task};
-use language_model::LanguageModel;
-use language_model::{LanguageModelRequest, LanguageModelToolSchemaFormat};
-use project::Project;
-use schemars::JsonSchema;
-use serde::{Deserialize, Serialize};
-use std::sync::Arc;
-use ui::IconName;
-use util::markdown::MarkdownInlineCode;
-
-#[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 (like `cp -r`).
-    ///
-    /// <example>
-    /// 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"
-    /// </example>
-    pub source_path: String,
-
-    /// The destination path where the file or directory should be copied to.
-    ///
-    /// <example>
-    /// To copy "directory1/a/something.txt" to "directory2/b/copy.txt",
-    /// provide a destination_path of "directory2/b/copy.txt"
-    /// </example>
-    pub destination_path: String,
-}
-
-pub struct CopyPathTool;
-
-impl Tool for CopyPathTool {
-    fn name(&self) -> String {
-        "copy_path".into()
-    }
-
-    fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
-        false
-    }
-
-    fn may_perform_edits(&self) -> bool {
-        true
-    }
-
-    fn description(&self) -> String {
-        include_str!("./copy_path_tool/description.md").into()
-    }
-
-    fn icon(&self) -> IconName {
-        IconName::ToolCopy
-    }
-
-    fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
-        json_schema_for::<CopyPathToolInput>(format)
-    }
-
-    fn ui_text(&self, input: &serde_json::Value) -> String {
-        match serde_json::from_value::<CopyPathToolInput>(input.clone()) {
-            Ok(input) => {
-                let src = MarkdownInlineCode(&input.source_path);
-                let dest = MarkdownInlineCode(&input.destination_path);
-                format!("Copy {src} to {dest}")
-            }
-            Err(_) => "Copy path".to_string(),
-        }
-    }
-
-    fn run(
-        self: Arc<Self>,
-        input: serde_json::Value,
-        _request: Arc<LanguageModelRequest>,
-        project: Entity<Project>,
-        _action_log: Entity<ActionLog>,
-        _model: Arc<dyn LanguageModel>,
-        _window: Option<AnyWindowHandle>,
-        cx: &mut App,
-    ) -> ToolResult {
-        let input = match serde_json::from_value::<CopyPathToolInput>(input) {
-            Ok(input) => input,
-            Err(err) => return Task::ready(Err(anyhow!(err))).into(),
-        };
-        let copy_task = 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).into())
-        })
-        .into()
-    }
-}

crates/assistant_tools/src/copy_path_tool/description.md 🔗

@@ -1,6 +0,0 @@
-Copies a file or directory in the project, and returns confirmation that the copy succeeded.
-Directory contents will be copied recursively (like `cp -r`).
-
-This tool should be used when it's desirable to create a copy of a file or directory without modifying the original.
-It's much more efficient than doing this by separately reading and then writing the file or directory's contents,
-so this tool should be preferred over that approach whenever copying is the goal.

crates/assistant_tools/src/create_directory_tool.rs 🔗

@@ -1,100 +0,0 @@
-use crate::schema::json_schema_for;
-use action_log::ActionLog;
-use anyhow::{Context as _, Result, anyhow};
-use assistant_tool::{Tool, ToolResult};
-use gpui::AnyWindowHandle;
-use gpui::{App, Entity, Task};
-use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
-use project::Project;
-use schemars::JsonSchema;
-use serde::{Deserialize, Serialize};
-use std::sync::Arc;
-use ui::IconName;
-use util::markdown::MarkdownInlineCode;
-
-#[derive(Debug, Serialize, Deserialize, JsonSchema)]
-pub struct CreateDirectoryToolInput {
-    /// The path of the new directory.
-    ///
-    /// <example>
-    /// If the project has the following structure:
-    ///
-    /// - directory1/
-    /// - directory2/
-    ///
-    /// You can create a new directory by providing a path of "directory1/new_directory"
-    /// </example>
-    pub path: String,
-}
-
-pub struct CreateDirectoryTool;
-
-impl Tool for CreateDirectoryTool {
-    fn name(&self) -> String {
-        "create_directory".into()
-    }
-
-    fn description(&self) -> String {
-        include_str!("./create_directory_tool/description.md").into()
-    }
-
-    fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
-        false
-    }
-
-    fn may_perform_edits(&self) -> bool {
-        false
-    }
-
-    fn icon(&self) -> IconName {
-        IconName::ToolFolder
-    }
-
-    fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
-        json_schema_for::<CreateDirectoryToolInput>(format)
-    }
-
-    fn ui_text(&self, input: &serde_json::Value) -> String {
-        match serde_json::from_value::<CreateDirectoryToolInput>(input.clone()) {
-            Ok(input) => {
-                format!("Create directory {}", MarkdownInlineCode(&input.path))
-            }
-            Err(_) => "Create directory".to_string(),
-        }
-    }
-
-    fn run(
-        self: Arc<Self>,
-        input: serde_json::Value,
-        _request: Arc<LanguageModelRequest>,
-        project: Entity<Project>,
-        _action_log: Entity<ActionLog>,
-        _model: Arc<dyn LanguageModel>,
-        _window: Option<AnyWindowHandle>,
-        cx: &mut App,
-    ) -> ToolResult {
-        let input = match serde_json::from_value::<CreateDirectoryToolInput>(input) {
-            Ok(input) => input,
-            Err(err) => return Task::ready(Err(anyhow!(err))).into(),
-        };
-        let project_path = match 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"))).into();
-            }
-        };
-        let destination_path: Arc<str> = input.path.as_str().into();
-
-        cx.spawn(async move |cx| {
-            project
-                .update(cx, |project, cx| {
-                    project.create_entry(project_path.clone(), true, cx)
-                })?
-                .await
-                .with_context(|| format!("Creating directory {destination_path}"))?;
-
-            Ok(format!("Created directory {destination_path}").into())
-        })
-        .into()
-    }
-}

crates/assistant_tools/src/create_directory_tool/description.md 🔗

@@ -1,3 +0,0 @@
-Creates a new directory at the specified path within the project. Returns confirmation that the directory was created.
-
-This tool creates a directory and all necessary parent directories (similar to `mkdir -p`). It should be used whenever you need to create new directories within the project.

crates/assistant_tools/src/delete_path_tool.rs 🔗

@@ -1,144 +0,0 @@
-use crate::schema::json_schema_for;
-use action_log::ActionLog;
-use anyhow::{Context as _, Result, anyhow};
-use assistant_tool::{Tool, ToolResult};
-use futures::{SinkExt, StreamExt, channel::mpsc};
-use gpui::{AnyWindowHandle, App, AppContext, Entity, Task};
-use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
-use project::{Project, ProjectPath};
-use schemars::JsonSchema;
-use serde::{Deserialize, Serialize};
-use std::sync::Arc;
-use ui::IconName;
-
-#[derive(Debug, Serialize, Deserialize, JsonSchema)]
-pub struct DeletePathToolInput {
-    /// The path of the file or directory to delete.
-    ///
-    /// <example>
-    /// 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"
-    /// </example>
-    pub path: String,
-}
-
-pub struct DeletePathTool;
-
-impl Tool for DeletePathTool {
-    fn name(&self) -> String {
-        "delete_path".into()
-    }
-
-    fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
-        true
-    }
-
-    fn may_perform_edits(&self) -> bool {
-        true
-    }
-
-    fn description(&self) -> String {
-        include_str!("./delete_path_tool/description.md").into()
-    }
-
-    fn icon(&self) -> IconName {
-        IconName::ToolDeleteFile
-    }
-
-    fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
-        json_schema_for::<DeletePathToolInput>(format)
-    }
-
-    fn ui_text(&self, input: &serde_json::Value) -> String {
-        match serde_json::from_value::<DeletePathToolInput>(input.clone()) {
-            Ok(input) => format!("Delete “`{}`”", input.path),
-            Err(_) => "Delete path".to_string(),
-        }
-    }
-
-    fn run(
-        self: Arc<Self>,
-        input: serde_json::Value,
-        _request: Arc<LanguageModelRequest>,
-        project: Entity<Project>,
-        action_log: Entity<ActionLog>,
-        _model: Arc<dyn LanguageModel>,
-        _window: Option<AnyWindowHandle>,
-        cx: &mut App,
-    ) -> ToolResult {
-        let path_str = match serde_json::from_value::<DeletePathToolInput>(input) {
-            Ok(input) => input.path,
-            Err(err) => return Task::ready(Err(anyhow!(err))).into(),
-        };
-        let Some(project_path) = project.read(cx).find_project_path(&path_str, cx) else {
-            return Task::ready(Err(anyhow!(
-                "Couldn't delete {path_str} because that path isn't in this project."
-            )))
-            .into();
-        };
-
-        let Some(worktree) = project
-            .read(cx)
-            .worktree_for_id(project_path.worktree_id, cx)
-        else {
-            return Task::ready(Err(anyhow!(
-                "Couldn't delete {path_str} because that path isn't in this project."
-            )))
-            .into();
-        };
-
-        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();
-
-        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_str} because that path isn't in this project.")
-                })?;
-            deletion_task
-                .await
-                .with_context(|| format!("Deleting {path_str}"))?;
-            Ok(format!("Deleted {path_str}").into())
-        })
-        .into()
-    }
-}

crates/assistant_tools/src/diagnostics_tool.rs 🔗

@@ -1,171 +0,0 @@
-use crate::schema::json_schema_for;
-use action_log::ActionLog;
-use anyhow::{Result, anyhow};
-use assistant_tool::{Tool, ToolResult};
-use gpui::{AnyWindowHandle, App, Entity, Task};
-use language::{DiagnosticSeverity, OffsetRangeExt};
-use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
-use project::Project;
-use schemars::JsonSchema;
-use serde::{Deserialize, Serialize};
-use std::{fmt::Write, sync::Arc};
-use ui::IconName;
-use util::markdown::MarkdownInlineCode;
-
-#[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.
-    ///
-    /// <example>
-    /// 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`.
-    /// </example>
-    #[serde(deserialize_with = "deserialize_path")]
-    pub path: Option<String>,
-}
-
-fn deserialize_path<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
-where
-    D: serde::Deserializer<'de>,
-{
-    let opt = Option::<String>::deserialize(deserializer)?;
-    // The model passes an empty string sometimes
-    Ok(opt.filter(|s| !s.is_empty()))
-}
-
-pub struct DiagnosticsTool;
-
-impl Tool for DiagnosticsTool {
-    fn name(&self) -> String {
-        "diagnostics".into()
-    }
-
-    fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
-        false
-    }
-
-    fn may_perform_edits(&self) -> bool {
-        false
-    }
-
-    fn description(&self) -> String {
-        include_str!("./diagnostics_tool/description.md").into()
-    }
-
-    fn icon(&self) -> IconName {
-        IconName::ToolDiagnostics
-    }
-
-    fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
-        json_schema_for::<DiagnosticsToolInput>(format)
-    }
-
-    fn ui_text(&self, input: &serde_json::Value) -> String {
-        if let Some(path) = serde_json::from_value::<DiagnosticsToolInput>(input.clone())
-            .ok()
-            .and_then(|input| match input.path {
-                Some(path) if !path.is_empty() => Some(path),
-                _ => None,
-            })
-        {
-            format!("Check diagnostics for {}", MarkdownInlineCode(&path))
-        } else {
-            "Check project diagnostics".to_string()
-        }
-    }
-
-    fn run(
-        self: Arc<Self>,
-        input: serde_json::Value,
-        _request: Arc<LanguageModelRequest>,
-        project: Entity<Project>,
-        _action_log: Entity<ActionLog>,
-        _model: Arc<dyn LanguageModel>,
-        _window: Option<AnyWindowHandle>,
-        cx: &mut App,
-    ) -> ToolResult {
-        match serde_json::from_value::<DiagnosticsToolInput>(input)
-            .ok()
-            .and_then(|input| input.path)
-        {
-            Some(path) if !path.is_empty() => {
-                let Some(project_path) = project.read(cx).find_project_path(&path, cx) else {
-                    return Task::ready(Err(anyhow!("Could not find path {path} in project",)))
-                        .into();
-                };
-
-                let buffer =
-                    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().into())
-                    } else {
-                        Ok(output.into())
-                    }
-                })
-                .into()
-            }
-            _ => {
-                let project = 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.into())).into()
-                } else {
-                    Task::ready(Ok("No errors or warnings found in the project."
-                        .to_string()
-                        .into()))
-                    .into()
-                }
-            }
-        }
-    }
-}

crates/assistant_tools/src/diagnostics_tool/description.md 🔗

@@ -1,21 +0,0 @@
-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.
-
-<example>
-To get diagnostics for a specific file:
-{
-    "path": "src/main.rs"
-}
-
-To get a project-wide diagnostic summary:
-{}
-</example>
-
-<guidelines>
-- 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.
-</guidelines>

crates/assistant_tools/src/edit_file_tool.rs 🔗

@@ -1,2423 +0,0 @@
-use crate::{
-    Templates,
-    edit_agent::{EditAgent, EditAgentOutput, EditAgentOutputEvent, EditFormat},
-    schema::json_schema_for,
-    ui::{COLLAPSED_LINES, ToolOutputPreview},
-};
-use action_log::ActionLog;
-use agent_settings;
-use anyhow::{Context as _, Result, anyhow};
-use assistant_tool::{
-    AnyToolCard, Tool, ToolCard, ToolResult, ToolResultContent, ToolResultOutput, ToolUseStatus,
-};
-use buffer_diff::{BufferDiff, BufferDiffSnapshot};
-use editor::{
-    Editor, EditorMode, MinimapVisibility, MultiBuffer, PathKey, multibuffer_context_lines,
-};
-use futures::StreamExt;
-use gpui::{
-    Animation, AnimationExt, AnyWindowHandle, App, AppContext, AsyncApp, Entity, Task,
-    TextStyleRefinement, WeakEntity, pulsating_between,
-};
-use indoc::formatdoc;
-use language::{
-    Anchor, Buffer, Capability, LanguageRegistry, LineEnding, OffsetRangeExt, Point, Rope,
-    TextBuffer,
-    language_settings::{self, FormatOnSave, SoftWrap},
-};
-use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
-use markdown::{Markdown, MarkdownElement, MarkdownStyle};
-use paths;
-use project::{
-    Project, ProjectPath,
-    lsp_store::{FormatTrigger, LspFormatTarget},
-};
-use schemars::JsonSchema;
-use serde::{Deserialize, Serialize};
-use settings::Settings;
-use std::{
-    cmp::Reverse,
-    collections::HashSet,
-    ffi::OsStr,
-    ops::Range,
-    path::{Path, PathBuf},
-    sync::Arc,
-    time::Duration,
-};
-use theme::ThemeSettings;
-use ui::{CommonAnimationExt, Disclosure, Tooltip, prelude::*};
-use util::{ResultExt, rel_path::RelPath};
-use workspace::Workspace;
-
-pub struct EditFileTool;
-
-#[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.
-    ///
-    /// <example>Fix API endpoint URLs</example>
-    /// <example>Update copyright year in `page_footer`</example>
-    ///
-    /// 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
-    ///
-    /// <example>
-    /// `backend/src/main.rs`
-    ///
-    /// Notice how the file path starts with `backend`. Without that, the path
-    /// would be ambiguous and the call would fail!
-    /// </example>
-    ///
-    /// <example>
-    /// `frontend/db.js`
-    /// </example>
-    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)]
-#[serde(rename_all = "lowercase")]
-pub enum EditFileMode {
-    Edit,
-    Create,
-    Overwrite,
-}
-
-#[derive(Debug, Serialize, Deserialize, JsonSchema)]
-pub struct EditFileToolOutput {
-    pub original_path: PathBuf,
-    pub new_text: String,
-    pub old_text: Arc<String>,
-    pub raw_output: Option<EditAgentOutput>,
-}
-
-#[derive(Debug, Serialize, Deserialize, JsonSchema)]
-struct PartialInput {
-    #[serde(default)]
-    path: String,
-    #[serde(default)]
-    display_description: String,
-}
-
-const DEFAULT_UI_TEXT: &str = "Editing file";
-
-impl Tool for EditFileTool {
-    fn name(&self) -> String {
-        "edit_file".into()
-    }
-
-    fn needs_confirmation(
-        &self,
-        input: &serde_json::Value,
-        project: &Entity<Project>,
-        cx: &App,
-    ) -> bool {
-        if agent_settings::AgentSettings::get_global(cx).always_allow_tool_actions {
-            return false;
-        }
-
-        let Ok(input) = serde_json::from_value::<EditFileToolInput>(input.clone()) else {
-            // If it's not valid JSON, it's going to error and confirming won't do anything.
-            return false;
-        };
-
-        // 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(|c| c.as_os_str() == <str as AsRef<OsStr>>::as_ref(local_settings_folder))
-        {
-            return true;
-        }
-
-        // It's also possible that the global config dir is configured to be inside the project,
-        // so check for that edge case too.
-        if let Ok(canonical_path) = std::fs::canonicalize(&input.path)
-            && canonical_path.starts_with(paths::config_dir())
-        {
-            return true;
-        }
-
-        // Check if path is inside the global config directory
-        // First check if it's already inside project - if not, try to canonicalize
-        let project_path = project.read(cx).find_project_path(&input.path, cx);
-
-        // 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.
-        project_path.is_none()
-    }
-
-    fn may_perform_edits(&self) -> bool {
-        true
-    }
-
-    fn description(&self) -> String {
-        include_str!("edit_file_tool/description.md").to_string()
-    }
-
-    fn icon(&self) -> IconName {
-        IconName::ToolPencil
-    }
-
-    fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
-        json_schema_for::<EditFileToolInput>(format)
-    }
-
-    fn ui_text(&self, input: &serde_json::Value) -> String {
-        match serde_json::from_value::<EditFileToolInput>(input.clone()) {
-            Ok(input) => {
-                let path = Path::new(&input.path);
-                let mut description = input.display_description.clone();
-
-                // Add context about why confirmation may be needed
-                let local_settings_folder = paths::local_settings_folder_name();
-                if path
-                    .components()
-                    .any(|c| c.as_os_str() == <str as AsRef<OsStr>>::as_ref(local_settings_folder))
-                {
-                    description.push_str(" (local settings)");
-                } else if let Ok(canonical_path) = std::fs::canonicalize(&input.path)
-                    && canonical_path.starts_with(paths::config_dir())
-                {
-                    description.push_str(" (global settings)");
-                }
-
-                description
-            }
-            Err(_) => "Editing file".to_string(),
-        }
-    }
-
-    fn still_streaming_ui_text(&self, input: &serde_json::Value) -> String {
-        if let Some(input) = serde_json::from_value::<PartialInput>(input.clone()).ok() {
-            let description = input.display_description.trim();
-            if !description.is_empty() {
-                return description.to_string();
-            }
-
-            let path = input.path.trim();
-            if !path.is_empty() {
-                return path.to_string();
-            }
-        }
-
-        DEFAULT_UI_TEXT.to_string()
-    }
-
-    fn run(
-        self: Arc<Self>,
-        input: serde_json::Value,
-        request: Arc<LanguageModelRequest>,
-        project: Entity<Project>,
-        action_log: Entity<ActionLog>,
-        model: Arc<dyn LanguageModel>,
-        window: Option<AnyWindowHandle>,
-        cx: &mut App,
-    ) -> ToolResult {
-        let input = match serde_json::from_value::<EditFileToolInput>(input) {
-            Ok(input) => input,
-            Err(err) => return Task::ready(Err(anyhow!(err))).into(),
-        };
-
-        let project_path = match resolve_path(&input, project.clone(), cx) {
-            Ok(path) => path,
-            Err(err) => return Task::ready(Err(anyhow!(err))).into(),
-        };
-
-        let card = window.and_then(|window| {
-            window
-                .update(cx, |_, window, cx| {
-                    cx.new(|cx| {
-                        EditFileToolCard::new(input.path.clone(), project.clone(), window, cx)
-                    })
-                })
-                .ok()
-        });
-
-        let card_clone = card.clone();
-        let action_log_clone = action_log.clone();
-        let task = cx.spawn(async move |cx: &mut AsyncApp| {
-            let edit_format = EditFormat::from_model(model.clone())?;
-            let edit_agent = EditAgent::new(
-                model,
-                project.clone(),
-                action_log_clone,
-                Templates::new(),
-                edit_format,
-            );
-
-            let buffer = project
-                .update(cx, |project, cx| {
-                    project.open_buffer(project_path.clone(), cx)
-                })?
-                .await?;
-
-            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;
-
-            if let Some(card) = card_clone.as_ref() {
-                card.update(cx, |card, cx| card.initialize(buffer.clone(), cx))?;
-            }
-
-            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();
-            while let Some(event) = events.next().await {
-                match event {
-                    EditAgentOutputEvent::Edited { .. } => {
-                        if let Some(card) = card_clone.as_ref() {
-                            card.update(cx, |card, cx| card.update_diff(cx))?;
-                        }
-                    }
-                    EditAgentOutputEvent::UnresolvedEditRange => hallucinated_old_text = true,
-                    EditAgentOutputEvent::AmbiguousEditRange(ranges) => ambiguous_ranges = ranges,
-                    EditAgentOutputEvent::ResolvingEditRange(range) => {
-                        if let Some(card) = card_clone.as_ref() {
-                            card.update(cx, |card, cx| card.reveal_range(range, cx))?;
-                        }
-                    }
-                }
-            }
-            let agent_output = output.await?;
-
-            // 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,
-                    );
-                    !matches!(settings.format_on_save, FormatOnSave::Off)
-                })
-                .unwrap_or(false);
-
-            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?;
-
-            // Notify the action log that we've edited the buffer (*after* formatting has completed).
-            action_log.update(cx, |log, cx| {
-                log.buffer_edited(buffer.clone(), cx);
-            })?;
-
-            let new_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
-            let (new_text, 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 output = EditFileToolOutput {
-                original_path: project_path.path.as_std_path().to_owned(),
-                new_text,
-                old_text,
-                raw_output: Some(agent_output),
-            };
-
-            if let Some(card) = card_clone {
-                card.update(cx, |card, cx| {
-                    card.update_diff(cx);
-                    card.finalize(cx)
-                })
-                .log_err();
-            }
-
-            let input_path = input.path.display();
-            if 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::<Vec<_>>()
-                            .join(", ");
-                        formatdoc! {"
-                            <old_text> matches more than one position in the file (lines: {line_numbers}). Read the
-                            relevant sections of {input_path} again and extend <old_text> so
-                            that I can perform the requested edits.
-                        "}
-                    }
-                );
-                Ok(ToolResultOutput {
-                    content: ToolResultContent::Text("No edits were made.".into()),
-                    output: serde_json::to_value(output).ok(),
-                })
-            } else {
-                Ok(ToolResultOutput {
-                    content: ToolResultContent::Text(format!(
-                        "Edited {}:\n\n```diff\n{}\n```",
-                        input_path, diff
-                    )),
-                    output: serde_json::to_value(output).ok(),
-                })
-            }
-        });
-
-        ToolResult {
-            output: task,
-            card: card.map(AnyToolCard::from),
-        }
-    }
-
-    fn deserialize_card(
-        self: Arc<Self>,
-        output: serde_json::Value,
-        project: Entity<Project>,
-        window: &mut Window,
-        cx: &mut App,
-    ) -> Option<AnyToolCard> {
-        let output = match serde_json::from_value::<EditFileToolOutput>(output) {
-            Ok(output) => output,
-            Err(_) => return None,
-        };
-
-        let card = cx.new(|cx| {
-            EditFileToolCard::new(output.original_path.clone(), project.clone(), window, cx)
-        });
-
-        cx.spawn({
-            let path: Arc<Path> = output.original_path.into();
-            let language_registry = project.read(cx).languages().clone();
-            let card = card.clone();
-            async move |cx| {
-                let buffer =
-                    build_buffer(output.new_text, path.clone(), &language_registry, cx).await?;
-                let buffer_diff =
-                    build_buffer_diff(output.old_text.clone(), &buffer, &language_registry, cx)
-                        .await?;
-                card.update(cx, |card, cx| {
-                    card.multibuffer.update(cx, |multibuffer, cx| {
-                        let snapshot = buffer.read(cx).snapshot();
-                        let diff = buffer_diff.read(cx);
-                        let diff_hunk_ranges = diff
-                            .hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx)
-                            .map(|diff_hunk| diff_hunk.buffer_range.to_point(&snapshot))
-                            .collect::<Vec<_>>();
-
-                        multibuffer.set_excerpts_for_path(
-                            PathKey::for_buffer(&buffer, cx),
-                            buffer,
-                            diff_hunk_ranges,
-                            multibuffer_context_lines(cx),
-                            cx,
-                        );
-                        multibuffer.add_diff(buffer_diff, cx);
-                        let end = multibuffer.len(cx);
-                        card.total_lines =
-                            Some(multibuffer.snapshot(cx).offset_to_point(end).row + 1);
-                    });
-
-                    cx.notify();
-                })?;
-                anyhow::Ok(())
-            }
-        })
-        .detach_and_log_err(cx);
-
-        Some(card.into())
-    }
-}
-
-/// 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<Project>,
-    cx: &mut App,
-) -> Result<ProjectPath> {
-    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())
-                .context("Can't create file: invalid filename")?;
-
-            let new_file_path = parent_project_path.map(|parent| ProjectPath {
-                path: parent.path.join(RelPath::unix(file_name).unwrap()),
-                ..parent
-            });
-
-            new_file_path.context("Can't create file")
-        }
-    }
-}
-
-pub struct EditFileToolCard {
-    path: PathBuf,
-    editor: Entity<Editor>,
-    multibuffer: Entity<MultiBuffer>,
-    project: Entity<Project>,
-    buffer: Option<Entity<Buffer>>,
-    base_text: Option<Arc<String>>,
-    buffer_diff: Option<Entity<BufferDiff>>,
-    revealed_ranges: Vec<Range<Anchor>>,
-    diff_task: Option<Task<Result<()>>>,
-    preview_expanded: bool,
-    error_expanded: Option<Entity<Markdown>>,
-    full_height_expanded: bool,
-    total_lines: Option<u32>,
-}
-
-impl EditFileToolCard {
-    pub fn new(path: PathBuf, project: Entity<Project>, window: &mut Window, cx: &mut App) -> Self {
-        let expand_edit_card = agent_settings::AgentSettings::get_global(cx).expand_edit_card;
-        let multibuffer = cx.new(|_| MultiBuffer::without_headers(Capability::ReadOnly));
-
-        let editor = cx.new(|cx| {
-            let mut editor = Editor::new(
-                EditorMode::Full {
-                    scale_ui_elements_with_buffer_font_size: false,
-                    show_active_line_background: false,
-                    sized_by_content: true,
-                },
-                multibuffer.clone(),
-                Some(project.clone()),
-                window,
-                cx,
-            );
-            editor.set_show_gutter(false, cx);
-            editor.disable_inline_diagnostics();
-            editor.disable_expand_excerpt_buttons(cx);
-            // Keep horizontal scrollbar so user can scroll horizontally if needed
-            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
-        });
-        Self {
-            path,
-            project,
-            editor,
-            multibuffer,
-            buffer: None,
-            base_text: None,
-            buffer_diff: None,
-            revealed_ranges: Vec::new(),
-            diff_task: None,
-            preview_expanded: true,
-            error_expanded: None,
-            full_height_expanded: expand_edit_card,
-            total_lines: None,
-        }
-    }
-
-    pub fn initialize(&mut self, buffer: Entity<Buffer>, cx: &mut App) {
-        let buffer_snapshot = buffer.read(cx).snapshot();
-        let base_text = buffer_snapshot.text();
-        let language_registry = buffer.read(cx).language_registry();
-        let text_snapshot = buffer.read(cx).text_snapshot();
-
-        // Create a buffer diff with the current text as the base
-        let buffer_diff = cx.new(|cx| {
-            let mut diff = BufferDiff::new(&text_snapshot, cx);
-            let _ = diff.set_base_text(
-                buffer_snapshot.clone(),
-                language_registry,
-                text_snapshot,
-                cx,
-            );
-            diff
-        });
-
-        self.buffer = Some(buffer);
-        self.base_text = Some(base_text.into());
-        self.buffer_diff = Some(buffer_diff.clone());
-
-        // Add the diff to the multibuffer
-        self.multibuffer
-            .update(cx, |multibuffer, cx| multibuffer.add_diff(buffer_diff, cx));
-    }
-
-    pub fn is_loading(&self) -> bool {
-        self.total_lines.is_none()
-    }
-
-    pub fn update_diff(&mut self, cx: &mut Context<Self>) {
-        let Some(buffer) = self.buffer.as_ref() else {
-            return;
-        };
-        let Some(buffer_diff) = self.buffer_diff.as_ref() else {
-            return;
-        };
-
-        let buffer = buffer.clone();
-        let buffer_diff = buffer_diff.clone();
-        let base_text = self.base_text.clone();
-        self.diff_task = Some(cx.spawn(async move |this, cx| {
-            let text_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot())?;
-            let diff_snapshot = BufferDiff::update_diff(
-                buffer_diff.clone(),
-                text_snapshot.clone(),
-                base_text,
-                false,
-                false,
-                None,
-                None,
-                cx,
-            )
-            .await?;
-            buffer_diff.update(cx, |diff, cx| {
-                diff.set_snapshot(diff_snapshot, &text_snapshot, cx)
-            })?;
-            this.update(cx, |this, cx| this.update_visible_ranges(cx))
-        }));
-    }
-
-    pub fn reveal_range(&mut self, range: Range<Anchor>, cx: &mut Context<Self>) {
-        self.revealed_ranges.push(range);
-        self.update_visible_ranges(cx);
-    }
-
-    fn update_visible_ranges(&mut self, cx: &mut Context<Self>) {
-        let Some(buffer) = self.buffer.as_ref() else {
-            return;
-        };
-
-        let ranges = self.excerpt_ranges(cx);
-        self.total_lines = self.multibuffer.update(cx, |multibuffer, cx| {
-            multibuffer.set_excerpts_for_path(
-                PathKey::for_buffer(buffer, cx),
-                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<Range<Point>> {
-        let Some(buffer) = self.buffer.as_ref() else {
-            return Vec::new();
-        };
-        let Some(diff) = self.buffer_diff.as_ref() else {
-            return Vec::new();
-        };
-
-        let buffer = buffer.read(cx);
-        let diff = diff.read(cx);
-        let mut ranges = diff
-            .hunks_intersecting_range(Anchor::MIN..Anchor::MAX, buffer, cx)
-            .map(|diff_hunk| diff_hunk.buffer_range.to_point(buffer))
-            .collect::<Vec<_>>();
-        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 fn finalize(&mut self, cx: &mut Context<Self>) -> Result<()> {
-        let ranges = self.excerpt_ranges(cx);
-        let buffer = self.buffer.take().context("card was already finalized")?;
-        let base_text = self
-            .base_text
-            .take()
-            .context("card was already finalized")?;
-        let language_registry = self.project.read(cx).languages().clone();
-
-        // Replace the buffer in the multibuffer with the snapshot
-        let buffer = cx.new(|cx| {
-            let language = buffer.read(cx).language().cloned();
-            let buffer = TextBuffer::new_normalized(
-                0,
-                cx.entity_id().as_non_zero_u64().into(),
-                buffer.read(cx).line_ending(),
-                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
-            }
-        });
-
-        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();
-            })
-        })
-        .detach_and_log_err(cx);
-        Ok(())
-    }
-}
-
-impl ToolCard for EditFileToolCard {
-    fn render(
-        &mut self,
-        status: &ToolUseStatus,
-        window: &mut Window,
-        workspace: WeakEntity<Workspace>,
-        cx: &mut Context<Self>,
-    ) -> impl IntoElement {
-        let error_message = match status {
-            ToolUseStatus::Error(err) => Some(err),
-            _ => None,
-        };
-
-        let running_or_pending = match status {
-            ToolUseStatus::Running | ToolUseStatus::Pending => Some(()),
-            _ => None,
-        };
-
-        let should_show_loading = running_or_pending.is_some() && !self.full_height_expanded;
-
-        let path_label_button = h_flex()
-            .id(("edit-tool-path-label-button", self.editor.entity_id()))
-            .w_full()
-            .max_w_full()
-            .px_1()
-            .gap_0p5()
-            .cursor_pointer()
-            .rounded_sm()
-            .opacity(0.8)
-            .hover(|label| {
-                label
-                    .opacity(1.)
-                    .bg(cx.theme().colors().element_hover.opacity(0.5))
-            })
-            .tooltip(Tooltip::text("Jump to File"))
-            .child(
-                h_flex()
-                    .child(
-                        Icon::new(IconName::ToolPencil)
-                            .size(IconSize::Small)
-                            .color(Color::Muted),
-                    )
-                    .child(
-                        div()
-                            .text_size(rems(0.8125))
-                            .child(self.path.display().to_string())
-                            .ml_1p5()
-                            .mr_0p5(),
-                    )
-                    .child(
-                        Icon::new(IconName::ArrowUpRight)
-                            .size(IconSize::Small)
-                            .color(Color::Ignored),
-                    ),
-            )
-            .on_click({
-                let path = self.path.clone();
-                move |_, window, cx| {
-                    workspace
-                        .update(cx, {
-                            |workspace, cx| {
-                                let Some(project_path) =
-                                    workspace.project().read(cx).find_project_path(&path, cx)
-                                else {
-                                    return;
-                                };
-                                let open_task =
-                                    workspace.open_path(project_path, None, true, window, cx);
-                                window
-                                    .spawn(cx, async move |cx| {
-                                        let item = open_task.await?;
-                                        if let Some(active_editor) = item.downcast::<Editor>() {
-                                            active_editor
-                                                .update_in(cx, |editor, window, cx| {
-                                                    let snapshot =
-                                                        editor.buffer().read(cx).snapshot(cx);
-                                                    let first_hunk = editor
-                                                        .diff_hunks_in_ranges(
-                                                            &[editor::Anchor::min()
-                                                                ..editor::Anchor::max()],
-                                                            &snapshot,
-                                                        )
-                                                        .next();
-                                                    if let Some(first_hunk) = first_hunk {
-                                                        let first_hunk_start =
-                                                            first_hunk.multi_buffer_range().start;
-                                                        editor.change_selections(
-                                                            Default::default(),
-                                                            window,
-                                                            cx,
-                                                            |selections| {
-                                                                selections.select_anchor_ranges([
-                                                                    first_hunk_start
-                                                                        ..first_hunk_start,
-                                                                ]);
-                                                            },
-                                                        )
-                                                    }
-                                                })
-                                                .log_err();
-                                        }
-                                        anyhow::Ok(())
-                                    })
-                                    .detach_and_log_err(cx);
-                            }
-                        })
-                        .ok();
-                }
-            })
-            .into_any_element();
-
-        let codeblock_header_bg = cx
-            .theme()
-            .colors()
-            .element_background
-            .blend(cx.theme().colors().editor_foreground.opacity(0.025));
-
-        let codeblock_header = h_flex()
-            .flex_none()
-            .p_1()
-            .gap_1()
-            .justify_between()
-            .rounded_t_md()
-            .when(error_message.is_none(), |header| {
-                header.bg(codeblock_header_bg)
-            })
-            .child(path_label_button)
-            .when(should_show_loading, |header| {
-                header.pr_1p5().child(
-                    Icon::new(IconName::ArrowCircle)
-                        .size(IconSize::XSmall)
-                        .color(Color::Info)
-                        .with_rotate_animation(2),
-                )
-            })
-            .when_some(error_message, |header, error_message| {
-                header.child(
-                    h_flex()
-                        .gap_1()
-                        .child(
-                            Icon::new(IconName::Close)
-                                .size(IconSize::Small)
-                                .color(Color::Error),
-                        )
-                        .child(
-                            Disclosure::new(
-                                ("edit-file-error-disclosure", self.editor.entity_id()),
-                                self.error_expanded.is_some(),
-                            )
-                            .opened_icon(IconName::ChevronUp)
-                            .closed_icon(IconName::ChevronDown)
-                            .on_click(cx.listener({
-                                let error_message = error_message.clone();
-
-                                move |this, _event, _window, cx| {
-                                    if this.error_expanded.is_some() {
-                                        this.error_expanded.take();
-                                    } else {
-                                        this.error_expanded = Some(cx.new(|cx| {
-                                            Markdown::new(error_message.clone(), None, None, cx)
-                                        }))
-                                    }
-                                    cx.notify();
-                                }
-                            })),
-                        ),
-                )
-            })
-            .when(error_message.is_none() && !self.is_loading(), |header| {
-                header.child(
-                    Disclosure::new(
-                        ("edit-file-disclosure", self.editor.entity_id()),
-                        self.preview_expanded,
-                    )
-                    .opened_icon(IconName::ChevronUp)
-                    .closed_icon(IconName::ChevronDown)
-                    .on_click(cx.listener(
-                        move |this, _event, _window, _cx| {
-                            this.preview_expanded = !this.preview_expanded;
-                        },
-                    )),
-                )
-            });
-
-        let (editor, editor_line_height) = self.editor.update(cx, |editor, cx| {
-            let line_height = editor
-                .style()
-                .map(|style| style.text.line_height_in_pixels(window.rem_size()))
-                .unwrap_or_default();
-
-            editor.set_text_style_refinement(TextStyleRefinement {
-                font_size: Some(
-                    TextSize::Small
-                        .rems(cx)
-                        .to_pixels(ThemeSettings::get_global(cx).agent_ui_font_size(cx))
-                        .into(),
-                ),
-                ..TextStyleRefinement::default()
-            });
-            let element = editor.render(window, cx);
-            (element.into_any_element(), line_height)
-        });
-
-        let border_color = cx.theme().colors().border.opacity(0.6);
-
-        let waiting_for_diff = {
-            let styles = [
-                ("w_4_5", (0.1, 0.85), 2000),
-                ("w_1_4", (0.2, 0.75), 2200),
-                ("w_2_4", (0.15, 0.64), 1900),
-                ("w_3_5", (0.25, 0.72), 2300),
-                ("w_2_5", (0.3, 0.56), 1800),
-            ];
-
-            let mut container = v_flex()
-                .p_3()
-                .gap_1()
-                .border_t_1()
-                .rounded_b_md()
-                .border_color(border_color)
-                .bg(cx.theme().colors().editor_background);
-
-            for (width_method, pulse_range, duration_ms) in styles.iter() {
-                let (min_opacity, max_opacity) = *pulse_range;
-                let placeholder = match *width_method {
-                    "w_4_5" => div().w_3_4(),
-                    "w_1_4" => div().w_1_4(),
-                    "w_2_4" => div().w_2_4(),
-                    "w_3_5" => div().w_3_5(),
-                    "w_2_5" => div().w_2_5(),
-                    _ => div().w_1_2(),
-                }
-                .id("loading_div")
-                .h_1()
-                .rounded_full()
-                .bg(cx.theme().colors().element_active)
-                .with_animation(
-                    "loading_pulsate",
-                    Animation::new(Duration::from_millis(*duration_ms))
-                        .repeat()
-                        .with_easing(pulsating_between(min_opacity, max_opacity)),
-                    |label, delta| label.opacity(delta),
-                );
-
-                container = container.child(placeholder);
-            }
-
-            container
-        };
-
-        v_flex()
-            .mb_2()
-            .border_1()
-            .when(error_message.is_some(), |card| card.border_dashed())
-            .border_color(border_color)
-            .rounded_md()
-            .overflow_hidden()
-            .child(codeblock_header)
-            .when_some(self.error_expanded.as_ref(), |card, error_markdown| {
-                card.child(
-                    v_flex()
-                        .p_2()
-                        .gap_1()
-                        .border_t_1()
-                        .border_dashed()
-                        .border_color(border_color)
-                        .bg(cx.theme().colors().editor_background)
-                        .rounded_b_md()
-                        .child(
-                            Label::new("Error")
-                                .size(LabelSize::XSmall)
-                                .color(Color::Error),
-                        )
-                        .child(
-                            div()
-                                .rounded_md()
-                                .text_ui_sm(cx)
-                                .bg(cx.theme().colors().editor_background)
-                                .child(MarkdownElement::new(
-                                    error_markdown.clone(),
-                                    markdown_style(window, cx),
-                                )),
-                        ),
-                )
-            })
-            .when(self.is_loading() && error_message.is_none(), |card| {
-                card.child(waiting_for_diff)
-            })
-            .when(self.preview_expanded && !self.is_loading(), |card| {
-                let editor_view = v_flex()
-                    .relative()
-                    .h_full()
-                    .when(!self.full_height_expanded, |editor_container| {
-                        editor_container.max_h(COLLAPSED_LINES as f32 * editor_line_height)
-                    })
-                    .overflow_hidden()
-                    .border_t_1()
-                    .border_color(border_color)
-                    .bg(cx.theme().colors().editor_background)
-                    .child(editor);
-
-                card.child(
-                    ToolOutputPreview::new(editor_view.into_any_element(), self.editor.entity_id())
-                        .with_total_lines(self.total_lines.unwrap_or(0) as usize)
-                        .toggle_state(self.full_height_expanded)
-                        .with_collapsed_fade()
-                        .on_toggle({
-                            let this = cx.entity().downgrade();
-                            move |is_expanded, _window, cx| {
-                                if let Some(this) = this.upgrade() {
-                                    this.update(cx, |this, _cx| {
-                                        this.full_height_expanded = is_expanded;
-                                    });
-                                }
-                            }
-                        }),
-                )
-            })
-    }
-}
-
-fn markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
-    let theme_settings = ThemeSettings::get_global(cx);
-    let ui_font_size = TextSize::Default.rems(cx);
-    let mut text_style = window.text_style();
-
-    text_style.refine(&TextStyleRefinement {
-        font_family: Some(theme_settings.ui_font.family.clone()),
-        font_fallbacks: theme_settings.ui_font.fallbacks.clone(),
-        font_features: Some(theme_settings.ui_font.features.clone()),
-        font_size: Some(ui_font_size.into()),
-        color: Some(cx.theme().colors().text),
-        ..Default::default()
-    });
-
-    MarkdownStyle {
-        base_text_style: text_style.clone(),
-        selection_background_color: cx.theme().colors().element_selection_background,
-        ..Default::default()
-    }
-}
-
-async fn build_buffer(
-    mut text: String,
-    path: Arc<Path>,
-    language_registry: &Arc<language::LanguageRegistry>,
-    cx: &mut AsyncApp,
-) -> Result<Entity<Buffer>> {
-    let line_ending = LineEnding::detect(&text);
-    LineEnding::normalize(&mut text);
-    let text = Rope::from(text);
-    let language = cx
-        .update(|_cx| language_registry.load_language_for_file_path(&path))?
-        .await
-        .ok();
-    let buffer = cx.new(|cx| {
-        let buffer = TextBuffer::new_normalized(
-            0,
-            cx.entity_id().as_non_zero_u64().into(),
-            line_ending,
-            text,
-        );
-        let mut buffer = Buffer::build(buffer, None, Capability::ReadWrite);
-        buffer.set_language(language, cx);
-        buffer
-    })?;
-    Ok(buffer)
-}
-
-async fn build_buffer_diff(
-    old_text: Arc<String>,
-    buffer: &Entity<Buffer>,
-    language_registry: &Arc<LanguageRegistry>,
-    cx: &mut AsyncApp,
-) -> Result<Entity<BufferDiff>> {
-    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(),
-                Some(language_registry.clone()),
-                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 super::*;
-    use ::fs::Fs;
-    use client::TelemetrySettings;
-    use gpui::{TestAppContext, UpdateGlobal};
-    use language_model::fake_provider::FakeLanguageModel;
-    use serde_json::json;
-    use settings::SettingsStore;
-    use std::fs;
-    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 action_log = cx.new(|_| ActionLog::new(project.clone()));
-        let model = Arc::new(FakeLanguageModel::default());
-        let result = cx
-            .update(|cx| {
-                let input = serde_json::to_value(EditFileToolInput {
-                    display_description: "Some edit".into(),
-                    path: "root/nonexistent_file.txt".into(),
-                    mode: EditFileMode::Edit,
-                })
-                .unwrap();
-                Arc::new(EditFileTool)
-                    .run(
-                        input,
-                        Arc::default(),
-                        project.clone(),
-                        action_log,
-                        model,
-                        None,
-                        cx,
-                    )
-                    .output
-            })
-            .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, "new.txt");
-
-        let result = test_resolve_path(mode, "new.txt", cx);
-        assert_resolved_path_eq(result.await, "new.txt");
-
-        let result = test_resolve_path(mode, "dir/new.txt", cx);
-        assert_resolved_path_eq(result.await, "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, path_without_root);
-
-        let result = test_resolve_path(mode, path_without_root, cx);
-        assert_resolved_path_eq(result.await, 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<ProjectPath> {
-        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<ProjectPath>, expected: &str) {
-        let actual = path.expect("Should return valid path").path;
-        assert_eq!(actual.as_ref(), rel_path(expected));
-    }
-
-    #[test]
-    fn still_streaming_ui_text_with_path() {
-        let input = json!({
-            "path": "src/main.rs",
-            "display_description": "",
-            "old_string": "old code",
-            "new_string": "new code"
-        });
-
-        assert_eq!(EditFileTool.still_streaming_ui_text(&input), "src/main.rs");
-    }
-
-    #[test]
-    fn still_streaming_ui_text_with_description() {
-        let input = json!({
-            "path": "",
-            "display_description": "Fix error handling",
-            "old_string": "old code",
-            "new_string": "new code"
-        });
-
-        assert_eq!(
-            EditFileTool.still_streaming_ui_text(&input),
-            "Fix error handling",
-        );
-    }
-
-    #[test]
-    fn still_streaming_ui_text_with_path_and_description() {
-        let input = json!({
-            "path": "src/main.rs",
-            "display_description": "Fix error handling",
-            "old_string": "old code",
-            "new_string": "new code"
-        });
-
-        assert_eq!(
-            EditFileTool.still_streaming_ui_text(&input),
-            "Fix error handling",
-        );
-    }
-
-    #[test]
-    fn still_streaming_ui_text_no_path_or_description() {
-        let input = json!({
-            "path": "",
-            "display_description": "",
-            "old_string": "old code",
-            "new_string": "new code"
-        });
-
-        assert_eq!(
-            EditFileTool.still_streaming_ui_text(&input),
-            DEFAULT_UI_TEXT,
-        );
-    }
-
-    #[test]
-    fn still_streaming_ui_text_with_null() {
-        let input = serde_json::Value::Null;
-
-        assert_eq!(
-            EditFileTool.still_streaming_ui_text(&input),
-            DEFAULT_UI_TEXT,
-        );
-    }
-
-    fn init_test(cx: &mut TestAppContext) {
-        cx.update(|cx| {
-            let settings_store = SettingsStore::test(cx);
-            cx.set_global(settings_store);
-            language::init(cx);
-            TelemetrySettings::register(cx);
-            agent_settings::AgentSettings::register(cx);
-            Project::init_settings(cx);
-        });
-    }
-
-    fn init_test_with_config(cx: &mut TestAppContext, data_dir: &Path) {
-        cx.update(|cx| {
-            paths::set_custom_data_dir(data_dir.to_str().unwrap());
-            // Set custom data directory (config will be under data_dir/config)
-
-            let settings_store = SettingsStore::test(cx);
-            cx.set_global(settings_store);
-            language::init(cx);
-            TelemetrySettings::register(cx);
-            agent_settings::AgentSettings::register(cx);
-            Project::init_settings(cx);
-        });
-    }
-
-    #[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::<lsp::request::Formatting, _, _>({
-            |_, _| 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 action_log = cx.new(|_| ActionLog::new(project.clone()));
-        let model = Arc::new(FakeLanguageModel::default());
-
-        // 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 = serde_json::to_value(EditFileToolInput {
-                    display_description: "Create main function".into(),
-                    path: "root/src/main.rs".into(),
-                    mode: EditFileMode::Overwrite,
-                })
-                .unwrap();
-                Arc::new(EditFileTool)
-                    .run(
-                        input,
-                        Arc::default(),
-                        project.clone(),
-                        action_log.clone(),
-                        model.clone(),
-                        None,
-                        cx,
-                    )
-                    .output
-            });
-
-            // 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 = action_log.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 = serde_json::to_value(EditFileToolInput {
-                    display_description: "Update main function".into(),
-                    path: "root/src/main.rs".into(),
-                    mode: EditFileMode::Overwrite,
-                })
-                .unwrap();
-                Arc::new(EditFileTool)
-                    .run(
-                        input,
-                        Arc::default(),
-                        project.clone(),
-                        action_log.clone(),
-                        model.clone(),
-                        None,
-                        cx,
-                    )
-                    .output
-            });
-
-            // 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 action_log = cx.new(|_| ActionLog::new(project.clone()));
-        let model = Arc::new(FakeLanguageModel::default());
-
-        // 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 = serde_json::to_value(EditFileToolInput {
-                    display_description: "Create main function".into(),
-                    path: "root/src/main.rs".into(),
-                    mode: EditFileMode::Overwrite,
-                })
-                .unwrap();
-                Arc::new(EditFileTool)
-                    .run(
-                        input,
-                        Arc::default(),
-                        project.clone(),
-                        action_log.clone(),
-                        model.clone(),
-                        None,
-                        cx,
-                    )
-                    .output
-            });
-
-            // 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 = serde_json::to_value(EditFileToolInput {
-                    display_description: "Update main function".into(),
-                    path: "root/src/main.rs".into(),
-                    mode: EditFileMode::Overwrite,
-                })
-                .unwrap();
-                Arc::new(EditFileTool)
-                    .run(
-                        input,
-                        Arc::default(),
-                        project.clone(),
-                        action_log.clone(),
-                        model.clone(),
-                        None,
-                        cx,
-                    )
-                    .output
-            });
-
-            // 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_needs_confirmation(cx: &mut TestAppContext) {
-        init_test(cx);
-        let tool = Arc::new(EditFileTool);
-        let fs = project::FakeFs::new(cx.executor());
-        fs.insert_tree("/root", json!({})).await;
-
-        // Test 1: Path with .zed component should require confirmation
-        let input_with_zed = json!({
-            "display_description": "Edit settings",
-            "path": ".zed/settings.json",
-            "mode": "edit"
-        });
-        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
-        cx.update(|cx| {
-            assert!(
-                tool.needs_confirmation(&input_with_zed, &project, cx),
-                "Path with .zed component should require confirmation"
-            );
-        });
-
-        // Test 2: Absolute path should require confirmation
-        let input_absolute = json!({
-            "display_description": "Edit file",
-            "path": "/etc/hosts",
-            "mode": "edit"
-        });
-        cx.update(|cx| {
-            assert!(
-                tool.needs_confirmation(&input_absolute, &project, cx),
-                "Absolute path should require confirmation"
-            );
-        });
-
-        // Test 3: Relative path without .zed should not require confirmation
-        let input_relative = json!({
-            "display_description": "Edit file",
-            "path": "root/src/main.rs",
-            "mode": "edit"
-        });
-        cx.update(|cx| {
-            assert!(
-                !tool.needs_confirmation(&input_relative, &project, cx),
-                "Relative path without .zed should not require confirmation"
-            );
-        });
-
-        // Test 4: Path with .zed in the middle should require confirmation
-        let input_zed_middle = json!({
-            "display_description": "Edit settings",
-            "path": "root/.zed/tasks.json",
-            "mode": "edit"
-        });
-        cx.update(|cx| {
-            assert!(
-                tool.needs_confirmation(&input_zed_middle, &project, cx),
-                "Path with .zed in any component should require confirmation"
-            );
-        });
-
-        // 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);
-
-            assert!(
-                !tool.needs_confirmation(&input_with_zed, &project, cx),
-                "When always_allow_tool_actions is true, no confirmation should be needed"
-            );
-            assert!(
-                !tool.needs_confirmation(&input_absolute, &project, cx),
-                "When always_allow_tool_actions is true, no confirmation should be needed for absolute paths"
-            );
-        });
-    }
-
-    #[gpui::test]
-    async fn test_ui_text_shows_correct_context(cx: &mut TestAppContext) {
-        // Set up a custom config directory for testing
-        let temp_dir = tempfile::tempdir().unwrap();
-        init_test_with_config(cx, temp_dir.path());
-
-        let tool = Arc::new(EditFileTool);
-
-        // Test ui_text shows context for various paths
-        let test_cases = vec![
-            (
-                json!({
-                    "display_description": "Update config",
-                    "path": ".zed/settings.json",
-                    "mode": "edit"
-                }),
-                "Update config (local settings)",
-                ".zed path should show local settings context",
-            ),
-            (
-                json!({
-                    "display_description": "Fix bug",
-                    "path": "src/.zed/local.json",
-                    "mode": "edit"
-                }),
-                "Fix bug (local settings)",
-                "Nested .zed path should show local settings context",
-            ),
-            (
-                json!({
-                    "display_description": "Update readme",
-                    "path": "README.md",
-                    "mode": "edit"
-                }),
-                "Update readme",
-                "Normal path should not show additional context",
-            ),
-            (
-                json!({
-                    "display_description": "Edit config",
-                    "path": "config.zed",
-                    "mode": "edit"
-                }),
-                "Edit config",
-                ".zed as extension should not show context",
-            ),
-        ];
-
-        for (input, expected_text, description) in test_cases {
-            cx.update(|_cx| {
-                let ui_text = tool.ui_text(&input);
-                assert_eq!(ui_text, expected_text, "Failed for case: {}", description);
-            });
-        }
-    }
-
-    #[gpui::test]
-    async fn test_needs_confirmation_outside_project(cx: &mut TestAppContext) {
-        init_test(cx);
-        let tool = Arc::new(EditFileTool);
-        let fs = project::FakeFs::new(cx.executor());
-
-        // Create a project in /project directory
-        fs.insert_tree("/project", json!({})).await;
-        let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
-
-        // Test file outside project requires confirmation
-        let input_outside = json!({
-            "display_description": "Edit file",
-            "path": "/outside/file.txt",
-            "mode": "edit"
-        });
-        cx.update(|cx| {
-            assert!(
-                tool.needs_confirmation(&input_outside, &project, cx),
-                "File outside project should require confirmation"
-            );
-        });
-
-        // Test file inside project doesn't require confirmation
-        let input_inside = json!({
-            "display_description": "Edit file",
-            "path": "project/file.txt",
-            "mode": "edit"
-        });
-        cx.update(|cx| {
-            assert!(
-                !tool.needs_confirmation(&input_inside, &project, cx),
-                "File inside project should not require confirmation"
-            );
-        });
-    }
-
-    #[gpui::test]
-    async fn test_needs_confirmation_config_paths(cx: &mut TestAppContext) {
-        // Set up a custom data directory for testing
-        let temp_dir = tempfile::tempdir().unwrap();
-        init_test_with_config(cx, temp_dir.path());
-
-        let tool = Arc::new(EditFileTool);
-        let fs = project::FakeFs::new(cx.executor());
-        fs.insert_tree("/home/user/myproject", json!({})).await;
-        let project = Project::test(fs.clone(), [path!("/home/user/myproject").as_ref()], cx).await;
-
-        // Get the actual local settings folder name
-        let local_settings_folder = paths::local_settings_folder_name();
-
-        // Test various config path patterns
-        let test_cases = vec![
-            (
-                format!("{local_settings_folder}/settings.json"),
-                true,
-                "Top-level local settings file".to_string(),
-            ),
-            (
-                format!("myproject/{local_settings_folder}/settings.json"),
-                true,
-                "Local settings in project path".to_string(),
-            ),
-            (
-                format!("src/{local_settings_folder}/config.toml"),
-                true,
-                "Local settings in subdirectory".to_string(),
-            ),
-            (
-                ".zed.backup/file.txt".to_string(),
-                true,
-                ".zed.backup is outside project".to_string(),
-            ),
-            (
-                "my.zed/file.txt".to_string(),
-                true,
-                "my.zed is outside project".to_string(),
-            ),
-            (
-                "myproject/src/file.zed".to_string(),
-                false,
-                ".zed as file extension".to_string(),
-            ),
-            (
-                "myproject/normal/path/file.rs".to_string(),
-                false,
-                "Normal file without config paths".to_string(),
-            ),
-        ];
-
-        for (path, should_confirm, description) in test_cases {
-            let input = json!({
-                "display_description": "Edit file",
-                "path": path,
-                "mode": "edit"
-            });
-            cx.update(|cx| {
-                assert_eq!(
-                    tool.needs_confirmation(&input, &project, cx),
-                    should_confirm,
-                    "Failed for case: {} - path: {}",
-                    description,
-                    path
-                );
-            });
-        }
-    }
-
-    #[gpui::test]
-    async fn test_needs_confirmation_global_config(cx: &mut TestAppContext) {
-        // Set up a custom data directory for testing
-        let temp_dir = tempfile::tempdir().unwrap();
-        init_test_with_config(cx, temp_dir.path());
-
-        let tool = Arc::new(EditFileTool);
-        let fs = project::FakeFs::new(cx.executor());
-
-        // Create test files in the global config directory
-        let global_config_dir = paths::config_dir();
-        fs::create_dir_all(&global_config_dir).unwrap();
-        let global_settings_path = global_config_dir.join("settings.json");
-        fs::write(&global_settings_path, "{}").unwrap();
-
-        fs.insert_tree("/project", json!({})).await;
-        let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
-
-        // Test global config paths
-        let test_cases = vec![
-            (
-                global_settings_path.to_str().unwrap().to_string(),
-                true,
-                "Global settings file should require confirmation",
-            ),
-            (
-                global_config_dir
-                    .join("keymap.json")
-                    .to_str()
-                    .unwrap()
-                    .to_string(),
-                true,
-                "Global keymap file should require confirmation",
-            ),
-            (
-                "project/normal_file.rs".to_string(),
-                false,
-                "Normal project file should not require confirmation",
-            ),
-        ];
-
-        for (path, should_confirm, description) in test_cases {
-            let input = json!({
-                "display_description": "Edit file",
-                "path": path,
-                "mode": "edit"
-            });
-            cx.update(|cx| {
-                assert_eq!(
-                    tool.needs_confirmation(&input, &project, cx),
-                    should_confirm,
-                    "Failed for case: {}",
-                    description
-                );
-            });
-        }
-    }
-
-    #[gpui::test]
-    async fn test_needs_confirmation_with_multiple_worktrees(cx: &mut TestAppContext) {
-        init_test(cx);
-        let tool = Arc::new(EditFileTool);
-        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;
-
-        // 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 input = json!({
-                "display_description": "Edit file",
-                "path": path,
-                "mode": "edit"
-            });
-            cx.update(|cx| {
-                assert_eq!(
-                    tool.needs_confirmation(&input, &project, cx),
-                    should_confirm,
-                    "Failed for case: {} - path: {}",
-                    description,
-                    path
-                );
-            });
-        }
-    }
-
-    #[gpui::test]
-    async fn test_needs_confirmation_edge_cases(cx: &mut TestAppContext) {
-        init_test(cx);
-        let tool = Arc::new(EditFileTool);
-        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;
-
-        // 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"),
-            ("project/../other", true, "Path with .. is outside project"),
-            (
-                "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 input = json!({
-                "display_description": "Edit file",
-                "path": path,
-                "mode": "edit"
-            });
-            cx.update(|cx| {
-                assert_eq!(
-                    tool.needs_confirmation(&input, &project, cx),
-                    should_confirm,
-                    "Failed for case: {} - path: {}",
-                    description,
-                    path
-                );
-            });
-        }
-    }
-
-    #[gpui::test]
-    async fn test_ui_text_with_all_path_types(cx: &mut TestAppContext) {
-        init_test(cx);
-        let tool = Arc::new(EditFileTool);
-
-        // Test UI text for various scenarios
-        let test_cases = vec![
-            (
-                json!({
-                    "display_description": "Update config",
-                    "path": ".zed/settings.json",
-                    "mode": "edit"
-                }),
-                "Update config (local settings)",
-                ".zed path should show local settings context",
-            ),
-            (
-                json!({
-                    "display_description": "Fix bug",
-                    "path": "src/.zed/local.json",
-                    "mode": "edit"
-                }),
-                "Fix bug (local settings)",
-                "Nested .zed path should show local settings context",
-            ),
-            (
-                json!({
-                    "display_description": "Update readme",
-                    "path": "README.md",
-                    "mode": "edit"
-                }),
-                "Update readme",
-                "Normal path should not show additional context",
-            ),
-            (
-                json!({
-                    "display_description": "Edit config",
-                    "path": "config.zed",
-                    "mode": "edit"
-                }),
-                "Edit config",
-                ".zed as extension should not show context",
-            ),
-        ];
-
-        for (input, expected_text, description) in test_cases {
-            cx.update(|_cx| {
-                let ui_text = tool.ui_text(&input);
-                assert_eq!(ui_text, expected_text, "Failed for case: {}", description);
-            });
-        }
-    }
-
-    #[gpui::test]
-    async fn test_needs_confirmation_with_different_modes(cx: &mut TestAppContext) {
-        init_test(cx);
-        let tool = Arc::new(EditFileTool);
-        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;
-
-        // Test different EditFileMode values
-        let modes = vec![
-            EditFileMode::Edit,
-            EditFileMode::Create,
-            EditFileMode::Overwrite,
-        ];
-
-        for mode in modes {
-            // Test .zed path with different modes
-            let input_zed = json!({
-                "display_description": "Edit settings",
-                "path": "project/.zed/settings.json",
-                "mode": mode
-            });
-            cx.update(|cx| {
-                assert!(
-                    tool.needs_confirmation(&input_zed, &project, cx),
-                    ".zed path should require confirmation regardless of mode: {:?}",
-                    mode
-                );
-            });
-
-            // Test outside path with different modes
-            let input_outside = json!({
-                "display_description": "Edit file",
-                "path": "/outside/file.txt",
-                "mode": mode
-            });
-            cx.update(|cx| {
-                assert!(
-                    tool.needs_confirmation(&input_outside, &project, cx),
-                    "Outside path should require confirmation regardless of mode: {:?}",
-                    mode
-                );
-            });
-
-            // Test normal path with different modes
-            let input_normal = json!({
-                "display_description": "Edit file",
-                "path": "project/normal.txt",
-                "mode": mode
-            });
-            cx.update(|cx| {
-                assert!(
-                    !tool.needs_confirmation(&input_normal, &project, cx),
-                    "Normal path should not require confirmation regardless of mode: {:?}",
-                    mode
-                );
-            });
-        }
-    }
-
-    #[gpui::test]
-    async fn test_always_allow_tool_actions_bypasses_all_checks(cx: &mut TestAppContext) {
-        // Set up with custom directories for deterministic testing
-        let temp_dir = tempfile::tempdir().unwrap();
-        init_test_with_config(cx, temp_dir.path());
-
-        let tool = Arc::new(EditFileTool);
-        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;
-
-        // Enable always_allow_tool_actions
-        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);
-        });
-
-        // Test that all paths that normally require confirmation are bypassed
-        let global_settings_path = paths::config_dir().join("settings.json");
-        fs::create_dir_all(paths::config_dir()).unwrap();
-        fs::write(&global_settings_path, "{}").unwrap();
-
-        let test_cases = vec![
-            ".zed/settings.json",
-            "project/.zed/config.toml",
-            global_settings_path.to_str().unwrap(),
-            "/etc/hosts",
-            "/absolute/path/file.txt",
-            "../outside/project.txt",
-        ];
-
-        for path in test_cases {
-            let input = json!({
-                "display_description": "Edit file",
-                "path": path,
-                "mode": "edit"
-            });
-            cx.update(|cx| {
-                assert!(
-                    !tool.needs_confirmation(&input, &project, cx),
-                    "Path {} should not require confirmation when always_allow_tool_actions is true",
-                    path
-                );
-            });
-        }
-
-        // Disable always_allow_tool_actions and verify confirmation is required again
-        cx.update(|cx| {
-            let mut settings = agent_settings::AgentSettings::get_global(cx).clone();
-            settings.always_allow_tool_actions = false;
-            agent_settings::AgentSettings::override_global(settings, cx);
-        });
-
-        // Verify .zed path requires confirmation again
-        let input = json!({
-            "display_description": "Edit file",
-            "path": ".zed/settings.json",
-            "mode": "edit"
-        });
-        cx.update(|cx| {
-            assert!(
-                tool.needs_confirmation(&input, &project, cx),
-                ".zed path should require confirmation when always_allow_tool_actions is false"
-            );
-        });
-    }
-}

crates/assistant_tools/src/edit_file_tool/description.md 🔗

@@ -1,8 +0,0 @@
-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

crates/assistant_tools/src/fetch_tool.rs 🔗

@@ -1,178 +0,0 @@
-use std::rc::Rc;
-use std::sync::Arc;
-use std::{borrow::Cow, cell::RefCell};
-
-use crate::schema::json_schema_for;
-use action_log::ActionLog;
-use anyhow::{Context as _, Result, anyhow, bail};
-use assistant_tool::{Tool, ToolResult};
-use futures::AsyncReadExt as _;
-use gpui::{AnyWindowHandle, App, AppContext as _, Entity, Task};
-use html_to_markdown::{TagHandler, convert_html_to_markdown, markdown};
-use http_client::{AsyncBody, HttpClientWithUrl};
-use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
-use project::Project;
-use schemars::JsonSchema;
-use serde::{Deserialize, Serialize};
-use ui::IconName;
-use util::markdown::MarkdownEscaped;
-
-#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
-enum ContentType {
-    Html,
-    Plaintext,
-    Json,
-}
-
-#[derive(Debug, Serialize, Deserialize, JsonSchema)]
-pub struct FetchToolInput {
-    /// The URL to fetch.
-    url: String,
-}
-
-pub struct FetchTool {
-    http_client: Arc<HttpClientWithUrl>,
-}
-
-impl FetchTool {
-    pub fn new(http_client: Arc<HttpClientWithUrl>) -> Self {
-        Self { http_client }
-    }
-
-    async fn build_message(http_client: Arc<HttpClientWithUrl>, url: &str) -> Result<String> {
-        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 = match content_type {
-            "text/html" | "application/xhtml+xml" => ContentType::Html,
-            "application/json" => ContentType::Json,
-            _ => ContentType::Plaintext,
-        };
-
-        match content_type {
-            ContentType::Html => {
-                let mut handlers: Vec<TagHandler> = 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 Tool for FetchTool {
-    fn name(&self) -> String {
-        "fetch".to_string()
-    }
-
-    fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
-        true
-    }
-
-    fn may_perform_edits(&self) -> bool {
-        false
-    }
-
-    fn description(&self) -> String {
-        include_str!("./fetch_tool/description.md").to_string()
-    }
-
-    fn icon(&self) -> IconName {
-        IconName::ToolWeb
-    }
-
-    fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
-        json_schema_for::<FetchToolInput>(format)
-    }
-
-    fn ui_text(&self, input: &serde_json::Value) -> String {
-        match serde_json::from_value::<FetchToolInput>(input.clone()) {
-            Ok(input) => format!("Fetch {}", MarkdownEscaped(&input.url)),
-            Err(_) => "Fetch URL".to_string(),
-        }
-    }
-
-    fn run(
-        self: Arc<Self>,
-        input: serde_json::Value,
-        _request: Arc<LanguageModelRequest>,
-        _project: Entity<Project>,
-        _action_log: Entity<ActionLog>,
-        _model: Arc<dyn LanguageModel>,
-        _window: Option<AnyWindowHandle>,
-        cx: &mut App,
-    ) -> ToolResult {
-        let input = match serde_json::from_value::<FetchToolInput>(input) {
-            Ok(input) => input,
-            Err(err) => return Task::ready(Err(anyhow!(err))).into(),
-        };
-
-        let text = cx.background_spawn({
-            let http_client = self.http_client.clone();
-            async move { 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.into())
-            })
-            .into()
-    }
-}

crates/assistant_tools/src/find_path_tool.rs 🔗

@@ -1,472 +0,0 @@
-use crate::{schema::json_schema_for, ui::ToolCallCardHeader};
-use action_log::ActionLog;
-use anyhow::{Result, anyhow};
-use assistant_tool::{
-    Tool, ToolCard, ToolResult, ToolResultContent, ToolResultOutput, ToolUseStatus,
-};
-use editor::Editor;
-use futures::channel::oneshot::{self, Receiver};
-use gpui::{
-    AnyWindowHandle, App, AppContext, Context, Entity, IntoElement, Task, WeakEntity, Window,
-};
-use language;
-use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
-use project::Project;
-use schemars::JsonSchema;
-use serde::{Deserialize, Serialize};
-use std::fmt::Write;
-use std::{cmp, path::PathBuf, sync::Arc};
-use ui::{Disclosure, Tooltip, prelude::*};
-use util::{ResultExt, paths::PathMatcher};
-use workspace::Workspace;
-
-#[derive(Debug, Serialize, Deserialize, JsonSchema)]
-pub struct FindPathToolInput {
-    /// The glob to match against every path in the project.
-    ///
-    /// <example>
-    /// 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"
-    /// </example>
-    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)]
-struct FindPathToolOutput {
-    glob: String,
-    paths: Vec<PathBuf>,
-}
-
-const RESULTS_PER_PAGE: usize = 50;
-
-pub struct FindPathTool;
-
-impl Tool for FindPathTool {
-    fn name(&self) -> String {
-        "find_path".into()
-    }
-
-    fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
-        false
-    }
-
-    fn may_perform_edits(&self) -> bool {
-        false
-    }
-
-    fn description(&self) -> String {
-        include_str!("./find_path_tool/description.md").into()
-    }
-
-    fn icon(&self) -> IconName {
-        IconName::ToolSearch
-    }
-
-    fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
-        json_schema_for::<FindPathToolInput>(format)
-    }
-
-    fn ui_text(&self, input: &serde_json::Value) -> String {
-        match serde_json::from_value::<FindPathToolInput>(input.clone()) {
-            Ok(input) => format!("Find paths matching “`{}`”", input.glob),
-            Err(_) => "Search paths".to_string(),
-        }
-    }
-
-    fn run(
-        self: Arc<Self>,
-        input: serde_json::Value,
-        _request: Arc<LanguageModelRequest>,
-        project: Entity<Project>,
-        _action_log: Entity<ActionLog>,
-        _model: Arc<dyn LanguageModel>,
-        _window: Option<AnyWindowHandle>,
-        cx: &mut App,
-    ) -> ToolResult {
-        let (offset, glob) = match serde_json::from_value::<FindPathToolInput>(input) {
-            Ok(input) => (input.offset, input.glob),
-            Err(err) => return Task::ready(Err(anyhow!(err))).into(),
-        };
-
-        let (sender, receiver) = oneshot::channel();
-
-        let card = cx.new(|cx| FindPathToolCard::new(glob.clone(), receiver, cx));
-
-        let search_paths_task = search_paths(&glob, project, cx);
-
-        let task = cx.background_spawn(async move {
-            let matches = search_paths_task.await?;
-            let paginated_matches: &[PathBuf] = &matches[cmp::min(offset, matches.len())
-                ..cmp::min(offset + RESULTS_PER_PAGE, matches.len())];
-
-            sender.send(paginated_matches.to_vec()).log_err();
-
-            if matches.is_empty() {
-                Ok("No matches found".to_string().into())
-            } else {
-                let mut message = format!("Found {} total matches.", matches.len());
-                if matches.len() > RESULTS_PER_PAGE {
-                    write!(
-                        &mut message,
-                        "\nShowing results {}-{} (provide 'offset' parameter for more results):",
-                        offset + 1,
-                        offset + paginated_matches.len()
-                    )
-                    .unwrap();
-                }
-
-                for mat in matches.iter().skip(offset).take(RESULTS_PER_PAGE) {
-                    write!(&mut message, "\n{}", mat.display()).unwrap();
-                }
-
-                let output = FindPathToolOutput {
-                    glob,
-                    paths: matches,
-                };
-
-                Ok(ToolResultOutput {
-                    content: ToolResultContent::Text(message),
-                    output: Some(serde_json::to_value(output)?),
-                })
-            }
-        });
-
-        ToolResult {
-            output: task,
-            card: Some(card.into()),
-        }
-    }
-
-    fn deserialize_card(
-        self: Arc<Self>,
-        output: serde_json::Value,
-        _project: Entity<Project>,
-        _window: &mut Window,
-        cx: &mut App,
-    ) -> Option<assistant_tool::AnyToolCard> {
-        let output = serde_json::from_value::<FindPathToolOutput>(output).ok()?;
-        let card = cx.new(|_| FindPathToolCard::from_output(output));
-        Some(card.into())
-    }
-}
-
-fn search_paths(glob: &str, project: Entity<Project>, cx: &mut App) -> Task<Result<Vec<PathBuf>>> {
-    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 },
-        ],
-        project.read(cx).path_style(cx),
-    ) {
-        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 {
-        Ok(snapshots
-            .iter()
-            .flat_map(|snapshot| {
-                snapshot
-                    .entries(false, 0)
-                    .map(move |entry| {
-                        snapshot
-                            .root_name()
-                            .join(&entry.path)
-                            .as_std_path()
-                            .to_path_buf()
-                    })
-                    .filter(|path| path_matcher.is_match(&path))
-            })
-            .collect())
-    })
-}
-
-struct FindPathToolCard {
-    paths: Vec<PathBuf>,
-    expanded: bool,
-    glob: String,
-    _receiver_task: Option<Task<Result<()>>>,
-}
-
-impl FindPathToolCard {
-    fn new(glob: String, receiver: Receiver<Vec<PathBuf>>, cx: &mut Context<Self>) -> Self {
-        let _receiver_task = cx.spawn(async move |this, cx| {
-            let paths = receiver.await?;
-
-            this.update(cx, |this, _cx| {
-                this.paths = paths;
-            })
-            .log_err();
-
-            Ok(())
-        });
-
-        Self {
-            paths: Vec::new(),
-            expanded: false,
-            glob,
-            _receiver_task: Some(_receiver_task),
-        }
-    }
-
-    fn from_output(output: FindPathToolOutput) -> Self {
-        Self {
-            glob: output.glob,
-            paths: output.paths,
-            expanded: false,
-            _receiver_task: None,
-        }
-    }
-}
-
-impl ToolCard for FindPathToolCard {
-    fn render(
-        &mut self,
-        _status: &ToolUseStatus,
-        _window: &mut Window,
-        workspace: WeakEntity<Workspace>,
-        cx: &mut Context<Self>,
-    ) -> impl IntoElement {
-        let matches_label: SharedString = if self.paths.is_empty() {
-            "No matches".into()
-        } else if self.paths.len() == 1 {
-            "1 match".into()
-        } else {
-            format!("{} matches", self.paths.len()).into()
-        };
-
-        let content = if !self.paths.is_empty() && self.expanded {
-            Some(
-                v_flex()
-                    .relative()
-                    .ml_1p5()
-                    .px_1p5()
-                    .gap_0p5()
-                    .border_l_1()
-                    .border_color(cx.theme().colors().border_variant)
-                    .children(self.paths.iter().enumerate().map(|(index, path)| {
-                        let path_clone = path.clone();
-                        let workspace_clone = workspace.clone();
-                        let button_label = path.to_string_lossy().into_owned();
-
-                        Button::new(("path", index), button_label)
-                            .icon(IconName::ArrowUpRight)
-                            .icon_size(IconSize::Small)
-                            .icon_position(IconPosition::End)
-                            .label_size(LabelSize::Small)
-                            .color(Color::Muted)
-                            .tooltip(Tooltip::text("Jump to File"))
-                            .on_click(move |_, window, cx| {
-                                workspace_clone
-                                    .update(cx, |workspace, cx| {
-                                        let path = PathBuf::from(&path_clone);
-                                        let Some(project_path) = workspace
-                                            .project()
-                                            .read(cx)
-                                            .find_project_path(&path, cx)
-                                        else {
-                                            return;
-                                        };
-                                        let open_task = workspace.open_path(
-                                            project_path,
-                                            None,
-                                            true,
-                                            window,
-                                            cx,
-                                        );
-                                        window
-                                            .spawn(cx, async move |cx| {
-                                                let item = open_task.await?;
-                                                if let Some(active_editor) =
-                                                    item.downcast::<Editor>()
-                                                {
-                                                    active_editor
-                                                        .update_in(cx, |editor, window, cx| {
-                                                            editor.go_to_singleton_buffer_point(
-                                                                language::Point::new(0, 0),
-                                                                window,
-                                                                cx,
-                                                            );
-                                                        })
-                                                        .log_err();
-                                                }
-                                                anyhow::Ok(())
-                                            })
-                                            .detach_and_log_err(cx);
-                                    })
-                                    .ok();
-                            })
-                    }))
-                    .into_any(),
-            )
-        } else {
-            None
-        };
-
-        v_flex()
-            .mb_2()
-            .gap_1()
-            .child(
-                ToolCallCardHeader::new(IconName::ToolSearch, matches_label)
-                    .with_code_path(&self.glob)
-                    .disclosure_slot(
-                        Disclosure::new("path-search-disclosure", self.expanded)
-                            .opened_icon(IconName::ChevronUp)
-                            .closed_icon(IconName::ChevronDown)
-                            .disabled(self.paths.is_empty())
-                            .on_click(cx.listener(move |this, _, _, _cx| {
-                                this.expanded = !this.expanded;
-                            })),
-                    ),
-            )
-            .children(content)
-    }
-}
-
-impl Component for FindPathTool {
-    fn scope() -> ComponentScope {
-        ComponentScope::Agent
-    }
-
-    fn sort_name() -> &'static str {
-        "FindPathTool"
-    }
-
-    fn preview(window: &mut Window, cx: &mut App) -> Option<AnyElement> {
-        let successful_card = cx.new(|_| FindPathToolCard {
-            paths: vec![
-                PathBuf::from("src/main.rs"),
-                PathBuf::from("src/lib.rs"),
-                PathBuf::from("tests/test.rs"),
-            ],
-            expanded: true,
-            glob: "*.rs".to_string(),
-            _receiver_task: None,
-        });
-
-        let empty_card = cx.new(|_| FindPathToolCard {
-            paths: Vec::new(),
-            expanded: false,
-            glob: "*.nonexistent".to_string(),
-            _receiver_task: None,
-        });
-
-        Some(
-            v_flex()
-                .gap_6()
-                .children(vec![example_group(vec![
-                    single_example(
-                        "With Paths",
-                        div()
-                            .size_full()
-                            .child(successful_card.update(cx, |tool, cx| {
-                                tool.render(
-                                    &ToolUseStatus::Finished("".into()),
-                                    window,
-                                    WeakEntity::new_invalid(),
-                                    cx,
-                                )
-                                .into_any_element()
-                            }))
-                            .into_any_element(),
-                    ),
-                    single_example(
-                        "No Paths",
-                        div()
-                            .size_full()
-                            .child(empty_card.update(cx, |tool, cx| {
-                                tool.render(
-                                    &ToolUseStatus::Finished("".into()),
-                                    window,
-                                    WeakEntity::new_invalid(),
-                                    cx,
-                                )
-                                .into_any_element()
-                            }))
-                            .into_any_element(),
-                    ),
-                ])])
-                .into_any_element(),
-        )
-    }
-}
-
-#[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);
-            language::init(cx);
-            Project::init_settings(cx);
-        });
-    }
-}

crates/assistant_tools/src/find_path_tool/description.md 🔗

@@ -1,7 +0,0 @@
-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.

crates/assistant_tools/src/grep_tool.rs 🔗

@@ -1,1308 +0,0 @@
-use crate::schema::json_schema_for;
-use action_log::ActionLog;
-use anyhow::{Result, anyhow};
-use assistant_tool::{Tool, ToolResult};
-use futures::StreamExt;
-use gpui::{AnyWindowHandle, App, Entity, Task};
-use language::{OffsetRangeExt, ParseStatus, Point};
-use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
-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 ui::IconName;
-use util::RangeExt;
-use util::markdown::MarkdownInlineCode;
-use util::paths::PathMatcher;
-
-#[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 "src/**/*.ts".
-    /// If omitted, all files in the project will be searched.
-    pub include_pattern: Option<String>,
-
-    /// 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;
-
-impl Tool for GrepTool {
-    fn name(&self) -> String {
-        "grep".into()
-    }
-
-    fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
-        false
-    }
-
-    fn may_perform_edits(&self) -> bool {
-        false
-    }
-
-    fn description(&self) -> String {
-        include_str!("./grep_tool/description.md").into()
-    }
-
-    fn icon(&self) -> IconName {
-        IconName::ToolRegex
-    }
-
-    fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
-        json_schema_for::<GrepToolInput>(format)
-    }
-
-    fn ui_text(&self, input: &serde_json::Value) -> String {
-        match serde_json::from_value::<GrepToolInput>(input.clone()) {
-            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".to_string(),
-        }
-    }
-
-    fn run(
-        self: Arc<Self>,
-        input: serde_json::Value,
-        _request: Arc<LanguageModelRequest>,
-        project: Entity<Project>,
-        _action_log: Entity<ActionLog>,
-        _model: Arc<dyn LanguageModel>,
-        _window: Option<AnyWindowHandle>,
-        cx: &mut App,
-    ) -> ToolResult {
-        const CONTEXT_LINES: u32 = 2;
-        const MAX_ANCESTOR_LINES: u32 = 10;
-
-        let input = match serde_json::from_value::<GrepToolInput>(input) {
-            Ok(input) => input,
-            Err(error) => {
-                return Task::ready(Err(anyhow!("Failed to parse input: {error}"))).into();
-            }
-        };
-
-        let include_matcher = match PathMatcher::new(
-            input
-                .include_pattern
-                .as_ref()
-                .into_iter()
-                .collect::<Vec<_>>(),
-            project.read(cx).path_style(cx),
-        ) {
-            Ok(matcher) => matcher,
-            Err(error) => {
-                return Task::ready(Err(anyhow!("invalid include glob pattern: {error}"))).into();
-            }
-        };
-
-        // 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()
-                .iter()
-                .chain(global_settings.private_files.sources().iter());
-
-            match PathMatcher::new(exclude_patterns, project.read(cx).path_style(cx)) {
-                Ok(matcher) => matcher,
-                Err(error) => {
-                    return Task::ready(Err(anyhow!("invalid exclude pattern: {error}"))).into();
-                }
-            }
-        };
-
-        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)).into(),
-        };
-
-        let results = project.update(cx, |project, cx| project.search(query, cx));
-
-        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".to_string().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,
-                ).into())
-            } else {
-                Ok(format!("Found {matches_found} matches:\n{output}").into())
-            }
-        }).into()
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use assistant_tool::Tool;
-    use gpui::{AppContext, TestAppContext, UpdateGlobal};
-    use language::{Language, LanguageConfig, LanguageMatcher};
-    use language_model::fake_provider::FakeLanguageModel;
-    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 = serde_json::to_value(GrepToolInput {
-            regex: "println".to_string(),
-            include_pattern: Some("root/**/*.rs".to_string()),
-            offset: 0,
-            case_sensitive: false,
-        })
-        .unwrap();
-
-        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 = serde_json::to_value(GrepToolInput {
-            regex: "fn".to_string(),
-            include_pattern: Some("root/**/src/**".to_string()),
-            offset: 0,
-            case_sensitive: false,
-        })
-        .unwrap();
-
-        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 = serde_json::to_value(GrepToolInput {
-            regex: "fn".to_string(),
-            include_pattern: None,
-            offset: 0,
-            case_sensitive: false,
-        })
-        .unwrap();
-
-        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 = serde_json::to_value(GrepToolInput {
-            regex: "uppercase".to_string(),
-            include_pattern: Some("**/*.txt".to_string()),
-            offset: 0,
-            case_sensitive: false,
-        })
-        .unwrap();
-
-        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 = serde_json::to_value(GrepToolInput {
-            regex: "uppercase".to_string(),
-            include_pattern: Some("**/*.txt".to_string()),
-            offset: 0,
-            case_sensitive: true,
-        })
-        .unwrap();
-
-        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 = serde_json::to_value(GrepToolInput {
-            regex: "LOWERCASE".to_string(),
-            include_pattern: Some("**/*.txt".to_string()),
-            offset: 0,
-            case_sensitive: true,
-        })
-        .unwrap();
-
-        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 = serde_json::to_value(GrepToolInput {
-            regex: "lowercase".to_string(),
-            include_pattern: Some("**/*.txt".to_string()),
-            offset: 0,
-            case_sensitive: true,
-        })
-        .unwrap();
-
-        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<Project> {
-        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(rust_lang().into())
-        });
-
-        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 = serde_json::to_value(GrepToolInput {
-            regex: "This is at the top level".to_string(),
-            include_pattern: Some("**/*.rs".to_string()),
-            offset: 0,
-            case_sensitive: false,
-        })
-        .unwrap();
-
-        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 = serde_json::to_value(GrepToolInput {
-            regex: "Function in nested module".to_string(),
-            include_pattern: Some("**/*.rs".to_string()),
-            offset: 0,
-            case_sensitive: false,
-        })
-        .unwrap();
-
-        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 = serde_json::to_value(GrepToolInput {
-            regex: "second_arg".to_string(),
-            include_pattern: Some("**/*.rs".to_string()),
-            offset: 0,
-            case_sensitive: false,
-        })
-        .unwrap();
-
-        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 = serde_json::to_value(GrepToolInput {
-            regex: "Inside if block".to_string(),
-            include_pattern: Some("**/*.rs".to_string()),
-            offset: 0,
-            case_sensitive: false,
-        })
-        .unwrap();
-
-        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 = serde_json::to_value(GrepToolInput {
-            regex: "Line 5".to_string(),
-            include_pattern: Some("**/*.rs".to_string()),
-            offset: 0,
-            case_sensitive: false,
-        })
-        .unwrap();
-
-        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 = serde_json::to_value(GrepToolInput {
-            regex: "Line 12".to_string(),
-            include_pattern: Some("**/*.rs".to_string()),
-            offset: 0,
-            case_sensitive: false,
-        })
-        .unwrap();
-
-        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: serde_json::Value,
-        project: Entity<Project>,
-        cx: &mut TestAppContext,
-    ) -> String {
-        let tool = Arc::new(GrepTool);
-        let action_log = cx.new(|_cx| ActionLog::new(project.clone()));
-        let model = Arc::new(FakeLanguageModel::default());
-        let task =
-            cx.update(|cx| tool.run(input, Arc::default(), project, action_log, model, None, cx));
-
-        match task.output.await {
-            Ok(result) => {
-                if cfg!(windows) {
-                    result.content.as_str().unwrap().replace("root\\", "root/")
-                } else {
-                    result.content.as_str().unwrap().to_string()
-                }
-            }
-            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);
-            language::init(cx);
-            Project::init_settings(cx);
-        });
-    }
-
-    fn rust_lang() -> Language {
-        Language::new(
-            LanguageConfig {
-                name: "Rust".into(),
-                matcher: LanguageMatcher {
-                    path_suffixes: vec!["rs".to_string()],
-                    ..Default::default()
-                },
-                ..Default::default()
-            },
-            Some(tree_sitter_rust::LANGUAGE.into()),
-        )
-        .with_outline_query(include_str!("../../languages/src/rust/outline.scm"))
-        .unwrap()
-    }
-
-    #[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;
-        let action_log = cx.new(|_| ActionLog::new(project.clone()));
-        let model = Arc::new(FakeLanguageModel::default());
-
-        // Searching for files outside the project worktree should return no results
-        let result = cx
-            .update(|cx| {
-                let input = json!({
-                    "regex": "outside_function"
-                });
-                Arc::new(GrepTool)
-                    .run(
-                        input,
-                        Arc::default(),
-                        project.clone(),
-                        action_log.clone(),
-                        model.clone(),
-                        None,
-                        cx,
-                    )
-                    .output
-            })
-            .await;
-        let results = result.unwrap();
-        let paths = extract_paths_from_results(results.content.as_str().unwrap());
-        assert!(
-            paths.is_empty(),
-            "grep_tool should not find files outside the project worktree"
-        );
-
-        // Searching within the project should succeed
-        let result = cx
-            .update(|cx| {
-                let input = json!({
-                    "regex": "main"
-                });
-                Arc::new(GrepTool)
-                    .run(
-                        input,
-                        Arc::default(),
-                        project.clone(),
-                        action_log.clone(),
-                        model.clone(),
-                        None,
-                        cx,
-                    )
-                    .output
-            })
-            .await;
-        let results = result.unwrap();
-        let paths = extract_paths_from_results(results.content.as_str().unwrap());
-        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 = cx
-            .update(|cx| {
-                let input = json!({
-                    "regex": "special_configuration"
-                });
-                Arc::new(GrepTool)
-                    .run(
-                        input,
-                        Arc::default(),
-                        project.clone(),
-                        action_log.clone(),
-                        model.clone(),
-                        None,
-                        cx,
-                    )
-                    .output
-            })
-            .await;
-        let results = result.unwrap();
-        let paths = extract_paths_from_results(results.content.as_str().unwrap());
-        assert!(
-            paths.is_empty(),
-            "grep_tool should not search files in .secretdir (file_scan_exclusions)"
-        );
-
-        let result = cx
-            .update(|cx| {
-                let input = json!({
-                    "regex": "custom_metadata"
-                });
-                Arc::new(GrepTool)
-                    .run(
-                        input,
-                        Arc::default(),
-                        project.clone(),
-                        action_log.clone(),
-                        model.clone(),
-                        None,
-                        cx,
-                    )
-                    .output
-            })
-            .await;
-        let results = result.unwrap();
-        let paths = extract_paths_from_results(results.content.as_str().unwrap());
-        assert!(
-            paths.is_empty(),
-            "grep_tool should not search .mymetadata files (file_scan_exclusions)"
-        );
-
-        // Searching private files should return no results
-        let result = cx
-            .update(|cx| {
-                let input = json!({
-                    "regex": "SECRET_KEY"
-                });
-                Arc::new(GrepTool)
-                    .run(
-                        input,
-                        Arc::default(),
-                        project.clone(),
-                        action_log.clone(),
-                        model.clone(),
-                        None,
-                        cx,
-                    )
-                    .output
-            })
-            .await;
-        let results = result.unwrap();
-        let paths = extract_paths_from_results(results.content.as_str().unwrap());
-        assert!(
-            paths.is_empty(),
-            "grep_tool should not search .mysecrets (private_files)"
-        );
-
-        let result = cx
-            .update(|cx| {
-                let input = json!({
-                    "regex": "private_key_content"
-                });
-                Arc::new(GrepTool)
-                    .run(
-                        input,
-                        Arc::default(),
-                        project.clone(),
-                        action_log.clone(),
-                        model.clone(),
-                        None,
-                        cx,
-                    )
-                    .output
-            })
-            .await;
-        let results = result.unwrap();
-        let paths = extract_paths_from_results(results.content.as_str().unwrap());
-        assert!(
-            paths.is_empty(),
-            "grep_tool should not search .privatekey files (private_files)"
-        );
-
-        let result = cx
-            .update(|cx| {
-                let input = json!({
-                    "regex": "sensitive_data"
-                });
-                Arc::new(GrepTool)
-                    .run(
-                        input,
-                        Arc::default(),
-                        project.clone(),
-                        action_log.clone(),
-                        model.clone(),
-                        None,
-                        cx,
-                    )
-                    .output
-            })
-            .await;
-        let results = result.unwrap();
-        let paths = extract_paths_from_results(results.content.as_str().unwrap());
-        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 = cx
-            .update(|cx| {
-                let input = json!({
-                    "regex": "normal_file_content"
-                });
-                Arc::new(GrepTool)
-                    .run(
-                        input,
-                        Arc::default(),
-                        project.clone(),
-                        action_log.clone(),
-                        model.clone(),
-                        None,
-                        cx,
-                    )
-                    .output
-            })
-            .await;
-        let results = result.unwrap();
-        let paths = extract_paths_from_results(results.content.as_str().unwrap());
-        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 = cx
-            .update(|cx| {
-                let input = json!({
-                    "regex": "outside_function",
-                    "include_pattern": "../outside_project/**/*.rs"
-                });
-                Arc::new(GrepTool)
-                    .run(
-                        input,
-                        Arc::default(),
-                        project.clone(),
-                        action_log.clone(),
-                        model.clone(),
-                        None,
-                        cx,
-                    )
-                    .output
-            })
-            .await;
-        let results = result.unwrap();
-        let paths = extract_paths_from_results(results.content.as_str().unwrap());
-        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();
-
-        let action_log = cx.new(|_| ActionLog::new(project.clone()));
-        let model = Arc::new(FakeLanguageModel::default());
-
-        // Search for "secret" - should exclude files based on worktree-specific settings
-        let result = cx
-            .update(|cx| {
-                let input = json!({
-                    "regex": "secret",
-                    "case_sensitive": false
-                });
-                Arc::new(GrepTool)
-                    .run(
-                        input,
-                        Arc::default(),
-                        project.clone(),
-                        action_log.clone(),
-                        model.clone(),
-                        None,
-                        cx,
-                    )
-                    .output
-            })
-            .await
-            .unwrap();
-
-        let content = result.content.as_str().unwrap();
-        let paths = extract_paths_from_results(content);
-
-        // 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 = cx
-            .update(|cx| {
-                let input = json!({
-                    "regex": "secret",
-                    "include_pattern": "worktree1/**/*.rs"
-                });
-                Arc::new(GrepTool)
-                    .run(
-                        input,
-                        Arc::default(),
-                        project.clone(),
-                        action_log.clone(),
-                        model.clone(),
-                        None,
-                        cx,
-                    )
-                    .output
-            })
-            .await
-            .unwrap();
-
-        let content = result.content.as_str().unwrap();
-        let paths = extract_paths_from_results(content);
-
-        // 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<String> {
-        results
-            .lines()
-            .filter(|line| line.starts_with("## Matches in "))
-            .map(|line| {
-                line.strip_prefix("## Matches in ")
-                    .unwrap()
-                    .trim()
-                    .to_string()
-            })
-            .collect()
-    }
-}

crates/assistant_tools/src/grep_tool/description.md 🔗

@@ -1,9 +0,0 @@
-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.

crates/assistant_tools/src/list_directory_tool.rs 🔗

@@ -1,869 +0,0 @@
-use crate::schema::json_schema_for;
-use action_log::ActionLog;
-use anyhow::{Result, anyhow};
-use assistant_tool::{Tool, ToolResult};
-use gpui::{AnyWindowHandle, App, Entity, Task};
-use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
-use project::{Project, ProjectPath, WorktreeSettings};
-use schemars::JsonSchema;
-use serde::{Deserialize, Serialize};
-use settings::Settings;
-use std::{fmt::Write, sync::Arc};
-use ui::IconName;
-use util::markdown::MarkdownInlineCode;
-
-#[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.
-    ///
-    /// <example>
-    /// If the project has the following root directories:
-    ///
-    /// - directory1
-    /// - directory2
-    ///
-    /// You can list the contents of `directory1` by using the path `directory1`.
-    /// </example>
-    ///
-    /// <example>
-    /// 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`.
-    /// </example>
-    pub path: String,
-}
-
-pub struct ListDirectoryTool;
-
-impl Tool for ListDirectoryTool {
-    fn name(&self) -> String {
-        "list_directory".into()
-    }
-
-    fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
-        false
-    }
-
-    fn may_perform_edits(&self) -> bool {
-        false
-    }
-
-    fn description(&self) -> String {
-        include_str!("./list_directory_tool/description.md").into()
-    }
-
-    fn icon(&self) -> IconName {
-        IconName::ToolFolder
-    }
-
-    fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
-        json_schema_for::<ListDirectoryToolInput>(format)
-    }
-
-    fn ui_text(&self, input: &serde_json::Value) -> String {
-        match serde_json::from_value::<ListDirectoryToolInput>(input.clone()) {
-            Ok(input) => {
-                let path = MarkdownInlineCode(&input.path);
-                format!("List the {path} directory's contents")
-            }
-            Err(_) => "List directory".to_string(),
-        }
-    }
-
-    fn run(
-        self: Arc<Self>,
-        input: serde_json::Value,
-        _request: Arc<LanguageModelRequest>,
-        project: Entity<Project>,
-        _action_log: Entity<ActionLog>,
-        _model: Arc<dyn LanguageModel>,
-        _window: Option<AnyWindowHandle>,
-        cx: &mut App,
-    ) -> ToolResult {
-        let path_style = project.read(cx).path_style(cx);
-        let input = match serde_json::from_value::<ListDirectoryToolInput>(input) {
-            Ok(input) => input,
-            Err(err) => return Task::ready(Err(anyhow!(err))).into(),
-        };
-
-        // 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 = project
-                .read(cx)
-                .worktrees(cx)
-                .filter_map(|worktree| {
-                    worktree.read(cx).root_entry().and_then(|entry| {
-                        if entry.is_dir() {
-                            Some(entry.path.display(path_style))
-                        } else {
-                            None
-                        }
-                    })
-                })
-                .collect::<Vec<_>>()
-                .join("\n");
-
-            return Task::ready(Ok(output.into())).into();
-        }
-
-        let Some(project_path) = project.read(cx).find_project_path(&input.path, cx) else {
-            return Task::ready(Err(anyhow!("Path {} not found in project", input.path))).into();
-        };
-        let Some(worktree) = project
-            .read(cx)
-            .worktree_for_id(project_path.worktree_id, cx)
-        else {
-            return Task::ready(Err(anyhow!("Worktree not found"))).into();
-        };
-
-        // 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
-            )))
-            .into();
-        }
-
-        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
-            )))
-            .into();
-        }
-
-        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
-            )))
-            .into();
-        }
-
-        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
-            )))
-            .into();
-        }
-
-        let worktree_snapshot = worktree.read(cx).snapshot();
-
-        let Some(entry) = worktree_snapshot.entry_for_path(&project_path.path) else {
-            return Task::ready(Err(anyhow!("Path not found: {}", input.path))).into();
-        };
-
-        if !entry.is_dir() {
-            return Task::ready(Err(anyhow!("{} is not a directory.", input.path))).into();
-        }
-        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_id: worktree_snapshot.id(),
-                path: entry.path.clone(),
-            };
-            let worktree_settings = WorktreeSettings::get(Some((&project_path).into()), cx);
-
-            if worktree_settings.is_path_excluded(&project_path.path)
-                || worktree_settings.is_path_private(&project_path.path)
-            {
-                continue;
-            }
-
-            let full_path = worktree_snapshot
-                .root_name()
-                .join(&entry.path)
-                .display(worktree_snapshot.path_style())
-                .to_string();
-            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.into())).into()
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use assistant_tool::Tool;
-    use gpui::{AppContext, TestAppContext, UpdateGlobal};
-    use indoc::indoc;
-    use language_model::fake_provider::FakeLanguageModel;
-    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);
-            language::init(cx);
-            Project::init_settings(cx);
-        });
-    }
-
-    #[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 action_log = cx.new(|_| ActionLog::new(project.clone()));
-        let model = Arc::new(FakeLanguageModel::default());
-        let tool = Arc::new(ListDirectoryTool);
-
-        // Test listing root directory
-        let input = json!({
-            "path": "project"
-        });
-
-        let result = cx
-            .update(|cx| {
-                tool.clone().run(
-                    input,
-                    Arc::default(),
-                    project.clone(),
-                    action_log.clone(),
-                    model.clone(),
-                    None,
-                    cx,
-                )
-            })
-            .output
-            .await
-            .unwrap();
-
-        let content = result.content.as_str().unwrap();
-        assert_eq!(
-            content,
-            platform_paths(indoc! {"
-                # Folders:
-                project/src
-                project/tests
-
-                # Files:
-                project/Cargo.toml
-                project/README.md
-            "})
-        );
-
-        // Test listing src directory
-        let input = json!({
-            "path": "project/src"
-        });
-
-        let result = cx
-            .update(|cx| {
-                tool.clone().run(
-                    input,
-                    Arc::default(),
-                    project.clone(),
-                    action_log.clone(),
-                    model.clone(),
-                    None,
-                    cx,
-                )
-            })
-            .output
-            .await
-            .unwrap();
-
-        let content = result.content.as_str().unwrap();
-        assert_eq!(
-            content,
-            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 = json!({
-            "path": "project/tests"
-        });
-
-        let result = cx
-            .update(|cx| {
-                tool.clone().run(
-                    input,
-                    Arc::default(),
-                    project.clone(),
-                    action_log.clone(),
-                    model.clone(),
-                    None,
-                    cx,
-                )
-            })
-            .output
-            .await
-            .unwrap();
-
-        let content = result.content.as_str().unwrap();
-        assert!(!content.contains("# Folders:"));
-        assert!(content.contains("# Files:"));
-        assert!(content.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 action_log = cx.new(|_| ActionLog::new(project.clone()));
-        let model = Arc::new(FakeLanguageModel::default());
-        let tool = Arc::new(ListDirectoryTool);
-
-        let input = json!({
-            "path": "project/empty_dir"
-        });
-
-        let result = cx
-            .update(|cx| tool.run(input, Arc::default(), project, action_log, model, None, cx))
-            .output
-            .await
-            .unwrap();
-
-        let content = result.content.as_str().unwrap();
-        assert_eq!(content, "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 action_log = cx.new(|_| ActionLog::new(project.clone()));
-        let model = Arc::new(FakeLanguageModel::default());
-        let tool = Arc::new(ListDirectoryTool);
-
-        // Test non-existent path
-        let input = json!({
-            "path": "project/nonexistent"
-        });
-
-        let result = cx
-            .update(|cx| {
-                tool.clone().run(
-                    input,
-                    Arc::default(),
-                    project.clone(),
-                    action_log.clone(),
-                    model.clone(),
-                    None,
-                    cx,
-                )
-            })
-            .output
-            .await;
-
-        assert!(result.is_err());
-        assert!(result.unwrap_err().to_string().contains("Path not found"));
-
-        // Test trying to list a file instead of directory
-        let input = json!({
-            "path": "project/file.txt"
-        });
-
-        let result = cx
-            .update(|cx| tool.run(input, Arc::default(), project, action_log, model, None, cx))
-            .output
-            .await;
-
-        assert!(result.is_err());
-        assert!(
-            result
-                .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 action_log = cx.new(|_| ActionLog::new(project.clone()));
-        let model = Arc::new(FakeLanguageModel::default());
-        let tool = Arc::new(ListDirectoryTool);
-
-        // Listing root directory should exclude private and excluded files
-        let input = json!({
-            "path": "project"
-        });
-
-        let result = cx
-            .update(|cx| {
-                tool.clone().run(
-                    input,
-                    Arc::default(),
-                    project.clone(),
-                    action_log.clone(),
-                    model.clone(),
-                    None,
-                    cx,
-                )
-            })
-            .output
-            .await
-            .unwrap();
-
-        let content = result.content.as_str().unwrap();
-
-        // Should include normal directories
-        assert!(content.contains("normal_dir"), "Should list normal_dir");
-        assert!(content.contains("visible_dir"), "Should list visible_dir");
-
-        // Should NOT include excluded or private files
-        assert!(
-            !content.contains(".secretdir"),
-            "Should not list .secretdir (file_scan_exclusions)"
-        );
-        assert!(
-            !content.contains(".mymetadata"),
-            "Should not list .mymetadata (file_scan_exclusions)"
-        );
-        assert!(
-            !content.contains(".mysecrets"),
-            "Should not list .mysecrets (private_files)"
-        );
-
-        // Trying to list an excluded directory should fail
-        let input = json!({
-            "path": "project/.secretdir"
-        });
-
-        let result = cx
-            .update(|cx| {
-                tool.clone().run(
-                    input,
-                    Arc::default(),
-                    project.clone(),
-                    action_log.clone(),
-                    model.clone(),
-                    None,
-                    cx,
-                )
-            })
-            .output
-            .await;
-
-        assert!(
-            result.is_err(),
-            "Should not be able to list excluded directory"
-        );
-        assert!(
-            result
-                .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 = json!({
-            "path": "project/visible_dir"
-        });
-
-        let result = cx
-            .update(|cx| {
-                tool.clone().run(
-                    input,
-                    Arc::default(),
-                    project.clone(),
-                    action_log.clone(),
-                    model.clone(),
-                    None,
-                    cx,
-                )
-            })
-            .output
-            .await
-            .unwrap();
-
-        let content = result.content.as_str().unwrap();
-
-        // Should include normal files
-        assert!(content.contains("normal.txt"), "Should list normal.txt");
-
-        // Should NOT include private files
-        assert!(
-            !content.contains("privatekey"),
-            "Should not list .privatekey files (private_files)"
-        );
-        assert!(
-            !content.contains("mysensitive"),
-            "Should not list .mysensitive files (private_files)"
-        );
-
-        // Should NOT include subdirectories that match exclusions
-        assert!(
-            !content.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 action_log = cx.new(|_| ActionLog::new(project.clone()));
-        let model = Arc::new(FakeLanguageModel::default());
-        let tool = Arc::new(ListDirectoryTool);
-
-        // Test listing worktree1/src - should exclude secret.rs and config.toml based on local settings
-        let input = json!({
-            "path": "worktree1/src"
-        });
-
-        let result = cx
-            .update(|cx| {
-                tool.clone().run(
-                    input,
-                    Arc::default(),
-                    project.clone(),
-                    action_log.clone(),
-                    model.clone(),
-                    None,
-                    cx,
-                )
-            })
-            .output
-            .await
-            .unwrap();
-
-        let content = result.content.as_str().unwrap();
-        assert!(content.contains("main.rs"), "Should list main.rs");
-        assert!(
-            !content.contains("secret.rs"),
-            "Should not list secret.rs (local private_files)"
-        );
-        assert!(
-            !content.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 = json!({
-            "path": "worktree1/tests"
-        });
-
-        let result = cx
-            .update(|cx| {
-                tool.clone().run(
-                    input,
-                    Arc::default(),
-                    project.clone(),
-                    action_log.clone(),
-                    model.clone(),
-                    None,
-                    cx,
-                )
-            })
-            .output
-            .await
-            .unwrap();
-
-        let content = result.content.as_str().unwrap();
-        assert!(content.contains("test.rs"), "Should list test.rs");
-        assert!(
-            !content.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 = json!({
-            "path": "worktree2/lib"
-        });
-
-        let result = cx
-            .update(|cx| {
-                tool.clone().run(
-                    input,
-                    Arc::default(),
-                    project.clone(),
-                    action_log.clone(),
-                    model.clone(),
-                    None,
-                    cx,
-                )
-            })
-            .output
-            .await
-            .unwrap();
-
-        let content = result.content.as_str().unwrap();
-        assert!(content.contains("public.js"), "Should list public.js");
-        assert!(
-            !content.contains("private.js"),
-            "Should not list private.js (local private_files)"
-        );
-        assert!(
-            !content.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 = json!({
-            "path": "worktree2/docs"
-        });
-
-        let result = cx
-            .update(|cx| {
-                tool.clone().run(
-                    input,
-                    Arc::default(),
-                    project.clone(),
-                    action_log.clone(),
-                    model.clone(),
-                    None,
-                    cx,
-                )
-            })
-            .output
-            .await
-            .unwrap();
-
-        let content = result.content.as_str().unwrap();
-        assert!(content.contains("README.md"), "Should list README.md");
-        assert!(
-            !content.contains("internal.md"),
-            "Should not list internal.md (local file_scan_exclusions)"
-        );
-
-        // Test trying to list an excluded directory directly
-        let input = json!({
-            "path": "worktree1/src/secret.rs"
-        });
-
-        let result = cx
-            .update(|cx| {
-                tool.clone().run(
-                    input,
-                    Arc::default(),
-                    project.clone(),
-                    action_log.clone(),
-                    model.clone(),
-                    None,
-                    cx,
-                )
-            })
-            .output
-            .await;
-
-        // This should fail because we're trying to list a file, not a directory
-        assert!(result.is_err(), "Should fail when trying to list a file");
-    }
-}

crates/assistant_tools/src/move_path_tool.rs 🔗

@@ -1,132 +0,0 @@
-use crate::schema::json_schema_for;
-use action_log::ActionLog;
-use anyhow::{Context as _, Result, anyhow};
-use assistant_tool::{Tool, ToolResult};
-use gpui::{AnyWindowHandle, App, AppContext, Entity, Task};
-use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
-use project::Project;
-use schemars::JsonSchema;
-use serde::{Deserialize, Serialize};
-use std::{path::Path, sync::Arc};
-use ui::IconName;
-use util::markdown::MarkdownInlineCode;
-
-#[derive(Debug, Serialize, Deserialize, JsonSchema)]
-pub struct MovePathToolInput {
-    /// The source path of the file or directory to move/rename.
-    ///
-    /// <example>
-    /// 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"
-    /// </example>
-    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.
-    ///
-    /// <example>
-    /// To move "directory1/a/something.txt" to "directory2/b/renamed.txt",
-    /// provide a destination_path of "directory2/b/renamed.txt"
-    /// </example>
-    pub destination_path: String,
-}
-
-pub struct MovePathTool;
-
-impl Tool for MovePathTool {
-    fn name(&self) -> String {
-        "move_path".into()
-    }
-
-    fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
-        false
-    }
-
-    fn may_perform_edits(&self) -> bool {
-        true
-    }
-
-    fn description(&self) -> String {
-        include_str!("./move_path_tool/description.md").into()
-    }
-
-    fn icon(&self) -> IconName {
-        IconName::ArrowRightLeft
-    }
-
-    fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
-        json_schema_for::<MovePathToolInput>(format)
-    }
-
-    fn ui_text(&self, input: &serde_json::Value) -> String {
-        match serde_json::from_value::<MovePathToolInput>(input.clone()) {
-            Ok(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}")
-                    }
-                    _ => {
-                        format!("Move {src} to {dest}")
-                    }
-                }
-            }
-            Err(_) => "Move path".to_string(),
-        }
-    }
-
-    fn run(
-        self: Arc<Self>,
-        input: serde_json::Value,
-        _request: Arc<LanguageModelRequest>,
-        project: Entity<Project>,
-        _action_log: Entity<ActionLog>,
-        _model: Arc<dyn LanguageModel>,
-        _window: Option<AnyWindowHandle>,
-        cx: &mut App,
-    ) -> ToolResult {
-        let input = match serde_json::from_value::<MovePathToolInput>(input) {
-            Ok(input) => input,
-            Err(err) => return Task::ready(Err(anyhow!(err))).into(),
-        };
-        let rename_task = 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).into())
-        })
-        .into()
-    }
-}

crates/assistant_tools/src/move_path_tool/description.md 🔗

@@ -1,5 +0,0 @@
-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.

crates/assistant_tools/src/now_tool.rs 🔗

@@ -1,84 +0,0 @@
-use std::sync::Arc;
-
-use crate::schema::json_schema_for;
-use action_log::ActionLog;
-use anyhow::{Result, anyhow};
-use assistant_tool::{Tool, ToolResult};
-use chrono::{Local, Utc};
-use gpui::{AnyWindowHandle, App, Entity, Task};
-use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
-use project::Project;
-use schemars::JsonSchema;
-use serde::{Deserialize, Serialize};
-use ui::IconName;
-
-#[derive(Debug, Serialize, Deserialize, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum Timezone {
-    /// Use UTC for the datetime.
-    Utc,
-    /// Use local time for the datetime.
-    Local,
-}
-
-#[derive(Debug, Serialize, Deserialize, JsonSchema)]
-pub struct NowToolInput {
-    /// The timezone to use for the datetime.
-    timezone: Timezone,
-}
-
-pub struct NowTool;
-
-impl Tool for NowTool {
-    fn name(&self) -> String {
-        "now".into()
-    }
-
-    fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
-        false
-    }
-
-    fn may_perform_edits(&self) -> bool {
-        false
-    }
-
-    fn description(&self) -> String {
-        "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.".into()
-    }
-
-    fn icon(&self) -> IconName {
-        IconName::Info
-    }
-
-    fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
-        json_schema_for::<NowToolInput>(format)
-    }
-
-    fn ui_text(&self, _input: &serde_json::Value) -> String {
-        "Get current time".to_string()
-    }
-
-    fn run(
-        self: Arc<Self>,
-        input: serde_json::Value,
-        _request: Arc<LanguageModelRequest>,
-        _project: Entity<Project>,
-        _action_log: Entity<ActionLog>,
-        _model: Arc<dyn LanguageModel>,
-        _window: Option<AnyWindowHandle>,
-        _cx: &mut App,
-    ) -> ToolResult {
-        let input: NowToolInput = match serde_json::from_value(input) {
-            Ok(input) => input,
-            Err(err) => return Task::ready(Err(anyhow!(err))).into(),
-        };
-
-        let now = match input.timezone {
-            Timezone::Utc => Utc::now().to_rfc3339(),
-            Timezone::Local => Local::now().to_rfc3339(),
-        };
-        let text = format!("The current datetime is {now}.");
-
-        Task::ready(Ok(text.into())).into()
-    }
-}

crates/assistant_tools/src/open_tool.rs 🔗

@@ -1,170 +0,0 @@
-use crate::schema::json_schema_for;
-use action_log::ActionLog;
-use anyhow::{Context as _, Result, anyhow};
-use assistant_tool::{Tool, ToolResult};
-use gpui::{AnyWindowHandle, App, AppContext, Entity, Task};
-use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
-use project::Project;
-use schemars::JsonSchema;
-use serde::{Deserialize, Serialize};
-use std::{path::PathBuf, sync::Arc};
-use ui::IconName;
-use util::markdown::MarkdownEscaped;
-
-#[derive(Debug, Serialize, Deserialize, JsonSchema)]
-pub struct OpenToolInput {
-    /// The path or URL to open with the default application.
-    path_or_url: String,
-}
-
-pub struct OpenTool;
-
-impl Tool for OpenTool {
-    fn name(&self) -> String {
-        "open".to_string()
-    }
-
-    fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
-        true
-    }
-    fn may_perform_edits(&self) -> bool {
-        false
-    }
-    fn description(&self) -> String {
-        include_str!("./open_tool/description.md").to_string()
-    }
-
-    fn icon(&self) -> IconName {
-        IconName::ArrowUpRight
-    }
-
-    fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
-        json_schema_for::<OpenToolInput>(format)
-    }
-
-    fn ui_text(&self, input: &serde_json::Value) -> String {
-        match serde_json::from_value::<OpenToolInput>(input.clone()) {
-            Ok(input) => format!("Open `{}`", MarkdownEscaped(&input.path_or_url)),
-            Err(_) => "Open file or URL".to_string(),
-        }
-    }
-
-    fn run(
-        self: Arc<Self>,
-        input: serde_json::Value,
-        _request: Arc<LanguageModelRequest>,
-        project: Entity<Project>,
-        _action_log: Entity<ActionLog>,
-        _model: Arc<dyn LanguageModel>,
-        _window: Option<AnyWindowHandle>,
-        cx: &mut App,
-    ) -> ToolResult {
-        let input: OpenToolInput = match serde_json::from_value(input) {
-            Ok(input) => input,
-            Err(err) => return Task::ready(Err(anyhow!(err))).into(),
-        };
-
-        // 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, project, cx);
-
-        cx.background_spawn(async move {
-            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).into())
-        })
-        .into()
-    }
-}
-
-fn to_absolute_path(
-    potential_path: &str,
-    project: Entity<Project>,
-    cx: &mut App,
-) -> Option<PathBuf> {
-    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);
-            language::init(cx);
-            Project::init_settings(cx);
-        });
-    }
-}

crates/assistant_tools/src/open_tool/description.md 🔗

@@ -1,9 +0,0 @@
-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.

crates/assistant_tools/src/project_notifications_tool.rs 🔗

@@ -1,360 +0,0 @@
-use crate::schema::json_schema_for;
-use action_log::ActionLog;
-use anyhow::Result;
-use assistant_tool::{Tool, ToolResult};
-use gpui::{AnyWindowHandle, App, Entity, Task};
-use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
-use project::Project;
-use schemars::JsonSchema;
-use serde::{Deserialize, Serialize};
-use std::{fmt::Write, sync::Arc};
-use ui::IconName;
-
-#[derive(Debug, Serialize, Deserialize, JsonSchema)]
-pub struct ProjectUpdatesToolInput {}
-
-pub struct ProjectNotificationsTool;
-
-impl Tool for ProjectNotificationsTool {
-    fn name(&self) -> String {
-        "project_notifications".to_string()
-    }
-
-    fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
-        false
-    }
-    fn may_perform_edits(&self) -> bool {
-        false
-    }
-    fn description(&self) -> String {
-        include_str!("./project_notifications_tool/description.md").to_string()
-    }
-
-    fn icon(&self) -> IconName {
-        IconName::ToolNotification
-    }
-
-    fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
-        json_schema_for::<ProjectUpdatesToolInput>(format)
-    }
-
-    fn ui_text(&self, _input: &serde_json::Value) -> String {
-        "Check project notifications".into()
-    }
-
-    fn run(
-        self: Arc<Self>,
-        _input: serde_json::Value,
-        _request: Arc<LanguageModelRequest>,
-        _project: Entity<Project>,
-        action_log: Entity<ActionLog>,
-        _model: Arc<dyn LanguageModel>,
-        _window: Option<AnyWindowHandle>,
-        cx: &mut App,
-    ) -> ToolResult {
-        let Some(user_edits_diff) =
-            action_log.update(cx, |log, cx| log.flush_unnotified_user_edits(cx))
-        else {
-            return result("No new notifications");
-        };
-
-        // NOTE: Changes to this prompt require a symmetric update in the LLM Worker
-        const HEADER: &str = include_str!("./project_notifications_tool/prompt_header.txt");
-        const MAX_BYTES: usize = 8000;
-        let diff = fit_patch_to_size(&user_edits_diff, MAX_BYTES);
-        result(&format!("{HEADER}\n\n```diff\n{diff}\n```\n").replace("\r\n", "\n"))
-    }
-}
-
-fn result(response: &str) -> ToolResult {
-    Task::ready(Ok(response.to_string().into())).into()
-}
-
-/// Make sure that the patch fits into the size limit (in bytes).
-/// Compress the patch by omitting some parts if needed.
-/// Unified diff format is assumed.
-fn fit_patch_to_size(patch: &str, max_size: usize) -> String {
-    if patch.len() <= max_size {
-        return patch.to_string();
-    }
-
-    // Compression level 1: remove context lines in diff bodies, but
-    // leave the counts and positions of inserted/deleted lines
-    let mut current_size = patch.len();
-    let mut file_patches = split_patch(patch);
-    file_patches.sort_by_key(|patch| patch.len());
-    let compressed_patches = file_patches
-        .iter()
-        .rev()
-        .map(|patch| {
-            if current_size > max_size {
-                let compressed = compress_patch(patch).unwrap_or_else(|_| patch.to_string());
-                current_size -= patch.len() - compressed.len();
-                compressed
-            } else {
-                patch.to_string()
-            }
-        })
-        .collect::<Vec<_>>();
-
-    if current_size <= max_size {
-        return compressed_patches.join("\n\n");
-    }
-
-    // Compression level 2: list paths of the changed files only
-    let filenames = file_patches
-        .iter()
-        .map(|patch| {
-            let patch = diffy::Patch::from_str(patch).unwrap();
-            let path = patch
-                .modified()
-                .and_then(|path| path.strip_prefix("b/"))
-                .unwrap_or_default();
-            format!("- {path}\n")
-        })
-        .collect::<Vec<_>>();
-
-    filenames.join("")
-}
-
-/// Split a potentially multi-file patch into multiple single-file patches
-fn split_patch(patch: &str) -> Vec<String> {
-    let mut result = Vec::new();
-    let mut current_patch = String::new();
-
-    for line in patch.lines() {
-        if line.starts_with("---") && !current_patch.is_empty() {
-            result.push(current_patch.trim_end_matches('\n').into());
-            current_patch = String::new();
-        }
-        current_patch.push_str(line);
-        current_patch.push('\n');
-    }
-
-    if !current_patch.is_empty() {
-        result.push(current_patch.trim_end_matches('\n').into());
-    }
-
-    result
-}
-
-fn compress_patch(patch: &str) -> anyhow::Result<String> {
-    let patch = diffy::Patch::from_str(patch)?;
-    let mut out = String::new();
-
-    writeln!(out, "--- {}", patch.original().unwrap_or("a"))?;
-    writeln!(out, "+++ {}", patch.modified().unwrap_or("b"))?;
-
-    for hunk in patch.hunks() {
-        writeln!(out, "@@ -{} +{} @@", hunk.old_range(), hunk.new_range())?;
-        writeln!(out, "[...skipped...]")?;
-    }
-
-    Ok(out)
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use assistant_tool::ToolResultContent;
-    use gpui::{AppContext, TestAppContext};
-    use indoc::indoc;
-    use language_model::{LanguageModelRequest, fake_provider::FakeLanguageModelProvider};
-    use project::{FakeFs, Project};
-    use serde_json::json;
-    use settings::SettingsStore;
-    use std::sync::Arc;
-    use util::path;
-
-    #[gpui::test]
-    async fn test_stale_buffer_notification(cx: &mut TestAppContext) {
-        init_test(cx);
-
-        let fs = FakeFs::new(cx.executor());
-        fs.insert_tree(
-            path!("/test"),
-            json!({"code.rs": "fn main() {\n    println!(\"Hello, world!\");\n}"}),
-        )
-        .await;
-
-        let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
-        let action_log = cx.new(|_| ActionLog::new(project.clone()));
-
-        let buffer_path = project
-            .read_with(cx, |project, cx| {
-                project.find_project_path("test/code.rs", cx)
-            })
-            .unwrap();
-
-        let buffer = project
-            .update(cx, |project, cx| {
-                project.open_buffer(buffer_path.clone(), cx)
-            })
-            .await
-            .unwrap();
-
-        // Start tracking the buffer
-        action_log.update(cx, |log, cx| {
-            log.buffer_read(buffer.clone(), cx);
-        });
-        cx.run_until_parked();
-
-        // Run the tool before any changes
-        let tool = Arc::new(ProjectNotificationsTool);
-        let provider = Arc::new(FakeLanguageModelProvider::default());
-        let model: Arc<dyn LanguageModel> = Arc::new(provider.test_model());
-        let request = Arc::new(LanguageModelRequest::default());
-        let tool_input = json!({});
-
-        let result = cx.update(|cx| {
-            tool.clone().run(
-                tool_input.clone(),
-                request.clone(),
-                project.clone(),
-                action_log.clone(),
-                model.clone(),
-                None,
-                cx,
-            )
-        });
-        cx.run_until_parked();
-
-        let response = result.output.await.unwrap();
-        let response_text = match &response.content {
-            ToolResultContent::Text(text) => text.clone(),
-            _ => panic!("Expected text response"),
-        };
-        assert_eq!(
-            response_text.as_str(),
-            "No new notifications",
-            "Tool should return 'No new notifications' when no stale buffers"
-        );
-
-        // Modify the buffer (makes it stale)
-        buffer.update(cx, |buffer, cx| {
-            buffer.edit([(1..1, "\nChange!\n")], None, cx);
-        });
-        cx.run_until_parked();
-
-        // Run the tool again
-        let result = cx.update(|cx| {
-            tool.clone().run(
-                tool_input.clone(),
-                request.clone(),
-                project.clone(),
-                action_log.clone(),
-                model.clone(),
-                None,
-                cx,
-            )
-        });
-        cx.run_until_parked();
-
-        // This time the buffer is stale, so the tool should return a notification
-        let response = result.output.await.unwrap();
-        let response_text = match &response.content {
-            ToolResultContent::Text(text) => text.clone(),
-            _ => panic!("Expected text response"),
-        };
-
-        assert!(
-            response_text.contains("These files have changed"),
-            "Tool should return the stale buffer notification"
-        );
-        assert!(
-            response_text.contains("test/code.rs"),
-            "Tool should return the stale buffer notification"
-        );
-
-        // Run the tool once more without any changes - should get no new notifications
-        let result = cx.update(|cx| {
-            tool.run(
-                tool_input.clone(),
-                request.clone(),
-                project.clone(),
-                action_log,
-                model.clone(),
-                None,
-                cx,
-            )
-        });
-        cx.run_until_parked();
-
-        let response = result.output.await.unwrap();
-        let response_text = match &response.content {
-            ToolResultContent::Text(text) => text.clone(),
-            _ => panic!("Expected text response"),
-        };
-
-        assert_eq!(
-            response_text.as_str(),
-            "No new notifications",
-            "Tool should return 'No new notifications' when running again without changes"
-        );
-    }
-
-    #[test]
-    fn test_patch_compression() {
-        // Given a patch that doesn't fit into the size budget
-        let patch = indoc! {"
-       --- a/dir/test.txt
-       +++ b/dir/test.txt
-       @@ -1,3 +1,3 @@
-        line 1
-       -line 2
-       +CHANGED
-        line 3
-       @@ -10,2 +10,2 @@
-        line 10
-       -line 11
-       +line eleven
-
-
-       --- a/dir/another.txt
-       +++ b/dir/another.txt
-       @@ -100,1 +1,1 @@
-       -before
-       +after
-       "};
-
-        // When the size deficit can be compensated by dropping the body,
-        // then the body should be trimmed for larger files first
-        let limit = patch.len() - 10;
-        let compressed = fit_patch_to_size(patch, limit);
-        let expected = indoc! {"
-       --- a/dir/test.txt
-       +++ b/dir/test.txt
-       @@ -1,3 +1,3 @@
-       [...skipped...]
-       @@ -10,2 +10,2 @@
-       [...skipped...]
-
-
-       --- a/dir/another.txt
-       +++ b/dir/another.txt
-       @@ -100,1 +1,1 @@
-       -before
-       +after"};
-        assert_eq!(compressed, expected);
-
-        // When the size deficit is too large, then only file paths
-        // should be returned
-        let limit = 10;
-        let compressed = fit_patch_to_size(patch, limit);
-        let expected = indoc! {"
-       - dir/another.txt
-       - dir/test.txt
-       "};
-        assert_eq!(compressed, expected);
-    }
-
-    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);
-            assistant_tool::init(cx);
-        });
-    }
-}

crates/assistant_tools/src/read_file_tool.rs 🔗

@@ -1,1190 +0,0 @@
-use crate::schema::json_schema_for;
-use action_log::ActionLog;
-use anyhow::{Context as _, Result, anyhow};
-use assistant_tool::{Tool, ToolResult};
-use assistant_tool::{ToolResultContent, outline};
-use gpui::{AnyWindowHandle, App, Entity, Task};
-use project::{ImageItem, image_store};
-
-use assistant_tool::ToolResultOutput;
-use indoc::formatdoc;
-use itertools::Itertools;
-use language::{Anchor, Point};
-use language_model::{
-    LanguageModel, LanguageModelImage, LanguageModelRequest, LanguageModelToolSchemaFormat,
-};
-use project::{AgentLocation, Project, WorktreeSettings};
-use schemars::JsonSchema;
-use serde::{Deserialize, Serialize};
-use settings::Settings;
-use std::sync::Arc;
-use ui::IconName;
-
-/// If the model requests to read a file whose size exceeds this, then
-#[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.
-    ///
-    /// <example>
-    /// 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`.
-    /// </example>
-    pub path: String,
-
-    /// Optional line number to start reading on (1-based index)
-    #[serde(default)]
-    pub start_line: Option<u32>,
-
-    /// Optional line number to end reading on (1-based index, inclusive)
-    #[serde(default)]
-    pub end_line: Option<u32>,
-}
-
-pub struct ReadFileTool;
-
-impl Tool for ReadFileTool {
-    fn name(&self) -> String {
-        "read_file".into()
-    }
-
-    fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
-        false
-    }
-
-    fn may_perform_edits(&self) -> bool {
-        false
-    }
-
-    fn description(&self) -> String {
-        include_str!("./read_file_tool/description.md").into()
-    }
-
-    fn icon(&self) -> IconName {
-        IconName::ToolSearch
-    }
-
-    fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
-        json_schema_for::<ReadFileToolInput>(format)
-    }
-
-    fn ui_text(&self, input: &serde_json::Value) -> String {
-        match serde_json::from_value::<ReadFileToolInput>(input.clone()) {
-            Ok(input) => {
-                let path = &input.path;
-                match (input.start_line, input.end_line) {
-                    (Some(start), Some(end)) => {
-                        format!(
-                            "[Read file `{}` (lines {}-{})](@selection:{}:({}-{}))",
-                            path, start, end, path, start, end
-                        )
-                    }
-                    (Some(start), None) => {
-                        format!(
-                            "[Read file `{}` (from line {})](@selection:{}:({}-{}))",
-                            path, start, path, start, start
-                        )
-                    }
-                    _ => format!("[Read file `{}`](@file:{})", path, path),
-                }
-            }
-            Err(_) => "Read file".to_string(),
-        }
-    }
-
-    fn run(
-        self: Arc<Self>,
-        input: serde_json::Value,
-        _request: Arc<LanguageModelRequest>,
-        project: Entity<Project>,
-        action_log: Entity<ActionLog>,
-        model: Arc<dyn LanguageModel>,
-        _window: Option<AnyWindowHandle>,
-        cx: &mut App,
-    ) -> ToolResult {
-        let input = match serde_json::from_value::<ReadFileToolInput>(input) {
-            Ok(input) => input,
-            Err(err) => return Task::ready(Err(anyhow!(err))).into(),
-        };
-
-        let Some(project_path) = project.read(cx).find_project_path(&input.path, cx) else {
-            return Task::ready(Err(anyhow!("Path {} not found in project", &input.path))).into();
-        };
-
-        // 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
-            )))
-            .into();
-        }
-
-        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
-            )))
-            .into();
-        }
-
-        // 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
-            )))
-            .into();
-        }
-
-        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
-            )))
-            .into();
-        }
-
-        let file_path = input.path.clone();
-
-        if image_store::is_image_file(&project, &project_path, cx) {
-            if !model.supports_images() {
-                return Task::ready(Err(anyhow!(
-                    "Attempted to read an image, but Zed doesn't currently support sending images to {}.",
-                    model.name().0
-                )))
-                .into();
-            }
-
-            let task = cx.spawn(async move |cx| -> Result<ToolResultOutput> {
-                let image_entity: Entity<ImageItem> = cx
-                    .update(|cx| {
-                        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(ToolResultOutput {
-                    content: ToolResultContent::Image(language_model_image),
-                    output: None,
-                })
-            });
-
-            return task.into();
-        }
-
-        cx.spawn(async move |cx| {
-            let buffer = cx
-                .update(|cx| {
-                    project.update(cx, |project, cx| project.open_buffer(project_path, 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");
-            }
-
-            project.update(cx, |project, cx| {
-                project.set_agent_location(
-                    Some(AgentLocation {
-                        buffer: buffer.downgrade(),
-                        position: Anchor::MIN,
-                    }),
-                    cx,
-                );
-            })?;
-
-            // Check if specific line ranges are provided
-            if input.start_line.is_some() || input.end_line.is_some() {
-                let mut anchor = None;
-                let result = buffer.read_with(cx, |buffer, _cx| {
-                    let text = buffer.text();
-                    // .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 lines = text.split('\n').skip(start_row as usize);
-                    if let Some(end) = input.end_line {
-                        let count = end.saturating_sub(start).saturating_add(1); // Ensure at least 1 line
-                        Itertools::intersperse(lines.take(count as usize), "\n")
-                            .collect::<String>()
-                            .into()
-                    } else {
-                        Itertools::intersperse(lines, "\n")
-                            .collect::<String>()
-                            .into()
-                    }
-                })?;
-
-                action_log.update(cx, |log, cx| {
-                    log.buffer_read(buffer.clone(), cx);
-                })?;
-
-                if let Some(anchor) = anchor {
-                    project.update(cx, |project, cx| {
-                        project.set_agent_location(
-                            Some(AgentLocation {
-                                buffer: buffer.downgrade(),
-                                position: anchor,
-                            }),
-                            cx,
-                        );
-                    })?;
-                }
-
-                Ok(result)
-            } 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(&file_path), cx)
-                        .await?;
-
-                action_log.update(cx, |log, cx| {
-                    log.buffer_read(buffer, cx);
-                })?;
-
-                if buffer_content.is_outline {
-                    Ok(formatdoc! {"
-                        This file was too big to read all at once.
-
-                        {}
-
-                        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.
-
-                        Alternatively, you can fall back to the `grep` tool (if available)
-                        to search the file for specific content.", buffer_content.text
-                    }
-                    .into())
-                } else {
-                    Ok(buffer_content.text.into())
-                }
-            }
-        })
-        .into()
-    }
-}
-
-#[cfg(test)]
-mod test {
-    use super::*;
-    use gpui::{AppContext, TestAppContext, UpdateGlobal};
-    use language::{Language, LanguageConfig, LanguageMatcher};
-    use language_model::fake_provider::FakeLanguageModel;
-    use project::{FakeFs, Project};
-    use serde_json::json;
-    use settings::SettingsStore;
-    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 model = Arc::new(FakeLanguageModel::default());
-        let result = cx
-            .update(|cx| {
-                let input = json!({
-                    "path": "root/nonexistent_file.txt"
-                });
-                Arc::new(ReadFileTool)
-                    .run(
-                        input,
-                        Arc::default(),
-                        project.clone(),
-                        action_log,
-                        model,
-                        None,
-                        cx,
-                    )
-                    .output
-            })
-            .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 model = Arc::new(FakeLanguageModel::default());
-        let result = cx
-            .update(|cx| {
-                let input = json!({
-                    "path": "root/small_file.txt"
-                });
-                Arc::new(ReadFileTool)
-                    .run(
-                        input,
-                        Arc::default(),
-                        project.clone(),
-                        action_log,
-                        model,
-                        None,
-                        cx,
-                    )
-                    .output
-            })
-            .await;
-        assert_eq!(
-            result.unwrap().content.as_str(),
-            Some("This is a small file content")
-        );
-    }
-
-    #[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::<Vec<_>>().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(Arc::new(rust_lang()));
-        let action_log = cx.new(|_| ActionLog::new(project.clone()));
-        let model = Arc::new(FakeLanguageModel::default());
-
-        let result = cx
-            .update(|cx| {
-                let input = json!({
-                    "path": "root/large_file.rs"
-                });
-                Arc::new(ReadFileTool)
-                    .run(
-                        input,
-                        Arc::default(),
-                        project.clone(),
-                        action_log.clone(),
-                        model.clone(),
-                        None,
-                        cx,
-                    )
-                    .output
-            })
-            .await;
-        let content = result.unwrap();
-        let content = content.as_str().unwrap();
-        assert_eq!(
-            content.lines().skip(4).take(6).collect::<Vec<_>>(),
-            vec![
-                "struct Test0 [L1-4]",
-                " a [L2]",
-                " b [L3]",
-                "struct Test1 [L5-8]",
-                " a [L6]",
-                " b [L7]",
-            ]
-        );
-
-        let result = cx
-            .update(|cx| {
-                let input = json!({
-                    "path": "root/large_file.rs",
-                    "offset": 1
-                });
-                Arc::new(ReadFileTool)
-                    .run(
-                        input,
-                        Arc::default(),
-                        project.clone(),
-                        action_log,
-                        model,
-                        None,
-                        cx,
-                    )
-                    .output
-            })
-            .await;
-        let content = result.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::<Vec<_>>();
-        pretty_assertions::assert_eq!(
-            content
-                .as_str()
-                .unwrap()
-                .lines()
-                .skip(4)
-                .take(expected_content.len())
-                .collect::<Vec<_>>(),
-            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 model = Arc::new(FakeLanguageModel::default());
-        let result = cx
-            .update(|cx| {
-                let input = json!({
-                    "path": "root/multiline.txt",
-                    "start_line": 2,
-                    "end_line": 4
-                });
-                Arc::new(ReadFileTool)
-                    .run(
-                        input,
-                        Arc::default(),
-                        project.clone(),
-                        action_log,
-                        model,
-                        None,
-                        cx,
-                    )
-                    .output
-            })
-            .await;
-        assert_eq!(
-            result.unwrap().content.as_str(),
-            Some("Line 2\nLine 3\nLine 4")
-        );
-    }
-
-    #[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 model = Arc::new(FakeLanguageModel::default());
-
-        // start_line of 0 should be treated as 1
-        let result = cx
-            .update(|cx| {
-                let input = json!({
-                    "path": "root/multiline.txt",
-                    "start_line": 0,
-                    "end_line": 2
-                });
-                Arc::new(ReadFileTool)
-                    .run(
-                        input,
-                        Arc::default(),
-                        project.clone(),
-                        action_log.clone(),
-                        model.clone(),
-                        None,
-                        cx,
-                    )
-                    .output
-            })
-            .await;
-        assert_eq!(result.unwrap().content.as_str(), Some("Line 1\nLine 2"));
-
-        // end_line of 0 should result in at least 1 line
-        let result = cx
-            .update(|cx| {
-                let input = json!({
-                    "path": "root/multiline.txt",
-                    "start_line": 1,
-                    "end_line": 0
-                });
-                Arc::new(ReadFileTool)
-                    .run(
-                        input,
-                        Arc::default(),
-                        project.clone(),
-                        action_log.clone(),
-                        model.clone(),
-                        None,
-                        cx,
-                    )
-                    .output
-            })
-            .await;
-        assert_eq!(result.unwrap().content.as_str(), Some("Line 1"));
-
-        // when start_line > end_line, should still return at least 1 line
-        let result = cx
-            .update(|cx| {
-                let input = json!({
-                    "path": "root/multiline.txt",
-                    "start_line": 3,
-                    "end_line": 2
-                });
-                Arc::new(ReadFileTool)
-                    .run(
-                        input,
-                        Arc::default(),
-                        project.clone(),
-                        action_log,
-                        model,
-                        None,
-                        cx,
-                    )
-                    .output
-            })
-            .await;
-        assert_eq!(result.unwrap().content.as_str(), Some("Line 3"));
-    }
-
-    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);
-        });
-    }
-
-    fn rust_lang() -> Language {
-        Language::new(
-            LanguageConfig {
-                name: "Rust".into(),
-                matcher: LanguageMatcher {
-                    path_suffixes: vec!["rs".to_string()],
-                    ..Default::default()
-                },
-                ..Default::default()
-            },
-            Some(tree_sitter_rust::LANGUAGE.into()),
-        )
-        .with_outline_query(
-            r#"
-            (line_comment) @annotation
-
-            (struct_item
-                "struct" @context
-                name: (_) @name) @item
-            (enum_item
-                "enum" @context
-                name: (_) @name) @item
-            (enum_variant
-                name: (_) @name) @item
-            (field_declaration
-                name: (_) @name) @item
-            (impl_item
-                "impl" @context
-                trait: (_)? @name
-                "for"? @context
-                type: (_) @name
-                body: (_ "{" (_)* "}")) @item
-            (function_item
-                "fn" @context
-                name: (_) @name) @item
-            (mod_item
-                "mod" @context
-                name: (_) @name) @item
-            "#,
-        )
-        .unwrap()
-    }
-
-    #[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 model = Arc::new(FakeLanguageModel::default());
-
-        // Reading a file outside the project worktree should fail
-        let result = cx
-            .update(|cx| {
-                let input = json!({
-                    "path": "/outside_project/sensitive_file.txt"
-                });
-                Arc::new(ReadFileTool)
-                    .run(
-                        input,
-                        Arc::default(),
-                        project.clone(),
-                        action_log.clone(),
-                        model.clone(),
-                        None,
-                        cx,
-                    )
-                    .output
-            })
-            .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 = json!({
-                    "path": "project_root/allowed_file.txt"
-                });
-                Arc::new(ReadFileTool)
-                    .run(
-                        input,
-                        Arc::default(),
-                        project.clone(),
-                        action_log.clone(),
-                        model.clone(),
-                        None,
-                        cx,
-                    )
-                    .output
-            })
-            .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 = json!({
-                    "path": "project_root/.secretdir/config"
-                });
-                Arc::new(ReadFileTool)
-                    .run(
-                        input,
-                        Arc::default(),
-                        project.clone(),
-                        action_log.clone(),
-                        model.clone(),
-                        None,
-                        cx,
-                    )
-                    .output
-            })
-            .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 = json!({
-                    "path": "project_root/.mymetadata"
-                });
-                Arc::new(ReadFileTool)
-                    .run(
-                        input,
-                        Arc::default(),
-                        project.clone(),
-                        action_log.clone(),
-                        model.clone(),
-                        None,
-                        cx,
-                    )
-                    .output
-            })
-            .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 = json!({
-                    "path": "project_root/.mysecrets"
-                });
-                Arc::new(ReadFileTool)
-                    .run(
-                        input,
-                        Arc::default(),
-                        project.clone(),
-                        action_log.clone(),
-                        model.clone(),
-                        None,
-                        cx,
-                    )
-                    .output
-            })
-            .await;
-        assert!(
-            result.is_err(),
-            "read_file_tool should error when attempting to read .mysecrets (private_files)"
-        );
-
-        let result = cx
-            .update(|cx| {
-                let input = json!({
-                    "path": "project_root/subdir/special.privatekey"
-                });
-                Arc::new(ReadFileTool)
-                    .run(
-                        input,
-                        Arc::default(),
-                        project.clone(),
-                        action_log.clone(),
-                        model.clone(),
-                        None,
-                        cx,
-                    )
-                    .output
-            })
-            .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 = json!({
-                    "path": "project_root/subdir/data.mysensitive"
-                });
-                Arc::new(ReadFileTool)
-                    .run(
-                        input,
-                        Arc::default(),
-                        project.clone(),
-                        action_log.clone(),
-                        model.clone(),
-                        None,
-                        cx,
-                    )
-                    .output
-            })
-            .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 = json!({
-                    "path": "project_root/subdir/normal_file.txt"
-                });
-                Arc::new(ReadFileTool)
-                    .run(
-                        input,
-                        Arc::default(),
-                        project.clone(),
-                        action_log.clone(),
-                        model.clone(),
-                        None,
-                        cx,
-                    )
-                    .output
-            })
-            .await;
-        assert!(result.is_ok(), "Should be able to read normal files");
-        assert_eq!(
-            result.unwrap().content.as_str().unwrap(),
-            "Normal file content"
-        );
-
-        // Path traversal attempts with .. should fail
-        let result = cx
-            .update(|cx| {
-                let input = json!({
-                    "path": "project_root/../outside_project/sensitive_file.txt"
-                });
-                Arc::new(ReadFileTool)
-                    .run(
-                        input,
-                        Arc::default(),
-                        project.clone(),
-                        action_log.clone(),
-                        model.clone(),
-                        None,
-                        cx,
-                    )
-                    .output
-            })
-            .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 model = Arc::new(FakeLanguageModel::default());
-        let tool = Arc::new(ReadFileTool);
-
-        // Test reading allowed files in worktree1
-        let input = json!({
-            "path": "worktree1/src/main.rs"
-        });
-
-        let result = cx
-            .update(|cx| {
-                tool.clone().run(
-                    input,
-                    Arc::default(),
-                    project.clone(),
-                    action_log.clone(),
-                    model.clone(),
-                    None,
-                    cx,
-                )
-            })
-            .output
-            .await
-            .unwrap();
-
-        assert_eq!(
-            result.content.as_str().unwrap(),
-            "fn main() { println!(\"Hello from worktree1\"); }"
-        );
-
-        // Test reading private file in worktree1 should fail
-        let input = json!({
-            "path": "worktree1/src/secret.rs"
-        });
-
-        let result = cx
-            .update(|cx| {
-                tool.clone().run(
-                    input,
-                    Arc::default(),
-                    project.clone(),
-                    action_log.clone(),
-                    model.clone(),
-                    None,
-                    cx,
-                )
-            })
-            .output
-            .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 input = json!({
-            "path": "worktree1/tests/fixture.sql"
-        });
-
-        let result = cx
-            .update(|cx| {
-                tool.clone().run(
-                    input,
-                    Arc::default(),
-                    project.clone(),
-                    action_log.clone(),
-                    model.clone(),
-                    None,
-                    cx,
-                )
-            })
-            .output
-            .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 input = json!({
-            "path": "worktree2/lib/public.js"
-        });
-
-        let result = cx
-            .update(|cx| {
-                tool.clone().run(
-                    input,
-                    Arc::default(),
-                    project.clone(),
-                    action_log.clone(),
-                    model.clone(),
-                    None,
-                    cx,
-                )
-            })
-            .output
-            .await
-            .unwrap();
-
-        assert_eq!(
-            result.content.as_str().unwrap(),
-            "export function greet() { return 'Hello from worktree2'; }"
-        );
-
-        // Test reading private file in worktree2 should fail
-        let input = json!({
-            "path": "worktree2/lib/private.js"
-        });
-
-        let result = cx
-            .update(|cx| {
-                tool.clone().run(
-                    input,
-                    Arc::default(),
-                    project.clone(),
-                    action_log.clone(),
-                    model.clone(),
-                    None,
-                    cx,
-                )
-            })
-            .output
-            .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 input = json!({
-            "path": "worktree2/docs/internal.md"
-        });
-
-        let result = cx
-            .update(|cx| {
-                tool.clone().run(
-                    input,
-                    Arc::default(),
-                    project.clone(),
-                    action_log.clone(),
-                    model.clone(),
-                    None,
-                    cx,
-                )
-            })
-            .output
-            .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 input = json!({
-            "path": "worktree1/src/config.toml"
-        });
-
-        let result = cx
-            .update(|cx| {
-                tool.clone().run(
-                    input,
-                    Arc::default(),
-                    project.clone(),
-                    action_log.clone(),
-                    model.clone(),
-                    None,
-                    cx,
-                )
-            })
-            .output
-            .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"
-        );
-    }
-}

crates/assistant_tools/src/schema.rs 🔗

@@ -1,60 +0,0 @@
-use anyhow::Result;
-use language_model::LanguageModelToolSchemaFormat;
-use schemars::{
-    JsonSchema, Schema,
-    generate::SchemaSettings,
-    transform::{Transform, transform_subschemas},
-};
-
-pub fn json_schema_for<T: JsonSchema>(
-    format: LanguageModelToolSchemaFormat,
-) -> Result<serde_json::Value> {
-    let schema = root_schema_for::<T>(format);
-    schema_to_json(&schema, format)
-}
-
-fn schema_to_json(
-    schema: &Schema,
-    format: LanguageModelToolSchemaFormat,
-) -> Result<serde_json::Value> {
-    let mut value = serde_json::to_value(schema)?;
-    assistant_tool::adapt_schema_to_format(&mut value, format)?;
-    Ok(value)
-}
-
-fn root_schema_for<T: JsonSchema>(format: LanguageModelToolSchemaFormat) -> Schema {
-    let mut generator = match format {
-        LanguageModelToolSchemaFormat::JsonSchema => SchemaSettings::draft07().into_generator(),
-        LanguageModelToolSchemaFormat::JsonSchemaSubset => SchemaSettings::openapi3()
-            .with(|settings| {
-                settings.meta_schema = None;
-                settings.inline_subschemas = true;
-            })
-            .with_transform(ToJsonSchemaSubsetTransform)
-            .into_generator(),
-    };
-    generator.root_schema_for::<T>()
-}
-
-#[derive(Debug, Clone)]
-struct ToJsonSchemaSubsetTransform;
-
-impl Transform for ToJsonSchemaSubsetTransform {
-    fn transform(&mut self, schema: &mut Schema) {
-        // Ensure that the type field is not an array, this happens when we use
-        // Option<T>, the type will be [T, "null"].
-        if let Some(type_field) = schema.get_mut("type")
-            && let Some(types) = type_field.as_array()
-            && let Some(first_type) = types.first()
-        {
-            *type_field = first_type.clone();
-        }
-
-        // oneOf is not supported, use anyOf instead
-        if let Some(one_of) = schema.remove("oneOf") {
-            schema.insert("anyOf".to_string(), one_of);
-        }
-
-        transform_subschemas(self, schema);
-    }
-}

crates/assistant_tools/src/templates.rs 🔗

@@ -1,32 +0,0 @@
-use anyhow::Result;
-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<Self> {
-        let mut handlebars = Handlebars::new();
-        handlebars.register_embed_templates::<Assets>().unwrap();
-        handlebars.register_escape_fn(|text| text.into());
-        Arc::new(Self(handlebars))
-    }
-}
-
-pub trait Template: Sized {
-    const TEMPLATE_NAME: &'static str;
-
-    fn render(&self, templates: &Templates) -> Result<String>
-    where
-        Self: Serialize + Sized,
-    {
-        Ok(templates.0.render(Self::TEMPLATE_NAME, self)?)
-    }
-}

crates/assistant_tools/src/terminal_tool.rs 🔗

@@ -1,883 +0,0 @@
-use crate::{
-    schema::json_schema_for,
-    ui::{COLLAPSED_LINES, ToolOutputPreview},
-};
-use action_log::ActionLog;
-use agent_settings;
-use anyhow::{Context as _, Result, anyhow};
-use assistant_tool::{Tool, ToolCard, ToolResult, ToolUseStatus};
-use futures::FutureExt as _;
-use gpui::{
-    AnyWindowHandle, App, AppContext, Empty, Entity, EntityId, Task, TextStyleRefinement,
-    WeakEntity, Window,
-};
-use language::LineEnding;
-use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
-use markdown::{Markdown, MarkdownElement, MarkdownStyle};
-use portable_pty::{CommandBuilder, PtySize, native_pty_system};
-use project::Project;
-use schemars::JsonSchema;
-use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsLocation};
-use std::{
-    env,
-    path::{Path, PathBuf},
-    process::ExitStatus,
-    sync::Arc,
-    time::{Duration, Instant},
-};
-use task::{Shell, ShellBuilder};
-use terminal::terminal_settings::TerminalSettings;
-use terminal_view::TerminalView;
-use theme::ThemeSettings;
-use ui::{CommonAnimationExt, Disclosure, Tooltip, prelude::*};
-use util::{
-    ResultExt, get_default_system_shell_preferring_bash, markdown::MarkdownInlineCode,
-    size::format_file_size, time::duration_alt_display,
-};
-use workspace::Workspace;
-
-const COMMAND_OUTPUT_LIMIT: usize = 16 * 1024;
-
-#[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;
-
-impl TerminalTool {
-    pub const NAME: &str = "terminal";
-}
-
-impl Tool for TerminalTool {
-    fn name(&self) -> String {
-        Self::NAME.to_string()
-    }
-
-    fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
-        true
-    }
-
-    fn may_perform_edits(&self) -> bool {
-        false
-    }
-
-    fn description(&self) -> String {
-        include_str!("./terminal_tool/description.md").to_string()
-    }
-
-    fn icon(&self) -> IconName {
-        IconName::ToolTerminal
-    }
-
-    fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
-        json_schema_for::<TerminalToolInput>(format)
-    }
-
-    fn ui_text(&self, input: &serde_json::Value) -> String {
-        match serde_json::from_value::<TerminalToolInput>(input.clone()) {
-            Ok(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(),
-                    1 => MarkdownInlineCode(&format!(
-                        "{} - {} more line",
-                        first_line, remaining_line_count
-                    ))
-                    .to_string(),
-                    n => MarkdownInlineCode(&format!("{} - {} more lines", first_line, n))
-                        .to_string(),
-                }
-            }
-            Err(_) => "Run terminal command".to_string(),
-        }
-    }
-
-    fn run(
-        self: Arc<Self>,
-        input: serde_json::Value,
-        _request: Arc<LanguageModelRequest>,
-        project: Entity<Project>,
-        _action_log: Entity<ActionLog>,
-        _model: Arc<dyn LanguageModel>,
-        window: Option<AnyWindowHandle>,
-        cx: &mut App,
-    ) -> ToolResult {
-        let input: TerminalToolInput = match serde_json::from_value(input) {
-            Ok(input) => input,
-            Err(err) => return Task::ready(Err(anyhow!(err))).into(),
-        };
-
-        let working_dir = match working_dir(&input, &project, cx) {
-            Ok(dir) => dir,
-            Err(err) => return Task::ready(Err(err)).into(),
-        };
-
-        let cwd = working_dir.clone();
-        let env = match &cwd {
-            Some(dir) => project.update(cx, |project, cx| {
-                let worktree = project.find_worktree(dir.as_path(), cx);
-                let shell = TerminalSettings::get(
-                    worktree.as_ref().map(|(worktree, path)| SettingsLocation {
-                        worktree_id: worktree.read(cx).id(),
-                        path: &path,
-                    }),
-                    cx,
-                )
-                .shell
-                .clone();
-                project.directory_environment(&shell, dir.as_path().into(), cx)
-            }),
-            None => Task::ready(None).shared(),
-        };
-        let is_windows = project.read(cx).path_style(cx).is_windows();
-        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 env = cx.spawn(async move |_| {
-            let mut env = env.await.unwrap_or_default();
-            if cfg!(unix) {
-                env.insert("PAGER".into(), "cat".into());
-            }
-            env
-        });
-
-        let build_cmd = {
-            let input_command = input.command.clone();
-            move || {
-                ShellBuilder::new(&Shell::Program(shell), is_windows)
-                    .redirect_stdin_to_dev_null()
-                    .build(Some(input_command), &[])
-            }
-        };
-
-        let Some(window) = window else {
-            // Headless setup, a test or eval. Our terminal subsystem requires a workspace,
-            // so bypass it and provide a convincing imitation using a pty.
-            let task = cx.background_spawn(async move {
-                let env = env.await;
-                let pty_system = native_pty_system();
-                let (command, args) = build_cmd();
-                let mut cmd = CommandBuilder::new(command);
-                cmd.args(args);
-                for (k, v) in env {
-                    cmd.env(k, v);
-                }
-                if let Some(cwd) = cwd {
-                    cmd.cwd(cwd);
-                }
-                let pair = pty_system.openpty(PtySize {
-                    rows: 24,
-                    cols: 80,
-                    ..Default::default()
-                })?;
-                let mut child = pair.slave.spawn_command(cmd)?;
-                let mut reader = pair.master.try_clone_reader()?;
-                drop(pair);
-                let mut content = String::new();
-                reader.read_to_string(&mut content)?;
-                // Massage the pty output a bit to try to match what the terminal codepath gives us
-                LineEnding::normalize(&mut content);
-                content = content
-                    .chars()
-                    .filter(|c| c.is_ascii_whitespace() || !c.is_ascii_control())
-                    .collect();
-                let content = content.trim_start().trim_start_matches("^D");
-                let exit_status = child.wait()?;
-                let (processed_content, _) =
-                    process_content(content, &input.command, Some(exit_status));
-                Ok(processed_content.into())
-            });
-            return ToolResult {
-                output: task,
-                card: None,
-            };
-        };
-
-        let terminal = cx.spawn({
-            let project = project.downgrade();
-            async move |cx| {
-                let (command, args) = build_cmd();
-                let env = env.await;
-                project
-                    .update(cx, |project, cx| {
-                        project.create_terminal_task(
-                            task::SpawnInTerminal {
-                                command: Some(command),
-                                args,
-                                cwd,
-                                env,
-                                ..Default::default()
-                            },
-                            cx,
-                        )
-                    })?
-                    .await
-            }
-        });
-
-        let command_markdown = cx.new(|cx| {
-            Markdown::new(
-                format!("```bash\n{}\n```", input.command).into(),
-                None,
-                None,
-                cx,
-            )
-        });
-
-        let card =
-            cx.new(|cx| TerminalToolCard::new(command_markdown, working_dir, cx.entity_id(), cx));
-
-        let output = cx.spawn({
-            let card = card.clone();
-            async move |cx| {
-                let terminal = terminal.await?;
-                let workspace = window
-                    .downcast::<Workspace>()
-                    .and_then(|handle| handle.entity(cx).ok())
-                    .context("no workspace entity in root of window")?;
-
-                let terminal_view = window.update(cx, |_, window, cx| {
-                    cx.new(|cx| {
-                        let mut view = TerminalView::new(
-                            terminal.clone(),
-                            workspace.downgrade(),
-                            None,
-                            project.downgrade(),
-                            window,
-                            cx,
-                        );
-                        view.set_embedded_mode(None, cx);
-                        view
-                    })
-                })?;
-
-                card.update(cx, |card, _| {
-                    card.terminal = Some(terminal_view.clone());
-                    card.start_instant = Instant::now();
-                })
-                .log_err();
-
-                let exit_status = terminal
-                    .update(cx, |terminal, cx| terminal.wait_for_completed_task(cx))?
-                    .await;
-                let (content, content_line_count) = terminal.read_with(cx, |terminal, _| {
-                    (terminal.get_content(), terminal.total_lines())
-                })?;
-
-                let previous_len = content.len();
-                let (processed_content, finished_with_empty_output) = process_content(
-                    &content,
-                    &input.command,
-                    exit_status.map(portable_pty::ExitStatus::from),
-                );
-
-                card.update(cx, |card, _| {
-                    card.command_finished = true;
-                    card.exit_status = exit_status;
-                    card.was_content_truncated = processed_content.len() < previous_len;
-                    card.original_content_len = previous_len;
-                    card.content_line_count = content_line_count;
-                    card.finished_with_empty_output = finished_with_empty_output;
-                    card.elapsed_time = Some(card.start_instant.elapsed());
-                })
-                .log_err();
-
-                Ok(processed_content.into())
-            }
-        });
-
-        ToolResult {
-            output,
-            card: Some(card.into()),
-        }
-    }
-}
-
-fn process_content(
-    content: &str,
-    command: &str,
-    exit_status: Option<portable_pty::ExitStatus>,
-) -> (String, bool) {
-    let should_truncate = content.len() > COMMAND_OUTPUT_LIMIT;
-
-    let content = if should_truncate {
-        let mut end_ix = COMMAND_OUTPUT_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[..end_ix]
-    } else {
-        content
-    };
-    let content = content.trim();
-    let is_empty = content.is_empty();
-    let content = format!("```\n{content}\n```");
-    let content = if should_truncate {
-        format!(
-            "Command output too long. The first {} bytes:\n\n{content}",
-            content.len(),
-        )
-    } else {
-        content
-    };
-
-    let content = match exit_status {
-        Some(exit_status) if exit_status.success() => {
-            if is_empty {
-                "Command executed successfully.".to_string()
-            } else {
-                content
-            }
-        }
-        Some(exit_status) => {
-            if is_empty {
-                format!(
-                    "Command \"{command}\" failed with exit code {}.",
-                    exit_status.exit_code()
-                )
-            } else {
-                format!(
-                    "Command \"{command}\" failed with exit code {}.\n\n{content}",
-                    exit_status.exit_code()
-                )
-            }
-        }
-        None => {
-            format!(
-                "Command failed or was interrupted.\nPartial output captured:\n\n{}",
-                content,
-            )
-        }
-    };
-    (content, is_empty)
-}
-
-fn working_dir(
-    input: &TerminalToolInput,
-    project: &Entity<Project>,
-    cx: &mut App,
-) -> Result<Option<PathBuf>> {
-    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.");
-    }
-}
-
-struct TerminalToolCard {
-    input_command: Entity<Markdown>,
-    working_dir: Option<PathBuf>,
-    entity_id: EntityId,
-    exit_status: Option<ExitStatus>,
-    terminal: Option<Entity<TerminalView>>,
-    command_finished: bool,
-    was_content_truncated: bool,
-    finished_with_empty_output: bool,
-    content_line_count: usize,
-    original_content_len: usize,
-    preview_expanded: bool,
-    start_instant: Instant,
-    elapsed_time: Option<Duration>,
-}
-
-impl TerminalToolCard {
-    pub fn new(
-        input_command: Entity<Markdown>,
-        working_dir: Option<PathBuf>,
-        entity_id: EntityId,
-        cx: &mut Context<Self>,
-    ) -> Self {
-        let expand_terminal_card =
-            agent_settings::AgentSettings::get_global(cx).expand_terminal_card;
-        Self {
-            input_command,
-            working_dir,
-            entity_id,
-            exit_status: None,
-            terminal: None,
-            command_finished: false,
-            was_content_truncated: false,
-            finished_with_empty_output: false,
-            original_content_len: 0,
-            content_line_count: 0,
-            preview_expanded: expand_terminal_card,
-            start_instant: Instant::now(),
-            elapsed_time: None,
-        }
-    }
-}
-
-impl ToolCard for TerminalToolCard {
-    fn render(
-        &mut self,
-        status: &ToolUseStatus,
-        window: &mut Window,
-        _workspace: WeakEntity<Workspace>,
-        cx: &mut Context<Self>,
-    ) -> impl IntoElement {
-        let Some(terminal) = self.terminal.as_ref() else {
-            return Empty.into_any();
-        };
-
-        let tool_failed = matches!(status, ToolUseStatus::Error(_));
-
-        let command_failed =
-            self.command_finished && self.exit_status.is_none_or(|code| !code.success());
-
-        if (tool_failed || command_failed) && self.elapsed_time.is_none() {
-            self.elapsed_time = Some(self.start_instant.elapsed());
-        }
-        let time_elapsed = self
-            .elapsed_time
-            .unwrap_or_else(|| self.start_instant.elapsed());
-
-        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 path = self
-            .working_dir
-            .as_ref()
-            .cloned()
-            .or_else(|| env::current_dir().ok())
-            .map(|path| path.display().to_string())
-            .unwrap_or_else(|| "current directory".to_string());
-
-        let header = h_flex()
-            .flex_none()
-            .gap_1()
-            .justify_between()
-            .rounded_t_md()
-            .child(
-                div()
-                    .id(("command-target-path", self.entity_id))
-                    .w_full()
-                    .max_w_full()
-                    .overflow_x_scroll()
-                    .child(
-                        Label::new(path)
-                            .buffer_font(cx)
-                            .size(LabelSize::XSmall)
-                            .color(Color::Muted),
-                    ),
-            )
-            .when(!self.command_finished, |header| {
-                header.child(
-                    Icon::new(IconName::ArrowCircle)
-                        .size(IconSize::XSmall)
-                        .color(Color::Info)
-                        .with_rotate_animation(2),
-                )
-            })
-            .when(tool_failed || command_failed, |header| {
-                header.child(
-                    div()
-                        .id(("terminal-tool-error-code-indicator", self.entity_id))
-                        .child(
-                            Icon::new(IconName::Close)
-                                .size(IconSize::Small)
-                                .color(Color::Error),
-                        )
-                        .when(command_failed && self.exit_status.is_some(), |this| {
-                            this.tooltip(Tooltip::text(format!(
-                                "Exited with code {}",
-                                self.exit_status
-                                    .and_then(|status| status.code())
-                                    .unwrap_or(-1),
-                            )))
-                        })
-                        .when(
-                            !command_failed && tool_failed && status.error().is_some(),
-                            |this| {
-                                this.tooltip(Tooltip::text(format!(
-                                    "Error: {}",
-                                    status.error().unwrap(),
-                                )))
-                            },
-                        ),
-                )
-            })
-            .when(self.was_content_truncated, |header| {
-                let tooltip = if self.content_line_count + 10 > terminal::MAX_SCROLL_HISTORY_LINES {
-                    "Output exceeded terminal max lines and was \
-                        truncated, the model received the first 16 KB."
-                        .to_string()
-                } else {
-                    format!(
-                        "Output is {} long, to avoid unexpected token usage, \
-                            only 16 KB was sent back to the model.",
-                        format_file_size(self.original_content_len as u64, true),
-                    )
-                };
-                header.child(
-                    h_flex()
-                        .id(("terminal-tool-truncated-label", self.entity_id))
-                        .tooltip(Tooltip::text(tooltip))
-                        .gap_1()
-                        .child(
-                            Icon::new(IconName::Info)
-                                .size(IconSize::XSmall)
-                                .color(Color::Ignored),
-                        )
-                        .child(
-                            Label::new("Truncated")
-                                .color(Color::Muted)
-                                .size(LabelSize::Small),
-                        ),
-                )
-            })
-            .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::Small),
-                )
-            })
-            .when(!self.finished_with_empty_output, |header| {
-                header.child(
-                    Disclosure::new(
-                        ("terminal-tool-disclosure", self.entity_id),
-                        self.preview_expanded,
-                    )
-                    .opened_icon(IconName::ChevronUp)
-                    .closed_icon(IconName::ChevronDown)
-                    .on_click(cx.listener(
-                        move |this, _event, _window, _cx| {
-                            this.preview_expanded = !this.preview_expanded;
-                        },
-                    )),
-                )
-            });
-
-        v_flex()
-            .mb_2()
-            .border_1()
-            .when(tool_failed || command_failed, |card| card.border_dashed())
-            .border_color(border_color)
-            .rounded_lg()
-            .overflow_hidden()
-            .child(
-                v_flex()
-                    .p_2()
-                    .gap_0p5()
-                    .bg(header_bg)
-                    .text_xs()
-                    .child(header)
-                    .child(
-                        MarkdownElement::new(
-                            self.input_command.clone(),
-                            markdown_style(window, cx),
-                        )
-                        .code_block_renderer(
-                            markdown::CodeBlockRenderer::Default {
-                                copy_button: false,
-                                copy_button_on_hover: true,
-                                border: false,
-                            },
-                        ),
-                    ),
-            )
-            .when(
-                self.preview_expanded && !self.finished_with_empty_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)
-                            .child({
-                                let content_mode = terminal.read(cx).content_mode(window, cx);
-
-                                if content_mode.is_scrollable() {
-                                    div().h_72().child(terminal.clone()).into_any_element()
-                                } else {
-                                    ToolOutputPreview::new(
-                                        terminal.clone().into_any_element(),
-                                        terminal.entity_id(),
-                                    )
-                                    .with_total_lines(self.content_line_count)
-                                    .toggle_state(!content_mode.is_limited())
-                                    .on_toggle({
-                                        let terminal = terminal.clone();
-                                        move |is_expanded, _, cx| {
-                                            terminal.update(cx, |terminal, cx| {
-                                                terminal.set_embedded_mode(
-                                                    if is_expanded {
-                                                        None
-                                                    } else {
-                                                        Some(COLLAPSED_LINES)
-                                                    },
-                                                    cx,
-                                                );
-                                            });
-                                        }
-                                    })
-                                    .into_any_element()
-                                }
-                            }),
-                    )
-                },
-            )
-            .into_any()
-    }
-}
-
-fn markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
-    let theme_settings = ThemeSettings::get_global(cx);
-    let buffer_font_size = TextSize::Default.rems(cx);
-    let mut text_style = window.text_style();
-
-    text_style.refine(&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()),
-        color: Some(cx.theme().colors().text),
-        ..Default::default()
-    });
-
-    MarkdownStyle {
-        base_text_style: text_style.clone(),
-        selection_background_color: cx.theme().colors().element_selection_background,
-        ..Default::default()
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use editor::EditorSettings;
-    use fs::RealFs;
-    use gpui::{BackgroundExecutor, TestAppContext};
-    use language_model::fake_provider::FakeLanguageModel;
-    use pretty_assertions::assert_eq;
-    use serde_json::json;
-    use settings::{Settings, SettingsStore};
-    use terminal::terminal_settings::TerminalSettings;
-    use util::{ResultExt as _, test::TempTree};
-
-    use super::*;
-
-    fn init_test(executor: &BackgroundExecutor, cx: &mut TestAppContext) {
-        zlog::init_test();
-
-        executor.allow_parking();
-        cx.update(|cx| {
-            let settings_store = SettingsStore::test(cx);
-            cx.set_global(settings_store);
-            language::init(cx);
-            Project::init_settings(cx);
-            workspace::init_settings(cx);
-            theme::init(theme::LoadThemes::JustBase, cx);
-            TerminalSettings::register(cx);
-            EditorSettings::register(cx);
-        });
-    }
-
-    #[gpui::test]
-    async fn test_interactive_command(executor: BackgroundExecutor, cx: &mut TestAppContext) {
-        if cfg!(windows) {
-            return;
-        }
-        init_test(&executor, cx);
-
-        let fs = Arc::new(RealFs::new(None, executor));
-        let tree = TempTree::new(json!({
-            "project": {},
-        }));
-        let project: Entity<Project> =
-            Project::test(fs, [tree.path().join("project").as_path()], cx).await;
-        let action_log = cx.update(|cx| cx.new(|_| ActionLog::new(project.clone())));
-        let model = Arc::new(FakeLanguageModel::default());
-
-        let input = TerminalToolInput {
-            command: "cat".to_owned(),
-            cd: tree
-                .path()
-                .join("project")
-                .as_path()
-                .to_string_lossy()
-                .to_string(),
-        };
-        let result = cx.update(|cx| {
-            TerminalTool::run(
-                Arc::new(TerminalTool),
-                serde_json::to_value(input).unwrap(),
-                Arc::default(),
-                project.clone(),
-                action_log.clone(),
-                model,
-                None,
-                cx,
-            )
-        });
-
-        let output = result.output.await.log_err().unwrap().content;
-        assert_eq!(output.as_str().unwrap(), "Command executed successfully.");
-    }
-
-    #[gpui::test]
-    async fn test_working_directory(executor: BackgroundExecutor, cx: &mut TestAppContext) {
-        if cfg!(windows) {
-            return;
-        }
-        init_test(&executor, cx);
-
-        let fs = Arc::new(RealFs::new(None, executor));
-        let tree = TempTree::new(json!({
-            "project": {},
-            "other-project": {},
-        }));
-        let project: Entity<Project> =
-            Project::test(fs, [tree.path().join("project").as_path()], cx).await;
-        let action_log = cx.update(|cx| cx.new(|_| ActionLog::new(project.clone())));
-        let model = Arc::new(FakeLanguageModel::default());
-
-        let check = |input, expected, cx: &mut App| {
-            let headless_result = TerminalTool::run(
-                Arc::new(TerminalTool),
-                serde_json::to_value(input).unwrap(),
-                Arc::default(),
-                project.clone(),
-                action_log.clone(),
-                model.clone(),
-                None,
-                cx,
-            );
-            cx.spawn(async move |_| {
-                let output = headless_result.output.await.map(|output| output.content);
-                assert_eq!(
-                    output
-                        .ok()
-                        .and_then(|content| content.as_str().map(ToString::to_string)),
-                    expected
-                );
-            })
-        };
-
-        cx.update(|cx| {
-            check(
-                TerminalToolInput {
-                    command: "pwd".into(),
-                    cd: ".".into(),
-                },
-                Some(format!(
-                    "```\n{}\n```",
-                    tree.path().join("project").display()
-                )),
-                cx,
-            )
-        })
-        .await;
-
-        cx.update(|cx| {
-            check(
-                TerminalToolInput {
-                    command: "pwd".into(),
-                    cd: "other-project".into(),
-                },
-                None, // other-project is a dir, but *not* a worktree (yet)
-                cx,
-            )
-        })
-        .await;
-
-        // Absolute path above the worktree root
-        cx.update(|cx| {
-            check(
-                TerminalToolInput {
-                    command: "pwd".into(),
-                    cd: tree.path().to_string_lossy().into(),
-                },
-                None,
-                cx,
-            )
-        })
-        .await;
-
-        project
-            .update(cx, |project, cx| {
-                project.create_worktree(tree.path().join("other-project"), true, cx)
-            })
-            .await
-            .unwrap();
-
-        cx.update(|cx| {
-            check(
-                TerminalToolInput {
-                    command: "pwd".into(),
-                    cd: "other-project".into(),
-                },
-                Some(format!(
-                    "```\n{}\n```",
-                    tree.path().join("other-project").display()
-                )),
-                cx,
-            )
-        })
-        .await;
-
-        cx.update(|cx| {
-            check(
-                TerminalToolInput {
-                    command: "pwd".into(),
-                    cd: ".".into(),
-                },
-                None,
-                cx,
-            )
-        })
-        .await;
-    }
-}

crates/assistant_tools/src/terminal_tool/description.md 🔗

@@ -1,11 +0,0 @@
-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.

crates/assistant_tools/src/thinking_tool.rs 🔗

@@ -1,69 +0,0 @@
-use std::sync::Arc;
-
-use crate::schema::json_schema_for;
-use action_log::ActionLog;
-use anyhow::{Result, anyhow};
-use assistant_tool::{Tool, ToolResult};
-use gpui::{AnyWindowHandle, App, Entity, Task};
-use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
-use project::Project;
-use schemars::JsonSchema;
-use serde::{Deserialize, Serialize};
-use ui::IconName;
-
-#[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 Tool for ThinkingTool {
-    fn name(&self) -> String {
-        "thinking".to_string()
-    }
-
-    fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
-        false
-    }
-
-    fn may_perform_edits(&self) -> bool {
-        false
-    }
-
-    fn description(&self) -> String {
-        include_str!("./thinking_tool/description.md").to_string()
-    }
-
-    fn icon(&self) -> IconName {
-        IconName::ToolThink
-    }
-
-    fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
-        json_schema_for::<ThinkingToolInput>(format)
-    }
-
-    fn ui_text(&self, _input: &serde_json::Value) -> String {
-        "Thinking".to_string()
-    }
-
-    fn run(
-        self: Arc<Self>,
-        input: serde_json::Value,
-        _request: Arc<LanguageModelRequest>,
-        _project: Entity<Project>,
-        _action_log: Entity<ActionLog>,
-        _model: Arc<dyn LanguageModel>,
-        _window: Option<AnyWindowHandle>,
-        _cx: &mut App,
-    ) -> ToolResult {
-        // This tool just "thinks out loud" and doesn't perform any actions.
-        Task::ready(match serde_json::from_value::<ThinkingToolInput>(input) {
-            Ok(_input) => Ok("Finished thinking.".to_string().into()),
-            Err(err) => Err(anyhow!(err)),
-        })
-        .into()
-    }
-}

crates/assistant_tools/src/thinking_tool/description.md 🔗

@@ -1 +0,0 @@
-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.

crates/assistant_tools/src/ui/tool_call_card_header.rs 🔗

@@ -1,131 +0,0 @@
-use gpui::{Animation, AnimationExt, AnyElement, App, IntoElement, pulsating_between};
-use std::time::Duration;
-use ui::{Tooltip, prelude::*};
-
-/// A reusable header component for tool call cards.
-#[derive(IntoElement)]
-pub struct ToolCallCardHeader {
-    icon: IconName,
-    primary_text: SharedString,
-    secondary_text: Option<SharedString>,
-    code_path: Option<SharedString>,
-    disclosure_slot: Option<AnyElement>,
-    is_loading: bool,
-    error: Option<String>,
-}
-
-impl ToolCallCardHeader {
-    pub fn new(icon: IconName, primary_text: impl Into<SharedString>) -> Self {
-        Self {
-            icon,
-            primary_text: primary_text.into(),
-            secondary_text: None,
-            code_path: None,
-            disclosure_slot: None,
-            is_loading: false,
-            error: None,
-        }
-    }
-
-    pub fn with_secondary_text(mut self, text: impl Into<SharedString>) -> Self {
-        self.secondary_text = Some(text.into());
-        self
-    }
-
-    pub fn with_code_path(mut self, text: impl Into<SharedString>) -> Self {
-        self.code_path = Some(text.into());
-        self
-    }
-
-    pub fn disclosure_slot(mut self, element: impl IntoElement) -> Self {
-        self.disclosure_slot = Some(element.into_any_element());
-        self
-    }
-
-    pub fn loading(mut self) -> Self {
-        self.is_loading = true;
-        self
-    }
-
-    pub fn with_error(mut self, error: impl Into<String>) -> Self {
-        self.error = Some(error.into());
-        self
-    }
-}
-
-impl RenderOnce for ToolCallCardHeader {
-    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
-        let font_size = rems(0.8125);
-        let line_height = window.line_height();
-
-        let secondary_text = self.secondary_text;
-        let code_path = self.code_path;
-
-        let bullet_divider = || {
-            div()
-                .size(px(3.))
-                .rounded_full()
-                .bg(cx.theme().colors().text)
-        };
-
-        h_flex()
-            .id("tool-label-container")
-            .gap_2()
-            .max_w_full()
-            .overflow_x_scroll()
-            .opacity(0.8)
-            .child(
-                h_flex()
-                    .h(line_height)
-                    .gap_1p5()
-                    .text_size(font_size)
-                    .child(
-                        h_flex().h(line_height).justify_center().child(
-                            Icon::new(self.icon)
-                                .size(IconSize::Small)
-                                .color(Color::Muted),
-                        ),
-                    )
-                    .map(|this| {
-                        if let Some(error) = &self.error {
-                            this.child(format!("{} failed", self.primary_text)).child(
-                                IconButton::new("error_info", IconName::Warning)
-                                    .shape(ui::IconButtonShape::Square)
-                                    .icon_size(IconSize::XSmall)
-                                    .icon_color(Color::Warning)
-                                    .tooltip(Tooltip::text(error.clone())),
-                            )
-                        } else {
-                            this.child(self.primary_text.clone())
-                        }
-                    })
-                    .when_some(secondary_text, |this, secondary_text| {
-                        this.child(bullet_divider())
-                            .child(div().text_size(font_size).child(secondary_text))
-                    })
-                    .when_some(code_path, |this, code_path| {
-                        this.child(bullet_divider())
-                            .child(Label::new(code_path).size(LabelSize::Small).inline_code(cx))
-                    })
-                    .with_animation(
-                        "loading-label",
-                        Animation::new(Duration::from_secs(2))
-                            .repeat()
-                            .with_easing(pulsating_between(0.6, 1.)),
-                        move |this, delta| {
-                            if self.is_loading {
-                                this.opacity(delta)
-                            } else {
-                                this
-                            }
-                        },
-                    ),
-            )
-            .when_some(self.disclosure_slot, |container, disclosure_slot| {
-                container
-                    .group("disclosure")
-                    .justify_between()
-                    .child(div().visible_on_hover("disclosure").child(disclosure_slot))
-            })
-    }
-}

crates/assistant_tools/src/ui/tool_output_preview.rs 🔗

@@ -1,115 +0,0 @@
-use gpui::{AnyElement, EntityId, prelude::*};
-use ui::{Tooltip, prelude::*};
-
-#[derive(IntoElement)]
-pub struct ToolOutputPreview<F>
-where
-    F: Fn(bool, &mut Window, &mut App) + 'static,
-{
-    content: AnyElement,
-    entity_id: EntityId,
-    full_height: bool,
-    total_lines: usize,
-    collapsed_fade: bool,
-    on_toggle: Option<F>,
-}
-
-pub const COLLAPSED_LINES: usize = 10;
-
-impl<F> ToolOutputPreview<F>
-where
-    F: Fn(bool, &mut Window, &mut App) + 'static,
-{
-    pub fn new(content: AnyElement, entity_id: EntityId) -> Self {
-        Self {
-            content,
-            entity_id,
-            full_height: true,
-            total_lines: 0,
-            collapsed_fade: false,
-            on_toggle: None,
-        }
-    }
-
-    pub fn with_total_lines(mut self, total_lines: usize) -> Self {
-        self.total_lines = total_lines;
-        self
-    }
-
-    pub fn toggle_state(mut self, full_height: bool) -> Self {
-        self.full_height = full_height;
-        self
-    }
-
-    pub fn with_collapsed_fade(mut self) -> Self {
-        self.collapsed_fade = true;
-        self
-    }
-
-    pub fn on_toggle(mut self, listener: F) -> Self {
-        self.on_toggle = Some(listener);
-        self
-    }
-}
-
-impl<F> RenderOnce for ToolOutputPreview<F>
-where
-    F: Fn(bool, &mut Window, &mut App) + 'static,
-{
-    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
-        if self.total_lines <= COLLAPSED_LINES {
-            return self.content;
-        }
-        let border_color = cx.theme().colors().border.opacity(0.6);
-
-        let (icon, tooltip_label) = if self.full_height {
-            (IconName::ChevronUp, "Collapse")
-        } else {
-            (IconName::ChevronDown, "Expand")
-        };
-
-        let gradient_overlay =
-            if self.collapsed_fade && !self.full_height {
-                Some(div().absolute().bottom_5().left_0().w_full().h_2_5().bg(
-                    gpui::linear_gradient(
-                        0.,
-                        gpui::linear_color_stop(cx.theme().colors().editor_background, 0.),
-                        gpui::linear_color_stop(
-                            cx.theme().colors().editor_background.opacity(0.),
-                            1.,
-                        ),
-                    ),
-                ))
-            } else {
-                None
-            };
-
-        v_flex()
-            .relative()
-            .child(self.content)
-            .children(gradient_overlay)
-            .child(
-                h_flex()
-                    .id(("expand-button", self.entity_id))
-                    .flex_none()
-                    .cursor_pointer()
-                    .h_5()
-                    .justify_center()
-                    .border_t_1()
-                    .rounded_b_md()
-                    .border_color(border_color)
-                    .bg(cx.theme().colors().editor_background)
-                    .hover(|style| style.bg(cx.theme().colors().element_hover.opacity(0.1)))
-                    .child(Icon::new(icon).size(IconSize::Small).color(Color::Muted))
-                    .tooltip(Tooltip::text(tooltip_label))
-                    .when_some(self.on_toggle, |this, on_toggle| {
-                        this.on_click({
-                            move |_, window, cx| {
-                                on_toggle(!self.full_height, window, cx);
-                            }
-                        })
-                    }),
-            )
-            .into_any()
-    }
-}

crates/assistant_tools/src/web_search_tool.rs 🔗

@@ -1,327 +0,0 @@
-use std::{sync::Arc, time::Duration};
-
-use crate::schema::json_schema_for;
-use crate::ui::ToolCallCardHeader;
-use action_log::ActionLog;
-use anyhow::{Context as _, Result, anyhow};
-use assistant_tool::{
-    Tool, ToolCard, ToolResult, ToolResultContent, ToolResultOutput, ToolUseStatus,
-};
-use cloud_llm_client::{WebSearchResponse, WebSearchResult};
-use futures::{Future, FutureExt, TryFutureExt};
-use gpui::{
-    AnyWindowHandle, App, AppContext, Context, Entity, IntoElement, Task, WeakEntity, Window,
-};
-use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
-use project::Project;
-use schemars::JsonSchema;
-use serde::{Deserialize, Serialize};
-use ui::{IconName, Tooltip, prelude::*};
-use web_search::WebSearchRegistry;
-use workspace::Workspace;
-
-#[derive(Debug, Serialize, Deserialize, JsonSchema)]
-pub struct WebSearchToolInput {
-    /// The search term or question to query on the web.
-    query: String,
-}
-
-pub struct WebSearchTool;
-
-impl Tool for WebSearchTool {
-    fn name(&self) -> String {
-        "web_search".into()
-    }
-
-    fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
-        false
-    }
-
-    fn may_perform_edits(&self) -> bool {
-        false
-    }
-
-    fn description(&self) -> String {
-        "Search the web for information using your query. Use this when you need real-time information, facts, or data that might not be in your training. Results will include snippets and links from relevant web pages.".into()
-    }
-
-    fn icon(&self) -> IconName {
-        IconName::ToolWeb
-    }
-
-    fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
-        json_schema_for::<WebSearchToolInput>(format)
-    }
-
-    fn ui_text(&self, _input: &serde_json::Value) -> String {
-        "Searching the Web".to_string()
-    }
-
-    fn run(
-        self: Arc<Self>,
-        input: serde_json::Value,
-        _request: Arc<LanguageModelRequest>,
-        _project: Entity<Project>,
-        _action_log: Entity<ActionLog>,
-        _model: Arc<dyn LanguageModel>,
-        _window: Option<AnyWindowHandle>,
-        cx: &mut App,
-    ) -> ToolResult {
-        let input = match serde_json::from_value::<WebSearchToolInput>(input) {
-            Ok(input) => input,
-            Err(err) => return Task::ready(Err(anyhow!(err))).into(),
-        };
-        let Some(provider) = WebSearchRegistry::read_global(cx).active_provider() else {
-            return Task::ready(Err(anyhow!("Web search is not available."))).into();
-        };
-
-        let search_task = provider.search(input.query, cx).map_err(Arc::new).shared();
-        let output = cx.background_spawn({
-            let search_task = search_task.clone();
-            async move {
-                let response = search_task.await.map_err(|err| anyhow!(err))?;
-                Ok(ToolResultOutput {
-                    content: ToolResultContent::Text(
-                        serde_json::to_string(&response)
-                            .context("Failed to serialize search results")?,
-                    ),
-                    output: Some(serde_json::to_value(response)?),
-                })
-            }
-        });
-
-        ToolResult {
-            output,
-            card: Some(cx.new(|cx| WebSearchToolCard::new(search_task, cx)).into()),
-        }
-    }
-
-    fn deserialize_card(
-        self: Arc<Self>,
-        output: serde_json::Value,
-        _project: Entity<Project>,
-        _window: &mut Window,
-        cx: &mut App,
-    ) -> Option<assistant_tool::AnyToolCard> {
-        let output = serde_json::from_value::<WebSearchResponse>(output).ok()?;
-        let card = cx.new(|cx| WebSearchToolCard::new(Task::ready(Ok(output)), cx));
-        Some(card.into())
-    }
-}
-
-#[derive(RegisterComponent)]
-struct WebSearchToolCard {
-    response: Option<Result<WebSearchResponse>>,
-    _task: Task<()>,
-}
-
-impl WebSearchToolCard {
-    fn new(
-        search_task: impl 'static + Future<Output = Result<WebSearchResponse, Arc<anyhow::Error>>>,
-        cx: &mut Context<Self>,
-    ) -> Self {
-        let _task = cx.spawn(async move |this, cx| {
-            let response = search_task.await.map_err(|err| anyhow!(err));
-            this.update(cx, |this, cx| {
-                this.response = Some(response);
-                cx.notify();
-            })
-            .ok();
-        });
-
-        Self {
-            response: None,
-            _task,
-        }
-    }
-}
-
-impl ToolCard for WebSearchToolCard {
-    fn render(
-        &mut self,
-        _status: &ToolUseStatus,
-        _window: &mut Window,
-        _workspace: WeakEntity<Workspace>,
-        cx: &mut Context<Self>,
-    ) -> impl IntoElement {
-        let icon = IconName::ToolWeb;
-
-        let header = match self.response.as_ref() {
-            Some(Ok(response)) => {
-                let text: SharedString = if response.results.len() == 1 {
-                    "1 result".into()
-                } else {
-                    format!("{} results", response.results.len()).into()
-                };
-                ToolCallCardHeader::new(icon, "Searched the Web").with_secondary_text(text)
-            }
-            Some(Err(error)) => {
-                ToolCallCardHeader::new(icon, "Web Search").with_error(error.to_string())
-            }
-            None => ToolCallCardHeader::new(icon, "Searching the Web").loading(),
-        };
-
-        let content = self.response.as_ref().and_then(|response| match response {
-            Ok(response) => Some(
-                v_flex()
-                    .overflow_hidden()
-                    .ml_1p5()
-                    .pl(px(5.))
-                    .border_l_1()
-                    .border_color(cx.theme().colors().border_variant)
-                    .gap_1()
-                    .children(response.results.iter().enumerate().map(|(index, result)| {
-                        let title = result.title.clone();
-                        let url = SharedString::from(result.url.clone());
-
-                        Button::new(("result", index), title)
-                            .label_size(LabelSize::Small)
-                            .color(Color::Muted)
-                            .icon(IconName::ArrowUpRight)
-                            .icon_size(IconSize::Small)
-                            .icon_position(IconPosition::End)
-                            .truncate(true)
-                            .tooltip({
-                                let url = url.clone();
-                                move |window, cx| {
-                                    Tooltip::with_meta(
-                                        "Web Search Result",
-                                        None,
-                                        url.clone(),
-                                        window,
-                                        cx,
-                                    )
-                                }
-                            })
-                            .on_click(move |_, _, cx| cx.open_url(&url))
-                    }))
-                    .into_any(),
-            ),
-            Err(_) => None,
-        });
-
-        v_flex().mb_3().gap_1().child(header).children(content)
-    }
-}
-
-impl Component for WebSearchToolCard {
-    fn scope() -> ComponentScope {
-        ComponentScope::Agent
-    }
-
-    fn preview(window: &mut Window, cx: &mut App) -> Option<AnyElement> {
-        let in_progress_search = cx.new(|cx| WebSearchToolCard {
-            response: None,
-            _task: cx.spawn(async move |_this, cx| {
-                loop {
-                    cx.background_executor()
-                        .timer(Duration::from_secs(60))
-                        .await
-                }
-            }),
-        });
-
-        let successful_search = cx.new(|_cx| WebSearchToolCard {
-            response: Some(Ok(example_search_response())),
-            _task: Task::ready(()),
-        });
-
-        let error_search = cx.new(|_cx| WebSearchToolCard {
-            response: Some(Err(anyhow!("Failed to resolve https://google.com"))),
-            _task: Task::ready(()),
-        });
-
-        Some(
-            v_flex()
-                .gap_6()
-                .children(vec![example_group(vec![
-                    single_example(
-                        "In Progress",
-                        div()
-                            .size_full()
-                            .child(in_progress_search.update(cx, |tool, cx| {
-                                tool.render(
-                                    &ToolUseStatus::Pending,
-                                    window,
-                                    WeakEntity::new_invalid(),
-                                    cx,
-                                )
-                                .into_any_element()
-                            }))
-                            .into_any_element(),
-                    ),
-                    single_example(
-                        "Successful",
-                        div()
-                            .size_full()
-                            .child(successful_search.update(cx, |tool, cx| {
-                                tool.render(
-                                    &ToolUseStatus::Finished("".into()),
-                                    window,
-                                    WeakEntity::new_invalid(),
-                                    cx,
-                                )
-                                .into_any_element()
-                            }))
-                            .into_any_element(),
-                    ),
-                    single_example(
-                        "Error",
-                        div()
-                            .size_full()
-                            .child(error_search.update(cx, |tool, cx| {
-                                tool.render(
-                                    &ToolUseStatus::Error("".into()),
-                                    window,
-                                    WeakEntity::new_invalid(),
-                                    cx,
-                                )
-                                .into_any_element()
-                            }))
-                            .into_any_element(),
-                    ),
-                ])])
-                .into_any_element(),
-        )
-    }
-}
-
-fn example_search_response() -> WebSearchResponse {
-    WebSearchResponse {
-        results: vec![
-            WebSearchResult {
-                title: "Alo".to_string(),
-                url: "https://www.google.com/maps/search/Alo%2C+Toronto%2C+Canada".to_string(),
-                text: "Alo is a popular restaurant in Toronto.".to_string(),
-            },
-            WebSearchResult {
-                title: "Alo".to_string(),
-                url: "https://www.google.com/maps/search/Alo%2C+Toronto%2C+Canada".to_string(),
-                text: "Information about Alo restaurant in Toronto.".to_string(),
-            },
-            WebSearchResult {
-                title: "Edulis".to_string(),
-                url: "https://www.google.com/maps/search/Edulis%2C+Toronto%2C+Canada".to_string(),
-                text: "Details about Edulis restaurant in Toronto.".to_string(),
-            },
-            WebSearchResult {
-                title: "Sushi Masaki Saito".to_string(),
-                url: "https://www.google.com/maps/search/Sushi+Masaki+Saito%2C+Toronto%2C+Canada"
-                    .to_string(),
-                text: "Information about Sushi Masaki Saito in Toronto.".to_string(),
-            },
-            WebSearchResult {
-                title: "Shoushin".to_string(),
-                url: "https://www.google.com/maps/search/Shoushin%2C+Toronto%2C+Canada".to_string(),
-                text: "Details about Shoushin restaurant in Toronto.".to_string(),
-            },
-            WebSearchResult {
-                title: "Restaurant 20 Victoria".to_string(),
-                url:
-                    "https://www.google.com/maps/search/Restaurant+20+Victoria%2C+Toronto%2C+Canada"
-                        .to_string(),
-                text: "Information about Restaurant 20 Victoria in Toronto.".to_string(),
-            },
-        ],
-    }
-}

crates/audio/Cargo.toml 🔗

@@ -21,13 +21,12 @@ gpui.workspace = true
 denoise = { path = "../denoise" }
 log.workspace = true
 parking_lot.workspace = true
-rodio = { workspace = true, features = [ "wav", "playback", "wav_output" ] }
+rodio.workspace = true
 serde.workspace = true
 settings.workspace = true
 smol.workspace = true
 thiserror.workspace = true
 util.workspace = true
-workspace-hack.workspace = true
 
 [target.'cfg(not(any(all(target_os = "windows", target_env = "gnu"), target_os = "freebsd")))'.dependencies]
 libwebrtc = { rev = "5f04705ac3f356350ae31534ffbc476abc9ea83d", git = "https://github.com/zed-industries/livekit-rust-sdks" }

crates/audio/src/rodio_ext.rs 🔗

@@ -433,7 +433,7 @@ where
     /// Stores already emitted samples, once its full we call the callback.
     buffer: [Sample; N],
     /// Next free element in buffer. If this is equal to the buffer length
-    /// we have no more free lements.
+    /// we have no more free elements.
     free: usize,
 }
 

crates/auto_update/Cargo.toml 🔗

@@ -27,7 +27,6 @@ settings.workspace = true
 smol.workspace = true
 tempfile.workspace = true
 workspace.workspace = true
-workspace-hack.workspace = true
 
 [target.'cfg(not(target_os = "windows"))'.dependencies]
 which.workspace = true

crates/auto_update_helper/Cargo.toml 🔗

@@ -17,7 +17,6 @@ doctest = false
 anyhow.workspace = true
 log.workspace = true
 simplelog.workspace = true
-workspace-hack.workspace = true
 
 [target.'cfg(target_os = "windows")'.dependencies]
 windows.workspace = true

crates/auto_update_ui/Cargo.toml 🔗

@@ -25,4 +25,3 @@ serde_json.workspace = true
 smol.workspace = true
 util.workspace = true
 workspace.workspace = true
-workspace-hack.workspace = true

crates/aws_http_client/Cargo.toml 🔗

@@ -18,4 +18,3 @@ default = []
 aws-smithy-runtime-api.workspace = true
 aws-smithy-types.workspace = true
 http_client.workspace = true
-workspace-hack.workspace = true

crates/bedrock/Cargo.toml 🔗

@@ -25,4 +25,3 @@ serde.workspace = true
 serde_json.workspace = true
 strum.workspace = true
 thiserror.workspace = true
-workspace-hack.workspace = true

crates/breadcrumbs/Cargo.toml 🔗

@@ -21,7 +21,6 @@ theme.workspace = true
 ui.workspace = true
 workspace.workspace = true
 zed_actions.workspace = true
-workspace-hack.workspace = true
 
 [dev-dependencies]
 editor = { workspace = true, features = ["test-support"] }

crates/breadcrumbs/src/breadcrumbs.rs 🔗

@@ -119,21 +119,19 @@ impl Render for Breadcrumbs {
                             }
                         }
                     })
-                    .tooltip(move |window, cx| {
+                    .tooltip(move |_window, cx| {
                         if let Some(editor) = editor.upgrade() {
                             let focus_handle = editor.read(cx).focus_handle(cx);
                             Tooltip::for_action_in(
                                 "Show Symbol Outline",
                                 &zed_actions::outline::ToggleOutline,
                                 &focus_handle,
-                                window,
                                 cx,
                             )
                         } else {
                             Tooltip::for_action(
                                 "Show Symbol Outline",
                                 &zed_actions::outline::ToggleOutline,
-                                window,
                                 cx,
                             )
                         }

crates/buffer_diff/Cargo.toml 🔗

@@ -27,7 +27,6 @@ rope.workspace = true
 sum_tree.workspace = true
 text.workspace = true
 util.workspace = true
-workspace-hack.workspace = true
 
 [dev-dependencies]
 ctor.workspace = true

crates/buffer_diff/src/buffer_diff.rs 🔗

@@ -85,7 +85,7 @@ struct PendingHunk {
     new_status: DiffHunkSecondaryStatus,
 }
 
-#[derive(Debug, Default, Clone)]
+#[derive(Debug, Clone)]
 pub struct DiffHunkSummary {
     buffer_range: Range<Anchor>,
 }
@@ -114,15 +114,17 @@ impl sum_tree::Summary for DiffHunkSummary {
     type Context<'a> = &'a text::BufferSnapshot;
 
     fn zero(_cx: Self::Context<'_>) -> Self {
-        Default::default()
+        DiffHunkSummary {
+            buffer_range: Anchor::MIN..Anchor::MIN,
+        }
     }
 
     fn add_summary(&mut self, other: &Self, buffer: Self::Context<'_>) {
-        self.buffer_range.start = self
+        self.buffer_range.start = *self
             .buffer_range
             .start
             .min(&other.buffer_range.start, buffer);
-        self.buffer_range.end = self.buffer_range.end.max(&other.buffer_range.end, buffer);
+        self.buffer_range.end = *self.buffer_range.end.max(&other.buffer_range.end, buffer);
     }
 }
 
@@ -937,7 +939,9 @@ impl BufferDiff {
 
     pub fn clear_pending_hunks(&mut self, cx: &mut Context<Self>) {
         if self.secondary_diff.is_some() {
-            self.inner.pending_hunks = SumTree::from_summary(DiffHunkSummary::default());
+            self.inner.pending_hunks = SumTree::from_summary(DiffHunkSummary {
+                buffer_range: Anchor::MIN..Anchor::MIN,
+            });
             cx.emit(BufferDiffEvent::DiffChanged {
                 changed_range: Some(Anchor::MIN..Anchor::MAX),
             });
@@ -1068,8 +1072,8 @@ impl BufferDiff {
                 self.range_to_hunk_range(secondary_changed_range, buffer, cx)
         {
             if let Some(range) = &mut changed_range {
-                range.start = secondary_hunk_range.start.min(&range.start, buffer);
-                range.end = secondary_hunk_range.end.max(&range.end, buffer);
+                range.start = *secondary_hunk_range.start.min(&range.start, buffer);
+                range.end = *secondary_hunk_range.end.max(&range.end, buffer);
             } else {
                 changed_range = Some(secondary_hunk_range);
             }
@@ -1083,8 +1087,8 @@ impl BufferDiff {
             if let Some((first, last)) = state.pending_hunks.first().zip(state.pending_hunks.last())
             {
                 if let Some(range) = &mut changed_range {
-                    range.start = range.start.min(&first.buffer_range.start, buffer);
-                    range.end = range.end.max(&last.buffer_range.end, buffer);
+                    range.start = *range.start.min(&first.buffer_range.start, buffer);
+                    range.end = *range.end.max(&last.buffer_range.end, buffer);
                 } else {
                     changed_range = Some(first.buffer_range.start..last.buffer_range.end);
                 }
@@ -1368,7 +1372,7 @@ mod tests {
     use gpui::TestAppContext;
     use pretty_assertions::{assert_eq, assert_ne};
     use rand::{Rng as _, rngs::StdRng};
-    use text::{Buffer, BufferId, Rope};
+    use text::{Buffer, BufferId, ReplicaId, Rope};
     use unindent::Unindent as _;
     use util::test::marked_text_ranges;
 
@@ -1393,7 +1397,7 @@ mod tests {
         "
         .unindent();
 
-        let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text);
+        let mut buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text);
         let mut diff = BufferDiffSnapshot::new_sync(buffer.clone(), diff_base.clone(), cx);
         assert_hunks(
             diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer),
@@ -1467,7 +1471,7 @@ mod tests {
         "
         .unindent();
 
-        let buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text);
+        let buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text);
         let unstaged_diff = BufferDiffSnapshot::new_sync(buffer.clone(), index_text, cx);
         let mut uncommitted_diff =
             BufferDiffSnapshot::new_sync(buffer.clone(), head_text.clone(), cx);
@@ -1536,7 +1540,7 @@ mod tests {
         "
         .unindent();
 
-        let buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text);
+        let buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text);
         let diff = cx
             .update(|cx| {
                 BufferDiffSnapshot::new_with_base_text(
@@ -1799,7 +1803,7 @@ mod tests {
 
         for example in table {
             let (buffer_text, ranges) = marked_text_ranges(&example.buffer_marked_text, false);
-            let buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text);
+            let buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text);
             let hunk_range =
                 buffer.anchor_before(ranges[0].start)..buffer.anchor_before(ranges[0].end);
 
@@ -1872,7 +1876,11 @@ mod tests {
         "
         .unindent();
 
-        let buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text.clone());
+        let buffer = Buffer::new(
+            ReplicaId::LOCAL,
+            BufferId::new(1).unwrap(),
+            buffer_text.clone(),
+        );
         let unstaged = BufferDiffSnapshot::new_sync(buffer.clone(), index_text, cx);
         let uncommitted = BufferDiffSnapshot::new_sync(buffer.clone(), head_text.clone(), cx);
         let unstaged_diff = cx.new(|cx| {
@@ -1945,7 +1953,7 @@ mod tests {
         "
         .unindent();
 
-        let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text_1);
+        let mut buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text_1);
 
         let empty_diff = cx.update(|cx| BufferDiffSnapshot::empty(&buffer, cx));
         let diff_1 = BufferDiffSnapshot::new_sync(buffer.clone(), base_text.clone(), cx);

crates/call/Cargo.toml 🔗

@@ -41,7 +41,6 @@ telemetry.workspace = true
 util.workspace = true
 gpui_tokio.workspace = true
 livekit_client.workspace = true
-workspace-hack.workspace = true
 
 [dev-dependencies]
 client = { workspace = true, features = ["test-support"] }

crates/channel/Cargo.toml 🔗

@@ -31,7 +31,6 @@ settings.workspace = true
 text.workspace = true
 time.workspace = true
 util.workspace = true
-workspace-hack.workspace = true
 
 [dev-dependencies]
 collections = { workspace = true, features = ["test-support"] }

crates/channel/src/channel_buffer.rs 🔗

@@ -9,7 +9,7 @@ use rpc::{
     proto::{self, PeerId},
 };
 use std::{sync::Arc, time::Duration};
-use text::BufferId;
+use text::{BufferId, ReplicaId};
 use util::ResultExt;
 
 pub const ACKNOWLEDGE_DEBOUNCE_INTERVAL: Duration = Duration::from_millis(250);
@@ -65,7 +65,12 @@ impl ChannelBuffer {
 
         let buffer = cx.new(|cx| {
             let capability = channel_store.read(cx).channel_capability(channel.id);
-            language::Buffer::remote(buffer_id, response.replica_id as u16, capability, base_text)
+            language::Buffer::remote(
+                buffer_id,
+                ReplicaId::new(response.replica_id as u16),
+                capability,
+                base_text,
+            )
         })?;
         buffer.update(cx, |buffer, cx| buffer.apply_ops(operations, cx))?;
 
@@ -272,7 +277,7 @@ impl ChannelBuffer {
         self.connected
     }
 
-    pub fn replica_id(&self, cx: &App) -> u16 {
+    pub fn replica_id(&self, cx: &App) -> ReplicaId {
         self.buffer.read(cx).replica_id()
     }
 }

crates/cli/Cargo.toml 🔗

@@ -32,7 +32,6 @@ release_channel.workspace = true
 serde.workspace = true
 util.workspace = true
 tempfile.workspace = true
-workspace-hack.workspace = true
 
 [target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies]
 exec.workspace = true

crates/cli/src/cli.rs 🔗

@@ -17,6 +17,7 @@ pub enum CliRequest {
         wsl: Option<String>,
         wait: bool,
         open_new_workspace: Option<bool>,
+        reuse: bool,
         env: Option<HashMap<String, String>>,
         user_data_dir: Option<String>,
     },

crates/cli/src/main.rs 🔗

@@ -62,11 +62,14 @@ struct Args {
     #[arg(short, long)]
     wait: bool,
     /// Add files to the currently open workspace
-    #[arg(short, long, overrides_with = "new")]
+    #[arg(short, long, overrides_with_all = ["new", "reuse"])]
     add: bool,
     /// Create a new workspace
-    #[arg(short, long, overrides_with = "add")]
+    #[arg(short, long, overrides_with_all = ["add", "reuse"])]
     new: bool,
+    /// Reuse an existing window, replacing its workspace
+    #[arg(short, long, overrides_with_all = ["add", "new"])]
+    reuse: bool,
     /// Sets a custom directory for all user data (e.g., database, extensions, logs).
     /// This overrides the default platform-specific data directory location:
     #[cfg_attr(target_os = "macos", doc = "`~/Library/Application Support/Zed`.")]
@@ -374,6 +377,7 @@ fn main() -> Result<()> {
                     wsl,
                     wait: args.wait,
                     open_new_workspace,
+                    reuse: args.reuse,
                     env,
                     user_data_dir: user_data_dir_for_thread,
                 })?;

crates/client/Cargo.toml 🔗

@@ -57,7 +57,6 @@ tokio-socks = { version = "0.5.2", default-features = false, features = ["future
 tokio.workspace = true
 url.workspace = true
 util.workspace = true
-workspace-hack.workspace = true
 worktree.workspace = true
 
 [dev-dependencies]

crates/client/src/client.rs 🔗

@@ -138,10 +138,6 @@ impl Settings for ProxySettings {
             proxy: content.proxy.clone(),
         }
     }
-
-    fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut SettingsContent) {
-        vscode.string_setting("http.proxy", &mut current.proxy);
-    }
 }
 
 pub fn init_settings(cx: &mut App) {
@@ -525,27 +521,6 @@ impl settings::Settings for TelemetrySettings {
             metrics: content.telemetry.as_ref().unwrap().metrics.unwrap(),
         }
     }
-
-    fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut SettingsContent) {
-        let mut telemetry = settings::TelemetrySettingsContent::default();
-        vscode.enum_setting("telemetry.telemetryLevel", &mut telemetry.metrics, |s| {
-            Some(s == "all")
-        });
-        vscode.enum_setting(
-            "telemetry.telemetryLevel",
-            &mut telemetry.diagnostics,
-            |s| Some(matches!(s, "all" | "error" | "crash")),
-        );
-        // we could translate telemetry.telemetryLevel, but just because users didn't want
-        // to send microsoft telemetry doesn't mean they don't want to send it to zed. their
-        // all/error/crash/off correspond to combinations of our "diagnostics" and "metrics".
-        if let Some(diagnostics) = telemetry.diagnostics {
-            current.telemetry.get_or_insert_default().diagnostics = Some(diagnostics)
-        }
-        if let Some(metrics) = telemetry.metrics {
-            current.telemetry.get_or_insert_default().metrics = Some(metrics)
-        }
-    }
 }
 
 impl Client {

crates/client/src/proxy/socks_proxy.rs 🔗

@@ -23,7 +23,7 @@ pub(super) struct Socks5Authorization<'a> {
 
 /// Socks Proxy Protocol Version
 ///
-/// V4 allows idenfication using a user_id
+/// V4 allows identification using a user_id
 /// V5 allows authorization using a username and password
 pub(super) enum SocksVersion<'a> {
     V4 {

crates/client/src/user.rs 🔗

@@ -943,7 +943,7 @@ impl Collaborator {
     pub fn from_proto(message: proto::Collaborator) -> Result<Self> {
         Ok(Self {
             peer_id: message.peer_id.context("invalid peer id")?,
-            replica_id: message.replica_id as ReplicaId,
+            replica_id: ReplicaId::new(message.replica_id as u16),
             user_id: message.user_id as UserId,
             is_host: message.is_host,
             committer_name: message.committer_name,

crates/clock/Cargo.toml 🔗

@@ -19,4 +19,3 @@ test-support = ["dep:parking_lot"]
 parking_lot = { workspace = true, optional = true }
 serde.workspace = true
 smallvec.workspace = true
-workspace-hack.workspace = true

crates/clock/src/clock.rs 🔗

@@ -4,33 +4,73 @@ use serde::{Deserialize, Serialize};
 use smallvec::SmallVec;
 use std::{
     cmp::{self, Ordering},
-    fmt, iter,
+    fmt,
 };
 
 pub use system_clock::*;
 
-pub const LOCAL_BRANCH_REPLICA_ID: u16 = u16::MAX;
-pub const AGENT_REPLICA_ID: u16 = u16::MAX - 1;
-
 /// A unique identifier for each distributed node.
-pub type ReplicaId = u16;
+#[derive(Clone, Copy, Default, Eq, Hash, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
+pub struct ReplicaId(u16);
+
+impl ReplicaId {
+    /// The local replica
+    pub const LOCAL: ReplicaId = ReplicaId(0);
+    /// The remote replica of the connected remote server.
+    pub const REMOTE_SERVER: ReplicaId = ReplicaId(1);
+    /// The agent's unique identifier.
+    pub const AGENT: ReplicaId = ReplicaId(2);
+    /// A local branch.
+    pub const LOCAL_BRANCH: ReplicaId = ReplicaId(3);
+    /// The first collaborative replica ID, any replica equal or greater than this is a collaborative replica.
+    pub const FIRST_COLLAB_ID: ReplicaId = ReplicaId(8);
+
+    pub fn new(id: u16) -> Self {
+        ReplicaId(id)
+    }
+
+    pub fn as_u16(&self) -> u16 {
+        self.0
+    }
+
+    pub fn is_remote(self) -> bool {
+        self == ReplicaId::REMOTE_SERVER || self >= ReplicaId::FIRST_COLLAB_ID
+    }
+}
+
+impl fmt::Debug for ReplicaId {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        if *self == ReplicaId::LOCAL {
+            write!(f, "<local>")
+        } else if *self == ReplicaId::REMOTE_SERVER {
+            write!(f, "<remote>")
+        } else if *self == ReplicaId::AGENT {
+            write!(f, "<agent>")
+        } else if *self == ReplicaId::LOCAL_BRANCH {
+            write!(f, "<branch>")
+        } else {
+            write!(f, "{}", self.0)
+        }
+    }
+}
 
 /// A [Lamport sequence number](https://en.wikipedia.org/wiki/Lamport_timestamp).
 pub type Seq = u32;
 
 /// A [Lamport timestamp](https://en.wikipedia.org/wiki/Lamport_timestamp),
 /// used to determine the ordering of events in the editor.
-#[derive(Clone, Copy, Default, Eq, Hash, PartialEq, Serialize, Deserialize)]
+#[derive(Clone, Copy, Eq, Hash, PartialEq, Serialize, Deserialize)]
 pub struct Lamport {
     pub replica_id: ReplicaId,
     pub value: Seq,
 }
 
-/// A [vector clock](https://en.wikipedia.org/wiki/Vector_clock).
+/// A [version vector](https://en.wikipedia.org/wiki/Version_vector).
 #[derive(Clone, Default, Hash, Eq, PartialEq)]
 pub struct Global {
-    values: SmallVec<[u32; 8]>,
-    local_branch_value: u32,
+    // 4 is chosen as it is the biggest count that does not increase the size of the field itself.
+    // Coincidentally, it also covers all the important non-collab replica ids.
+    values: SmallVec<[u32; 4]>,
 }
 
 impl Global {
@@ -38,30 +78,31 @@ impl Global {
         Self::default()
     }
 
+    /// Fetches the sequence number for the given replica ID.
     pub fn get(&self, replica_id: ReplicaId) -> Seq {
-        if replica_id == LOCAL_BRANCH_REPLICA_ID {
-            self.local_branch_value
-        } else {
-            self.values.get(replica_id as usize).copied().unwrap_or(0) as Seq
-        }
+        self.values.get(replica_id.0 as usize).copied().unwrap_or(0) as Seq
     }
 
+    /// Observe the lamport timestamp.
+    ///
+    /// This sets the current sequence number of the observed replica ID to the maximum of this global's observed sequence and the observed timestamp.
     pub fn observe(&mut self, timestamp: Lamport) {
+        debug_assert_ne!(timestamp.replica_id, Lamport::MAX.replica_id);
         if timestamp.value > 0 {
-            if timestamp.replica_id == LOCAL_BRANCH_REPLICA_ID {
-                self.local_branch_value = cmp::max(self.local_branch_value, timestamp.value);
-            } else {
-                let new_len = timestamp.replica_id as usize + 1;
-                if new_len > self.values.len() {
-                    self.values.resize(new_len, 0);
-                }
-
-                let entry = &mut self.values[timestamp.replica_id as usize];
-                *entry = cmp::max(*entry, timestamp.value);
+            let new_len = timestamp.replica_id.0 as usize + 1;
+            if new_len > self.values.len() {
+                self.values.resize(new_len, 0);
             }
+
+            let entry = &mut self.values[timestamp.replica_id.0 as usize];
+            *entry = cmp::max(*entry, timestamp.value);
         }
     }
 
+    /// Join another global.
+    ///
+    /// This observes all timestamps from the other global.
+    #[doc(alias = "synchronize")]
     pub fn join(&mut self, other: &Self) {
         if other.values.len() > self.values.len() {
             self.values.resize(other.values.len(), 0);
@@ -70,34 +111,36 @@ impl Global {
         for (left, right) in self.values.iter_mut().zip(&other.values) {
             *left = cmp::max(*left, *right);
         }
-
-        self.local_branch_value = cmp::max(self.local_branch_value, other.local_branch_value);
     }
 
+    /// Meet another global.
+    ///
+    /// Sets all unobserved timestamps of this global to the sequences of other and sets all observed timestamps of this global to the minimum observed of both globals.
     pub fn meet(&mut self, other: &Self) {
         if other.values.len() > self.values.len() {
             self.values.resize(other.values.len(), 0);
         }
 
         let mut new_len = 0;
-        for (ix, (left, right)) in self
-            .values
-            .iter_mut()
-            .zip(other.values.iter().chain(iter::repeat(&0)))
-            .enumerate()
-        {
-            if *left == 0 {
-                *left = *right;
-            } else if *right > 0 {
-                *left = cmp::min(*left, *right);
+        for (ix, (left, &right)) in self.values.iter_mut().zip(&other.values).enumerate() {
+            match (*left, right) {
+                // left has not observed the replica
+                (0, _) => *left = right,
+                // right has not observed the replica
+                (_, 0) => (),
+                (_, _) => *left = cmp::min(*left, right),
             }
-
             if *left != 0 {
                 new_len = ix + 1;
             }
         }
-        self.values.resize(new_len, 0);
-        self.local_branch_value = cmp::min(self.local_branch_value, other.local_branch_value);
+        if other.values.len() == self.values.len() {
+            // only truncate if other was equal or shorter (which at this point
+            // cant be due to the resize above) to `self` as otherwise we would
+            // truncate the unprocessed tail that is guaranteed to contain
+            // non-null timestamps
+            self.values.truncate(new_len);
+        }
     }
 
     pub fn observed(&self, timestamp: Lamport) -> bool {
@@ -105,20 +148,18 @@ impl Global {
     }
 
     pub fn observed_any(&self, other: &Self) -> bool {
-        self.values
-            .iter()
-            .zip(other.values.iter())
-            .any(|(left, right)| *right > 0 && left >= right)
-            || (other.local_branch_value > 0 && self.local_branch_value >= other.local_branch_value)
+        self.iter()
+            .zip(other.iter())
+            .any(|(left, right)| right.value > 0 && left.value >= right.value)
     }
 
     pub fn observed_all(&self, other: &Self) -> bool {
-        let mut rhs = other.values.iter();
-        self.values.iter().all(|left| match rhs.next() {
-            Some(right) => left >= right,
-            None => true,
-        }) && rhs.next().is_none()
-            && self.local_branch_value >= other.local_branch_value
+        if self.values.len() < other.values.len() {
+            return false;
+        }
+        self.iter()
+            .zip(other.iter())
+            .all(|(left, right)| left.value >= right.value)
     }
 
     pub fn changed_since(&self, other: &Self) -> bool {
@@ -128,21 +169,21 @@ impl Global {
                 .iter()
                 .zip(other.values.iter())
                 .any(|(left, right)| left > right)
-            || self.local_branch_value > other.local_branch_value
     }
 
+    pub fn most_recent(&self) -> Option<Lamport> {
+        self.iter().max_by_key(|timestamp| timestamp.value)
+    }
+
+    /// Iterates all replicas observed by this global as well as any unobserved replicas whose ID is lower than the highest observed replica.
     pub fn iter(&self) -> impl Iterator<Item = Lamport> + '_ {
         self.values
             .iter()
             .enumerate()
             .map(|(replica_id, seq)| Lamport {
-                replica_id: replica_id as ReplicaId,
+                replica_id: ReplicaId(replica_id as u16),
                 value: *seq,
             })
-            .chain((self.local_branch_value > 0).then_some(Lamport {
-                replica_id: LOCAL_BRANCH_REPLICA_ID,
-                value: self.local_branch_value,
-            }))
     }
 }
 
@@ -173,12 +214,12 @@ impl PartialOrd for Lamport {
 
 impl Lamport {
     pub const MIN: Self = Self {
-        replica_id: ReplicaId::MIN,
+        replica_id: ReplicaId(u16::MIN),
         value: Seq::MIN,
     };
 
     pub const MAX: Self = Self {
-        replica_id: ReplicaId::MAX,
+        replica_id: ReplicaId(u16::MAX),
         value: Seq::MAX,
     };
 
@@ -190,7 +231,7 @@ impl Lamport {
     }
 
     pub fn as_u64(self) -> u64 {
-        ((self.value as u64) << 32) | (self.replica_id as u64)
+        ((self.value as u64) << 32) | (self.replica_id.0 as u64)
     }
 
     pub fn tick(&mut self) -> Self {
@@ -206,7 +247,13 @@ impl Lamport {
 
 impl fmt::Debug for Lamport {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        write!(f, "Lamport {{{}: {}}}", self.replica_id, self.value)
+        if *self == Self::MAX {
+            write!(f, "Lamport {{MAX}}")
+        } else if *self == Self::MIN {
+            write!(f, "Lamport {{MIN}}")
+        } else {
+            write!(f, "Lamport {{{:?}: {}}}", self.replica_id, self.value)
+        }
     }
 }
 
@@ -214,14 +261,10 @@ impl fmt::Debug for Global {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         write!(f, "Global {{")?;
         for timestamp in self.iter() {
-            if timestamp.replica_id > 0 {
+            if timestamp.replica_id.0 > 0 {
                 write!(f, ", ")?;
             }
-            if timestamp.replica_id == LOCAL_BRANCH_REPLICA_ID {
-                write!(f, "<branch>: {}", timestamp.value)?;
-            } else {
-                write!(f, "{}: {}", timestamp.replica_id, timestamp.value)?;
-            }
+            write!(f, "{:?}: {}", timestamp.replica_id, timestamp.value)?;
         }
         write!(f, "}}")
     }

crates/cloud_api_client/Cargo.toml 🔗

@@ -20,5 +20,4 @@ gpui_tokio.workspace = true
 http_client.workspace = true
 parking_lot.workspace = true
 serde_json.workspace = true
-workspace-hack.workspace = true
 yawc.workspace = true

crates/cloud_api_types/Cargo.toml 🔗

@@ -17,7 +17,6 @@ chrono.workspace = true
 ciborium.workspace = true
 cloud_llm_client.workspace = true
 serde.workspace = true
-workspace-hack.workspace = true
 
 [dev-dependencies]
 pretty_assertions.workspace = true

crates/cloud_llm_client/Cargo.toml 🔗

@@ -21,7 +21,6 @@ serde = { workspace = true, features = ["derive", "rc"] }
 serde_json.workspace = true
 strum = { workspace = true, features = ["derive"] }
 uuid = { workspace = true, features = ["serde"] }
-workspace-hack.workspace = true
 
 [dev-dependencies]
 pretty_assertions.workspace = true

crates/codestral/Cargo.toml 🔗

@@ -23,6 +23,5 @@ serde.workspace = true
 serde_json.workspace = true
 smol.workspace = true
 text.workspace = true
-workspace-hack.workspace = true
 
 [dev-dependencies]

crates/collab/Cargo.toml 🔗

@@ -20,7 +20,7 @@ test-support = ["sqlite"]
 [dependencies]
 anyhow.workspace = true
 async-trait.workspace = true
-async-tungstenite.workspace = true
+async-tungstenite = { workspace = true, features = ["tokio", "tokio-rustls-manual-roots" ] }
 aws-config = { version = "1.1.5" }
 aws-sdk-kinesis = "1.51.0"
 aws-sdk-s3 = { version = "1.15.0" }
@@ -47,7 +47,9 @@ reqwest = { version = "0.11", features = ["json"] }
 reqwest_client.workspace = true
 rpc.workspace = true
 scrypt = "0.11"
-sea-orm = { version = "1.1.0-rc.1", features = ["sqlx-postgres", "postgres-array", "runtime-tokio-rustls", "with-uuid"] }
+# sea-orm and sea-orm-macros versions must match exactly.
+sea-orm = { version = "=1.1.10", features = ["sqlx-postgres", "postgres-array", "runtime-tokio-rustls", "with-uuid"] }
+sea-orm-macros = "=1.1.10"
 semantic_version.workspace = true
 semver.workspace = true
 serde.workspace = true
@@ -68,11 +70,10 @@ tracing = "0.1.40"
 tracing-subscriber = { version = "0.3.18", features = ["env-filter", "json", "registry", "tracing-log"] } # workaround for https://github.com/tokio-rs/tracing/issues/2927
 util.workspace = true
 uuid.workspace = true
-workspace-hack.workspace = true
 
 [dev-dependencies]
 agent_settings.workspace = true
-assistant_context.workspace = true
+assistant_text_thread.workspace = true
 assistant_slash_command.workspace = true
 async-trait.workspace = true
 audio.workspace = true
@@ -116,7 +117,7 @@ release_channel.workspace = true
 remote = { workspace = true, features = ["test-support"] }
 remote_server.workspace = true
 rpc = { workspace = true, features = ["test-support"] }
-sea-orm = { version = "1.1.0-rc.1", features = ["sqlx-sqlite"] }
+sea-orm = { version = "=1.1.10", features = ["sqlx-sqlite"] }
 serde_json.workspace = true
 session = { workspace = true, features = ["test-support"] }
 settings = { workspace = true, features = ["test-support"] }

crates/collab/src/db/queries/buffers.rs 🔗

@@ -62,9 +62,9 @@ impl Database {
                 .iter()
                 .map(|c| c.replica_id)
                 .collect::<HashSet<_>>();
-            let mut replica_id = ReplicaId(0);
+            let mut replica_id = ReplicaId(clock::ReplicaId::FIRST_COLLAB_ID.as_u16() as i32);
             while replica_ids.contains(&replica_id) {
-                replica_id.0 += 1;
+                replica_id = ReplicaId(replica_id.0 + 1);
             }
             let collaborator = channel_buffer_collaborator::ActiveModel {
                 channel_id: ActiveValue::Set(channel_id),
@@ -203,7 +203,7 @@ impl Database {
                 while let Some(row) = rows.next().await {
                     let row = row?;
                     let timestamp = clock::Lamport {
-                        replica_id: row.replica_id as u16,
+                        replica_id: clock::ReplicaId::new(row.replica_id as u16),
                         value: row.lamport_timestamp as u32,
                     };
                     server_version.observe(timestamp);
@@ -701,7 +701,11 @@ impl Database {
             return Ok(());
         }
 
-        let mut text_buffer = text::Buffer::new(0, text::BufferId::new(1).unwrap(), base_text);
+        let mut text_buffer = text::Buffer::new(
+            clock::ReplicaId::LOCAL,
+            text::BufferId::new(1).unwrap(),
+            base_text,
+        );
         text_buffer.apply_ops(operations.into_iter().filter_map(operation_from_wire));
 
         let base_text = text_buffer.text();
@@ -934,7 +938,7 @@ pub fn operation_from_wire(operation: proto::Operation) -> Option<text::Operatio
     match operation.variant? {
         proto::operation::Variant::Edit(edit) => Some(text::Operation::Edit(EditOperation {
             timestamp: clock::Lamport {
-                replica_id: edit.replica_id as text::ReplicaId,
+                replica_id: clock::ReplicaId::new(edit.replica_id as u16),
                 value: edit.lamport_timestamp,
             },
             version: version_from_wire(&edit.version),
@@ -949,7 +953,7 @@ pub fn operation_from_wire(operation: proto::Operation) -> Option<text::Operatio
         })),
         proto::operation::Variant::Undo(undo) => Some(text::Operation::Undo(UndoOperation {
             timestamp: clock::Lamport {
-                replica_id: undo.replica_id as text::ReplicaId,
+                replica_id: clock::ReplicaId::new(undo.replica_id as u16),
                 value: undo.lamport_timestamp,
             },
             version: version_from_wire(&undo.version),
@@ -959,7 +963,7 @@ pub fn operation_from_wire(operation: proto::Operation) -> Option<text::Operatio
                 .map(|c| {
                     (
                         clock::Lamport {
-                            replica_id: c.replica_id as text::ReplicaId,
+                            replica_id: clock::ReplicaId::new(c.replica_id as u16),
                             value: c.lamport_timestamp,
                         },
                         c.count,
@@ -975,7 +979,7 @@ fn version_from_wire(message: &[proto::VectorClockEntry]) -> clock::Global {
     let mut version = clock::Global::new();
     for entry in message {
         version.observe(clock::Lamport {
-            replica_id: entry.replica_id as text::ReplicaId,
+            replica_id: clock::ReplicaId::new(entry.replica_id as u16),
             value: entry.timestamp,
         });
     }
@@ -986,7 +990,7 @@ fn version_to_wire(version: &clock::Global) -> Vec<proto::VectorClockEntry> {
     let mut message = Vec::new();
     for entry in version.iter() {
         message.push(proto::VectorClockEntry {
-            replica_id: entry.replica_id as u32,
+            replica_id: entry.replica_id.as_u16() as u32,
             timestamp: entry.value,
         });
     }

crates/collab/src/db/queries/extensions.rs 🔗

@@ -255,7 +255,7 @@ impl Database {
 
                 let insert = extension::Entity::insert(extension::ActiveModel {
                     name: ActiveValue::Set(latest_version.name.clone()),
-                    external_id: ActiveValue::Set(external_id.to_string()),
+                    external_id: ActiveValue::Set((*external_id).to_owned()),
                     id: ActiveValue::NotSet,
                     latest_version: ActiveValue::Set(latest_version.version.to_string()),
                     total_download_count: ActiveValue::NotSet,

crates/collab/src/db/queries/notifications.rs 🔗

@@ -17,7 +17,7 @@ impl Database {
                     .any(|existing| existing.name == **kind)
             })
             .map(|kind| notification_kind::ActiveModel {
-                name: ActiveValue::Set(kind.to_string()),
+                name: ActiveValue::Set((*kind).to_owned()),
                 ..Default::default()
             })
             .collect();
@@ -260,7 +260,7 @@ pub fn model_to_proto(this: &Database, row: notification::Model) -> Result<proto
         .context("Unknown notification kind")?;
     Ok(proto::Notification {
         id: row.id.to_proto(),
-        kind: kind.to_string(),
+        kind: (*kind).to_owned(),
         timestamp: row.created_at.assume_utc().unix_timestamp() as u64,
         is_read: row.is_read,
         response: row.response,

crates/collab/src/db/queries/projects.rs 🔗

@@ -91,14 +91,18 @@ impl Database {
                 .await?;
             }
 
-            let replica_id = if is_ssh_project { 1 } else { 0 };
+            let replica_id = if is_ssh_project {
+                clock::ReplicaId::REMOTE_SERVER
+            } else {
+                clock::ReplicaId::LOCAL
+            };
 
             project_collaborator::ActiveModel {
                 project_id: ActiveValue::set(project.id),
                 connection_id: ActiveValue::set(connection.id as i32),
                 connection_server_id: ActiveValue::set(ServerId(connection.owner_id as i32)),
                 user_id: ActiveValue::set(participant.user_id),
-                replica_id: ActiveValue::set(ReplicaId(replica_id)),
+                replica_id: ActiveValue::set(ReplicaId(replica_id.as_u16() as i32)),
                 is_host: ActiveValue::set(true),
                 id: ActiveValue::NotSet,
                 committer_name: ActiveValue::Set(None),
@@ -841,7 +845,7 @@ impl Database {
             .iter()
             .map(|c| c.replica_id)
             .collect::<HashSet<_>>();
-        let mut replica_id = ReplicaId(1);
+        let mut replica_id = ReplicaId(clock::ReplicaId::FIRST_COLLAB_ID.as_u16() as i32);
         while replica_ids.contains(&replica_id) {
             replica_id.0 += 1;
         }

crates/collab/src/db/tests.rs 🔗

@@ -196,7 +196,7 @@ fn channel_tree(channels: &[(ChannelId, &[ChannelId], &'static str)]) -> Vec<Cha
 
         result.push(Channel {
             id: *id,
-            name: name.to_string(),
+            name: (*name).to_owned(),
             visibility: ChannelVisibility::Members,
             parent_path: parent_key,
             channel_order: order,

crates/collab/src/db/tests/buffer_tests.rs 🔗

@@ -1,7 +1,7 @@
 use super::*;
 use crate::test_both_dbs;
 use language::proto::{self, serialize_version};
-use text::Buffer;
+use text::{Buffer, ReplicaId};
 
 test_both_dbs!(
     test_channel_buffers,
@@ -70,7 +70,11 @@ async fn test_channel_buffers(db: &Arc<Database>) {
         .await
         .unwrap();
 
-    let mut buffer_a = Buffer::new(0, text::BufferId::new(1).unwrap(), "".to_string());
+    let mut buffer_a = Buffer::new(
+        ReplicaId::new(0),
+        text::BufferId::new(1).unwrap(),
+        "".to_string(),
+    );
     let operations = vec![
         buffer_a.edit([(0..0, "hello world")]),
         buffer_a.edit([(5..5, ", cruel")]),
@@ -95,7 +99,7 @@ async fn test_channel_buffers(db: &Arc<Database>) {
         .unwrap();
 
     let mut buffer_b = Buffer::new(
-        0,
+        ReplicaId::new(0),
         text::BufferId::new(1).unwrap(),
         buffer_response_b.base_text,
     );
@@ -124,7 +128,7 @@ async fn test_channel_buffers(db: &Arc<Database>) {
             rpc::proto::Collaborator {
                 user_id: a_id.to_proto(),
                 peer_id: Some(rpc::proto::PeerId { id: 1, owner_id }),
-                replica_id: 0,
+                replica_id: ReplicaId::FIRST_COLLAB_ID.as_u16() as u32,
                 is_host: false,
                 committer_name: None,
                 committer_email: None,
@@ -132,7 +136,7 @@ async fn test_channel_buffers(db: &Arc<Database>) {
             rpc::proto::Collaborator {
                 user_id: b_id.to_proto(),
                 peer_id: Some(rpc::proto::PeerId { id: 2, owner_id }),
-                replica_id: 1,
+                replica_id: ReplicaId::FIRST_COLLAB_ID.as_u16() as u32 + 1,
                 is_host: false,
                 committer_name: None,
                 committer_email: None,
@@ -228,7 +232,8 @@ async fn test_channel_buffers_last_operations(db: &Database) {
             .await
             .unwrap();
 
-        db.join_channel_buffer(channel, user_id, connection_id)
+        let res = db
+            .join_channel_buffer(channel, user_id, connection_id)
             .await
             .unwrap();
 
@@ -239,7 +244,7 @@ async fn test_channel_buffers_last_operations(db: &Database) {
         );
 
         text_buffers.push(Buffer::new(
-            0,
+            ReplicaId::new(res.replica_id as u16),
             text::BufferId::new(1).unwrap(),
             "".to_string(),
         ));
@@ -276,7 +281,12 @@ async fn test_channel_buffers_last_operations(db: &Database) {
     db.join_channel_buffer(buffers[1].channel_id, user_id, connection_id)
         .await
         .unwrap();
-    text_buffers[1] = Buffer::new(1, text::BufferId::new(1).unwrap(), "def".to_string());
+    let replica_id = text_buffers[1].replica_id();
+    text_buffers[1] = Buffer::new(
+        replica_id,
+        text::BufferId::new(1).unwrap(),
+        "def".to_string(),
+    );
     update_buffer(
         buffers[1].channel_id,
         user_id,
@@ -304,20 +314,32 @@ async fn test_channel_buffers_last_operations(db: &Database) {
             rpc::proto::ChannelBufferVersion {
                 channel_id: buffers[0].channel_id.to_proto(),
                 epoch: 0,
-                version: serialize_version(&text_buffers[0].version()),
+                version: serialize_version(&text_buffers[0].version())
+                    .into_iter()
+                    .filter(
+                        |vector| vector.replica_id == text_buffers[0].replica_id().as_u16() as u32
+                    )
+                    .collect::<Vec<_>>(),
             },
             rpc::proto::ChannelBufferVersion {
                 channel_id: buffers[1].channel_id.to_proto(),
                 epoch: 1,
                 version: serialize_version(&text_buffers[1].version())
                     .into_iter()
-                    .filter(|vector| vector.replica_id == text_buffers[1].replica_id() as u32)
+                    .filter(
+                        |vector| vector.replica_id == text_buffers[1].replica_id().as_u16() as u32
+                    )
                     .collect::<Vec<_>>(),
             },
             rpc::proto::ChannelBufferVersion {
                 channel_id: buffers[2].channel_id.to_proto(),
                 epoch: 0,
-                version: serialize_version(&text_buffers[2].version()),
+                version: serialize_version(&text_buffers[2].version())
+                    .into_iter()
+                    .filter(
+                        |vector| vector.replica_id == text_buffers[2].replica_id().as_u16() as u32
+                    )
+                    .collect::<Vec<_>>(),
             },
         ]
     );

crates/collab/src/rpc.rs 🔗

@@ -343,7 +343,6 @@ impl Server {
             .add_request_handler(forward_read_only_project_request::<proto::OpenBufferForSymbol>)
             .add_request_handler(forward_read_only_project_request::<proto::OpenBufferById>)
             .add_request_handler(forward_read_only_project_request::<proto::SynchronizeBuffers>)
-            .add_request_handler(forward_read_only_project_request::<proto::InlayHints>)
             .add_request_handler(forward_read_only_project_request::<proto::ResolveInlayHint>)
             .add_request_handler(forward_read_only_project_request::<proto::GetColorPresentation>)
             .add_request_handler(forward_read_only_project_request::<proto::OpenBufferByPath>)

crates/collab/src/tests/editor_tests.rs 🔗

@@ -505,7 +505,7 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
                     label: "third_method(…)".into(),
                     detail: Some("fn(&mut self, B, C, D) -> E".into()),
                     text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
-                        // no snippet placehodlers
+                        // no snippet placeholders
                         new_text: "third_method".to_string(),
                         range: lsp::Range::new(
                             lsp::Position::new(1, 32),
@@ -877,7 +877,7 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T
             6..9
         );
         rename.editor.update(cx, |rename_editor, cx| {
-            let rename_selection = rename_editor.selections.newest::<usize>(cx);
+            let rename_selection = rename_editor.selections.newest::<usize>(&rename_editor.display_snapshot(cx));
             assert_eq!(
                 rename_selection.range(),
                 0..3,
@@ -924,7 +924,7 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T
         let lsp_rename_end = rename.range.end.to_offset(&buffer);
         assert_eq!(lsp_rename_start..lsp_rename_end, 6..9);
         rename.editor.update(cx, |rename_editor, cx| {
-            let rename_selection = rename_editor.selections.newest::<usize>(cx);
+            let rename_selection = rename_editor.selections.newest::<usize>(&rename_editor.display_snapshot(cx));
             assert_eq!(
                 rename_selection.range(),
                 1..2,
@@ -1849,10 +1849,40 @@ async fn test_mutual_editor_inlay_hint_cache_update(
         ..lsp::ServerCapabilities::default()
     };
     client_a.language_registry().add(rust_lang());
+
+    // Set up the language server to return an additional inlay hint on each request.
+    let edits_made = Arc::new(AtomicUsize::new(0));
+    let closure_edits_made = Arc::clone(&edits_made);
     let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
         "Rust",
         FakeLspAdapter {
             capabilities: capabilities.clone(),
+            initializer: Some(Box::new(move |fake_language_server| {
+                let closure_edits_made = closure_edits_made.clone();
+                fake_language_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
+                    move |params, _| {
+                        let edits_made_2 = Arc::clone(&closure_edits_made);
+                        async move {
+                            assert_eq!(
+                                params.text_document.uri,
+                                lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
+                            );
+                            let edits_made =
+                                AtomicUsize::load(&edits_made_2, atomic::Ordering::Acquire);
+                            Ok(Some(vec![lsp::InlayHint {
+                                position: lsp::Position::new(0, edits_made as u32),
+                                label: lsp::InlayHintLabel::String(edits_made.to_string()),
+                                kind: None,
+                                text_edits: None,
+                                tooltip: None,
+                                padding_left: None,
+                                padding_right: None,
+                                data: None,
+                            }]))
+                        }
+                    },
+                );
+            })),
             ..FakeLspAdapter::default()
         },
     );
@@ -1894,61 +1924,20 @@ async fn test_mutual_editor_inlay_hint_cache_update(
         .unwrap();
 
     let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
-    executor.start_waiting();
 
     // The host opens a rust file.
-    let _buffer_a = project_a
-        .update(cx_a, |project, cx| {
-            project.open_local_buffer(path!("/a/main.rs"), cx)
-        })
-        .await
-        .unwrap();
-    let editor_a = workspace_a
-        .update_in(cx_a, |workspace, window, cx| {
-            workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
-        })
-        .await
-        .unwrap()
-        .downcast::<Editor>()
-        .unwrap();
-
+    let file_a = workspace_a.update_in(cx_a, |workspace, window, cx| {
+        workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
+    });
     let fake_language_server = fake_language_servers.next().await.unwrap();
-
-    // Set up the language server to return an additional inlay hint on each request.
-    let edits_made = Arc::new(AtomicUsize::new(0));
-    let closure_edits_made = Arc::clone(&edits_made);
-    fake_language_server
-        .set_request_handler::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
-            let edits_made_2 = Arc::clone(&closure_edits_made);
-            async move {
-                assert_eq!(
-                    params.text_document.uri,
-                    lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
-                );
-                let edits_made = AtomicUsize::load(&edits_made_2, atomic::Ordering::Acquire);
-                Ok(Some(vec![lsp::InlayHint {
-                    position: lsp::Position::new(0, edits_made as u32),
-                    label: lsp::InlayHintLabel::String(edits_made.to_string()),
-                    kind: None,
-                    text_edits: None,
-                    tooltip: None,
-                    padding_left: None,
-                    padding_right: None,
-                    data: None,
-                }]))
-            }
-        })
-        .next()
-        .await
-        .unwrap();
-
+    let editor_a = file_a.await.unwrap().downcast::<Editor>().unwrap();
     executor.run_until_parked();
 
     let initial_edit = edits_made.load(atomic::Ordering::Acquire);
-    editor_a.update(cx_a, |editor, _| {
+    editor_a.update(cx_a, |editor, cx| {
         assert_eq!(
             vec![initial_edit.to_string()],
-            extract_hint_labels(editor),
+            extract_hint_labels(editor, cx),
             "Host should get its first hints when opens an editor"
         );
     });
@@ -1963,10 +1952,10 @@ async fn test_mutual_editor_inlay_hint_cache_update(
         .unwrap();
 
     executor.run_until_parked();
-    editor_b.update(cx_b, |editor, _| {
+    editor_b.update(cx_b, |editor, cx| {
         assert_eq!(
             vec![initial_edit.to_string()],
-            extract_hint_labels(editor),
+            extract_hint_labels(editor, cx),
             "Client should get its first hints when opens an editor"
         );
     });
@@ -1981,16 +1970,16 @@ async fn test_mutual_editor_inlay_hint_cache_update(
     cx_b.focus(&editor_b);
 
     executor.run_until_parked();
-    editor_a.update(cx_a, |editor, _| {
+    editor_a.update(cx_a, |editor, cx| {
         assert_eq!(
             vec![after_client_edit.to_string()],
-            extract_hint_labels(editor),
+            extract_hint_labels(editor, cx),
         );
     });
-    editor_b.update(cx_b, |editor, _| {
+    editor_b.update(cx_b, |editor, cx| {
         assert_eq!(
             vec![after_client_edit.to_string()],
-            extract_hint_labels(editor),
+            extract_hint_labels(editor, cx),
         );
     });
 
@@ -2004,16 +1993,16 @@ async fn test_mutual_editor_inlay_hint_cache_update(
     cx_a.focus(&editor_a);
 
     executor.run_until_parked();
-    editor_a.update(cx_a, |editor, _| {
+    editor_a.update(cx_a, |editor, cx| {
         assert_eq!(
             vec![after_host_edit.to_string()],
-            extract_hint_labels(editor),
+            extract_hint_labels(editor, cx),
         );
     });
-    editor_b.update(cx_b, |editor, _| {
+    editor_b.update(cx_b, |editor, cx| {
         assert_eq!(
             vec![after_host_edit.to_string()],
-            extract_hint_labels(editor),
+            extract_hint_labels(editor, cx),
         );
     });
 
@@ -2025,26 +2014,22 @@ async fn test_mutual_editor_inlay_hint_cache_update(
         .expect("inlay refresh request failed");
 
     executor.run_until_parked();
-    editor_a.update(cx_a, |editor, _| {
+    editor_a.update(cx_a, |editor, cx| {
         assert_eq!(
             vec![after_special_edit_for_refresh.to_string()],
-            extract_hint_labels(editor),
+            extract_hint_labels(editor, cx),
             "Host should react to /refresh LSP request"
         );
     });
-    editor_b.update(cx_b, |editor, _| {
+    editor_b.update(cx_b, |editor, cx| {
         assert_eq!(
             vec![after_special_edit_for_refresh.to_string()],
-            extract_hint_labels(editor),
+            extract_hint_labels(editor, cx),
             "Guest should get a /refresh LSP request propagated by host"
         );
     });
 }
 
-// This test started hanging on seed 2 after the theme settings
-// PR. The hypothesis is that it's been buggy for a while, but got lucky
-// on seeds.
-#[ignore]
 #[gpui::test(iterations = 10)]
 async fn test_inlay_hint_refresh_is_forwarded(
     cx_a: &mut TestAppContext,
@@ -2206,18 +2191,18 @@ async fn test_inlay_hint_refresh_is_forwarded(
     executor.finish_waiting();
 
     executor.run_until_parked();
-    editor_a.update(cx_a, |editor, _| {
+    editor_a.update(cx_a, |editor, cx| {
         assert!(
-            extract_hint_labels(editor).is_empty(),
+            extract_hint_labels(editor, cx).is_empty(),
             "Host should get no hints due to them turned off"
         );
     });
 
     executor.run_until_parked();
-    editor_b.update(cx_b, |editor, _| {
+    editor_b.update(cx_b, |editor, cx| {
         assert_eq!(
             vec!["initial hint".to_string()],
-            extract_hint_labels(editor),
+            extract_hint_labels(editor, cx),
             "Client should get its first hints when opens an editor"
         );
     });
@@ -2229,18 +2214,18 @@ async fn test_inlay_hint_refresh_is_forwarded(
         .into_response()
         .expect("inlay refresh request failed");
     executor.run_until_parked();
-    editor_a.update(cx_a, |editor, _| {
+    editor_a.update(cx_a, |editor, cx| {
         assert!(
-            extract_hint_labels(editor).is_empty(),
+            extract_hint_labels(editor, cx).is_empty(),
             "Host should get no hints due to them turned off, even after the /refresh"
         );
     });
 
     executor.run_until_parked();
-    editor_b.update(cx_b, |editor, _| {
+    editor_b.update(cx_b, |editor, cx| {
         assert_eq!(
             vec!["other hint".to_string()],
-            extract_hint_labels(editor),
+            extract_hint_labels(editor, cx),
             "Guest should get a /refresh LSP request propagated by host despite host hints are off"
         );
     });
@@ -4217,15 +4202,35 @@ fn tab_undo_assert(
     cx_b.assert_editor_state(expected_initial);
 }
 
-fn extract_hint_labels(editor: &Editor) -> Vec<String> {
-    let mut labels = Vec::new();
-    for hint in editor.inlay_hint_cache().hints() {
-        match hint.label {
-            project::InlayHintLabel::String(s) => labels.push(s),
-            _ => unreachable!(),
-        }
+fn extract_hint_labels(editor: &Editor, cx: &mut App) -> Vec<String> {
+    let lsp_store = editor.project().unwrap().read(cx).lsp_store();
+
+    let mut all_cached_labels = Vec::new();
+    let mut all_fetched_hints = Vec::new();
+    for buffer in editor.buffer().read(cx).all_buffers() {
+        lsp_store.update(cx, |lsp_store, cx| {
+            let hints = &lsp_store.latest_lsp_data(&buffer, cx).inlay_hints();
+            all_cached_labels.extend(hints.all_cached_hints().into_iter().map(|hint| {
+                let mut label = hint.text().to_string();
+                if hint.padding_left {
+                    label.insert(0, ' ');
+                }
+                if hint.padding_right {
+                    label.push_str(" ");
+                }
+                label
+            }));
+            all_fetched_hints.extend(hints.all_fetched_hints());
+        });
     }
-    labels
+
+    assert!(
+        all_fetched_hints.is_empty(),
+        "Did not expect background hints fetch tasks, but got {} of them",
+        all_fetched_hints.len()
+    );
+
+    all_cached_labels
 }
 
 #[track_caller]

crates/collab/src/tests/following_tests.rs 🔗

@@ -122,13 +122,19 @@ async fn test_basic_following(
         editor.handle_input("b", window, cx);
         editor.handle_input("c", window, cx);
         editor.select_left(&Default::default(), window, cx);
-        assert_eq!(editor.selections.ranges(cx), vec![3..2]);
+        assert_eq!(
+            editor.selections.ranges(&editor.display_snapshot(cx)),
+            vec![3..2]
+        );
     });
     editor_a2.update_in(cx_a, |editor, window, cx| {
         editor.handle_input("d", window, cx);
         editor.handle_input("e", window, cx);
         editor.select_left(&Default::default(), window, cx);
-        assert_eq!(editor.selections.ranges(cx), vec![2..1]);
+        assert_eq!(
+            editor.selections.ranges(&editor.display_snapshot(cx)),
+            vec![2..1]
+        );
     });
 
     // When client B starts following client A, only the active view state is replicated to client B.
@@ -149,11 +155,15 @@ async fn test_basic_following(
         Some((worktree_id, rel_path("2.txt")).into())
     );
     assert_eq!(
-        editor_b2.update(cx_b, |editor, cx| editor.selections.ranges(cx)),
+        editor_b2.update(cx_b, |editor, cx| editor
+            .selections
+            .ranges(&editor.display_snapshot(cx))),
         vec![2..1]
     );
     assert_eq!(
-        editor_b1.update(cx_b, |editor, cx| editor.selections.ranges(cx)),
+        editor_b1.update(cx_b, |editor, cx| editor
+            .selections
+            .ranges(&editor.display_snapshot(cx))),
         vec![3..3]
     );
 
@@ -384,7 +394,10 @@ async fn test_basic_following(
     cx_b.background_executor.run_until_parked();
 
     editor_b1.update(cx_b, |editor, cx| {
-        assert_eq!(editor.selections.ranges(cx), &[1..1, 2..2]);
+        assert_eq!(
+            editor.selections.ranges(&editor.display_snapshot(cx)),
+            &[1..1, 2..2]
+        );
     });
 
     editor_a1.update_in(cx_a, |editor, window, cx| {
@@ -402,7 +415,10 @@ async fn test_basic_following(
     executor.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
     executor.run_until_parked();
     editor_b1.update(cx_b, |editor, cx| {
-        assert_eq!(editor.selections.ranges(cx), &[3..3]);
+        assert_eq!(
+            editor.selections.ranges(&editor.display_snapshot(cx)),
+            &[3..3]
+        );
     });
 
     // After unfollowing, client B stops receiving updates from client A.
@@ -760,26 +776,30 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
         .unwrap();
 
     // Clients A and B follow each other in split panes
-    workspace_a.update_in(cx_a, |workspace, window, cx| {
-        workspace.split_and_clone(
-            workspace.active_pane().clone(),
-            SplitDirection::Right,
-            window,
-            cx,
-        );
-    });
+    workspace_a
+        .update_in(cx_a, |workspace, window, cx| {
+            workspace.split_and_clone(
+                workspace.active_pane().clone(),
+                SplitDirection::Right,
+                window,
+                cx,
+            )
+        })
+        .await;
     workspace_a.update_in(cx_a, |workspace, window, cx| {
         workspace.follow(client_b.peer_id().unwrap(), window, cx)
     });
     executor.run_until_parked();
-    workspace_b.update_in(cx_b, |workspace, window, cx| {
-        workspace.split_and_clone(
-            workspace.active_pane().clone(),
-            SplitDirection::Right,
-            window,
-            cx,
-        );
-    });
+    workspace_b
+        .update_in(cx_b, |workspace, window, cx| {
+            workspace.split_and_clone(
+                workspace.active_pane().clone(),
+                SplitDirection::Right,
+                window,
+                cx,
+            )
+        })
+        .await;
     workspace_b.update_in(cx_b, |workspace, window, cx| {
         workspace.follow(client_a.peer_id().unwrap(), window, cx)
     });
@@ -1353,9 +1373,11 @@ async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont
     );
 
     // When client B activates a different pane, it continues following client A in the original pane.
-    workspace_b.update_in(cx_b, |workspace, window, cx| {
-        workspace.split_and_clone(pane_b.clone(), SplitDirection::Right, window, cx)
-    });
+    workspace_b
+        .update_in(cx_b, |workspace, window, cx| {
+            workspace.split_and_clone(pane_b.clone(), SplitDirection::Right, window, cx)
+        })
+        .await;
     assert_eq!(
         workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
         Some(leader_id.into())
@@ -1679,7 +1701,10 @@ async fn test_following_stops_on_unshare(cx_a: &mut TestAppContext, cx_b: &mut T
         .advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
     cx_a.run_until_parked();
     editor_b.update(cx_b, |editor, cx| {
-        assert_eq!(editor.selections.ranges(cx), vec![1..1])
+        assert_eq!(
+            editor.selections.ranges(&editor.display_snapshot(cx)),
+            vec![1..1]
+        )
     });
 
     // a unshares the project
@@ -1701,7 +1726,10 @@ async fn test_following_stops_on_unshare(cx_a: &mut TestAppContext, cx_b: &mut T
         .advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
     cx_a.run_until_parked();
     editor_b.update(cx_b, |editor, cx| {
-        assert_eq!(editor.selections.ranges(cx), vec![1..1])
+        assert_eq!(
+            editor.selections.ranges(&editor.display_snapshot(cx)),
+            vec![1..1]
+        )
     });
     cx_b.update(|_, cx| {
         let room = ActiveCall::global(cx).read(cx).room().unwrap().read(cx);
@@ -1799,13 +1827,19 @@ async fn test_following_into_excluded_file(
         editor.handle_input("b", window, cx);
         editor.handle_input("c", window, cx);
         editor.select_left(&Default::default(), window, cx);
-        assert_eq!(editor.selections.ranges(cx), vec![3..2]);
+        assert_eq!(
+            editor.selections.ranges(&editor.display_snapshot(cx)),
+            vec![3..2]
+        );
     });
     editor_for_excluded_a.update_in(cx_a, |editor, window, cx| {
         editor.select_all(&Default::default(), window, cx);
         editor.handle_input("new commit message", window, cx);
         editor.select_left(&Default::default(), window, cx);
-        assert_eq!(editor.selections.ranges(cx), vec![18..17]);
+        assert_eq!(
+            editor.selections.ranges(&editor.display_snapshot(cx)),
+            vec![18..17]
+        );
     });
 
     // When client B starts following client A, currently visible file is replicated
@@ -1827,7 +1861,9 @@ async fn test_following_into_excluded_file(
         Some((worktree_id, rel_path(".git/COMMIT_EDITMSG")).into())
     );
     assert_eq!(
-        editor_for_excluded_b.update(cx_b, |editor, cx| editor.selections.ranges(cx)),
+        editor_for_excluded_b.update(cx_b, |editor, cx| editor
+            .selections
+            .ranges(&editor.display_snapshot(cx))),
         vec![18..17]
     );
 
@@ -2037,7 +2073,12 @@ async fn test_following_to_channel_notes_without_a_shared_project(
         assert_eq!(notes.channel(cx).unwrap().name, "channel-1");
         notes.editor.update(cx, |editor, cx| {
             assert_eq!(editor.text(cx), "Hello from A.");
-            assert_eq!(editor.selections.ranges::<usize>(cx), &[3..4]);
+            assert_eq!(
+                editor
+                    .selections
+                    .ranges::<usize>(&editor.display_snapshot(cx)),
+                &[3..4]
+            );
         })
     });
 

crates/collab/src/tests/integration_tests.rs 🔗

@@ -6,8 +6,8 @@ use crate::{
     },
 };
 use anyhow::{Result, anyhow};
-use assistant_context::ContextStore;
 use assistant_slash_command::SlashCommandWorkingSet;
+use assistant_text_thread::TextThreadStore;
 use buffer_diff::{DiffHunkSecondaryStatus, DiffHunkStatus, assert_hunks};
 use call::{ActiveCall, ParticipantLocation, Room, room};
 use client::{RECEIVE_TIMEOUT, User};
@@ -6748,7 +6748,7 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
     pane.update(cx, |pane, cx| {
         pane.split(workspace::SplitDirection::Right, cx);
     });
-
+    cx.run_until_parked();
     let right_pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
 
     pane.update(cx, |pane, cx| {
@@ -6877,9 +6877,9 @@ async fn test_context_collaboration_with_reconnect(
     });
 
     let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
-    let context_store_a = cx_a
+    let text_thread_store_a = cx_a
         .update(|cx| {
-            ContextStore::new(
+            TextThreadStore::new(
                 project_a.clone(),
                 prompt_builder.clone(),
                 Arc::new(SlashCommandWorkingSet::default()),
@@ -6888,9 +6888,9 @@ async fn test_context_collaboration_with_reconnect(
         })
         .await
         .unwrap();
-    let context_store_b = cx_b
+    let text_thread_store_b = cx_b
         .update(|cx| {
-            ContextStore::new(
+            TextThreadStore::new(
                 project_b.clone(),
                 prompt_builder.clone(),
                 Arc::new(SlashCommandWorkingSet::default()),
@@ -6901,60 +6901,60 @@ async fn test_context_collaboration_with_reconnect(
         .unwrap();
 
     // Client A creates a new chats.
-    let context_a = context_store_a.update(cx_a, |store, cx| store.create(cx));
+    let text_thread_a = text_thread_store_a.update(cx_a, |store, cx| store.create(cx));
     executor.run_until_parked();
 
     // Client B retrieves host's contexts and joins one.
-    let context_b = context_store_b
+    let text_thread_b = text_thread_store_b
         .update(cx_b, |store, cx| {
-            let host_contexts = store.host_contexts().to_vec();
-            assert_eq!(host_contexts.len(), 1);
-            store.open_remote_context(host_contexts[0].id.clone(), cx)
+            let host_text_threads = store.host_text_threads().collect::<Vec<_>>();
+            assert_eq!(host_text_threads.len(), 1);
+            store.open_remote(host_text_threads[0].id.clone(), cx)
         })
         .await
         .unwrap();
 
     // Host and guest make changes
-    context_a.update(cx_a, |context, cx| {
-        context.buffer().update(cx, |buffer, cx| {
+    text_thread_a.update(cx_a, |text_thread, cx| {
+        text_thread.buffer().update(cx, |buffer, cx| {
             buffer.edit([(0..0, "Host change\n")], None, cx)
         })
     });
-    context_b.update(cx_b, |context, cx| {
-        context.buffer().update(cx, |buffer, cx| {
+    text_thread_b.update(cx_b, |text_thread, cx| {
+        text_thread.buffer().update(cx, |buffer, cx| {
             buffer.edit([(0..0, "Guest change\n")], None, cx)
         })
     });
     executor.run_until_parked();
     assert_eq!(
-        context_a.read_with(cx_a, |context, cx| context.buffer().read(cx).text()),
+        text_thread_a.read_with(cx_a, |text_thread, cx| text_thread.buffer().read(cx).text()),
         "Guest change\nHost change\n"
     );
     assert_eq!(
-        context_b.read_with(cx_b, |context, cx| context.buffer().read(cx).text()),
+        text_thread_b.read_with(cx_b, |text_thread, cx| text_thread.buffer().read(cx).text()),
         "Guest change\nHost change\n"
     );
 
     // Disconnect client A and make some changes while disconnected.
     server.disconnect_client(client_a.peer_id().unwrap());
     server.forbid_connections();
-    context_a.update(cx_a, |context, cx| {
-        context.buffer().update(cx, |buffer, cx| {
+    text_thread_a.update(cx_a, |text_thread, cx| {
+        text_thread.buffer().update(cx, |buffer, cx| {
             buffer.edit([(0..0, "Host offline change\n")], None, cx)
         })
     });
-    context_b.update(cx_b, |context, cx| {
-        context.buffer().update(cx, |buffer, cx| {
+    text_thread_b.update(cx_b, |text_thread, cx| {
+        text_thread.buffer().update(cx, |buffer, cx| {
             buffer.edit([(0..0, "Guest offline change\n")], None, cx)
         })
     });
     executor.run_until_parked();
     assert_eq!(
-        context_a.read_with(cx_a, |context, cx| context.buffer().read(cx).text()),
+        text_thread_a.read_with(cx_a, |text_thread, cx| text_thread.buffer().read(cx).text()),
         "Host offline change\nGuest change\nHost change\n"
     );
     assert_eq!(
-        context_b.read_with(cx_b, |context, cx| context.buffer().read(cx).text()),
+        text_thread_b.read_with(cx_b, |text_thread, cx| text_thread.buffer().read(cx).text()),
         "Guest offline change\nGuest change\nHost change\n"
     );
 
@@ -6962,11 +6962,11 @@ async fn test_context_collaboration_with_reconnect(
     server.allow_connections();
     executor.advance_clock(RECEIVE_TIMEOUT);
     assert_eq!(
-        context_a.read_with(cx_a, |context, cx| context.buffer().read(cx).text()),
+        text_thread_a.read_with(cx_a, |text_thread, cx| text_thread.buffer().read(cx).text()),
         "Guest offline change\nHost offline change\nGuest change\nHost change\n"
     );
     assert_eq!(
-        context_b.read_with(cx_b, |context, cx| context.buffer().read(cx).text()),
+        text_thread_b.read_with(cx_b, |text_thread, cx| text_thread.buffer().read(cx).text()),
         "Guest offline change\nHost offline change\nGuest change\nHost change\n"
     );
 
@@ -6974,8 +6974,8 @@ async fn test_context_collaboration_with_reconnect(
     server.forbid_connections();
     server.disconnect_client(client_a.peer_id().unwrap());
     executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
-    context_b.read_with(cx_b, |context, cx| {
-        assert!(context.buffer().read(cx).read_only());
+    text_thread_b.read_with(cx_b, |text_thread, cx| {
+        assert!(text_thread.buffer().read(cx).read_only());
     });
 }
 

crates/collab/src/tests/test_server.rs 🔗

@@ -358,7 +358,7 @@ impl TestServer {
                 settings::KeymapFile::load_asset_allow_partial_failure(os_keymap, cx).unwrap(),
             );
             language_model::LanguageModelRegistry::test(cx);
-            assistant_context::init(client.clone(), cx);
+            assistant_text_thread::init(client.clone(), cx);
             agent_settings::init(cx);
         });
 

crates/collab_ui/Cargo.toml 🔗

@@ -60,7 +60,6 @@ title_bar.workspace = true
 ui.workspace = true
 util.workspace = true
 workspace.workspace = true
-workspace-hack.workspace = true
 
 [dev-dependencies]
 call = { workspace = true, features = ["test-support"] }

crates/collab_ui/src/channel_view.rs 🔗

@@ -287,9 +287,12 @@ impl ChannelView {
     }
 
     fn copy_link(&mut self, _: &CopyLink, window: &mut Window, cx: &mut Context<Self>) {
-        let position = self
-            .editor
-            .update(cx, |editor, cx| editor.selections.newest_display(cx).start);
+        let position = self.editor.update(cx, |editor, cx| {
+            editor
+                .selections
+                .newest_display(&editor.display_snapshot(cx))
+                .start
+        });
         self.copy_link_for_position(position, window, cx)
     }
 
@@ -495,8 +498,8 @@ impl Item for ChannelView {
         _: Option<WorkspaceId>,
         window: &mut Window,
         cx: &mut Context<Self>,
-    ) -> Option<Entity<Self>> {
-        Some(cx.new(|cx| {
+    ) -> Task<Option<Entity<Self>>> {
+        Task::ready(Some(cx.new(|cx| {
             Self::new(
                 self.project.clone(),
                 self.workspace.clone(),
@@ -505,7 +508,7 @@ impl Item for ChannelView {
                 window,
                 cx,
             )
-        }))
+        })))
     }
 
     fn navigate(

crates/collab_ui/src/collab_panel.rs 🔗

@@ -3037,6 +3037,10 @@ impl Panel for CollabPanel {
         "CollabPanel"
     }
 
+    fn panel_key() -> &'static str {
+        COLLABORATION_PANEL_KEY
+    }
+
     fn activation_priority(&self) -> u32 {
         6
     }

crates/collab_ui/src/notification_panel.rs 🔗

@@ -612,6 +612,10 @@ impl Panel for NotificationPanel {
         "NotificationPanel"
     }
 
+    fn panel_key() -> &'static str {
+        NOTIFICATION_PANEL_KEY
+    }
+
     fn position(&self, _: &Window, cx: &App) -> DockPosition {
         NotificationPanelSettings::get_global(cx).dock
     }
@@ -734,19 +738,17 @@ impl Render for NotificationToast {
             .on_modifiers_changed(cx.listener(|_, _, _, cx| cx.notify()))
             .child(
                 IconButton::new(close_id, close_icon)
-                    .tooltip(move |window, cx| {
+                    .tooltip(move |_window, cx| {
                         if suppress {
                             Tooltip::for_action(
                                 "Suppress.\nClose with click.",
                                 &workspace::SuppressNotification,
-                                window,
                                 cx,
                             )
                         } else {
                             Tooltip::for_action(
                                 "Close.\nSuppress with shift-click",
                                 &menu::Cancel,
-                                window,
                                 cx,
                             )
                         }

crates/collections/Cargo.toml 🔗

@@ -19,4 +19,3 @@ test-support = []
 [dependencies]
 indexmap.workspace = true
 rustc-hash.workspace = true
-workspace-hack.workspace = true

crates/command_palette/Cargo.toml 🔗

@@ -32,7 +32,6 @@ util.workspace = true
 telemetry.workspace = true
 workspace.workspace = true
 zed_actions.workspace = true
-workspace-hack.workspace = true
 
 [dev-dependencies]
 ctor.workspace = true

crates/command_palette/src/command_palette.rs 🔗

@@ -443,7 +443,7 @@ impl PickerDelegate for CommandPaletteDelegate {
         &self,
         ix: usize,
         selected: bool,
-        window: &mut Window,
+        _: &mut Window,
         cx: &mut Context<Picker<Self>>,
     ) -> Option<Self::ListItem> {
         let matching_command = self.matches.get(ix)?;
@@ -462,10 +462,9 @@ impl PickerDelegate for CommandPaletteDelegate {
                             command.name.clone(),
                             matching_command.positions.clone(),
                         ))
-                        .children(KeyBinding::for_action_in(
+                        .child(KeyBinding::for_action_in(
                             &*command.action,
                             &self.previous_focus_handle,
-                            window,
                             cx,
                         )),
                 ),
@@ -698,7 +697,11 @@ mod tests {
         editor.update_in(cx, |editor, window, cx| {
             assert!(editor.focus_handle(cx).is_focused(window));
             assert_eq!(
-                editor.selections.last::<Point>(cx).range().start,
+                editor
+                    .selections
+                    .last::<Point>(&editor.display_snapshot(cx))
+                    .range()
+                    .start,
                 Point::new(2, 0)
             );
         });

crates/command_palette_hooks/Cargo.toml 🔗

@@ -16,5 +16,4 @@ doctest = false
 collections.workspace = true
 derive_more.workspace = true
 gpui.workspace = true
-workspace-hack.workspace = true
 workspace.workspace = true

crates/component/Cargo.toml 🔗

@@ -18,7 +18,6 @@ inventory.workspace = true
 parking_lot.workspace = true
 strum.workspace = true
 theme.workspace = true
-workspace-hack.workspace = true
 
 [dev-dependencies]
 documented.workspace = true

crates/context_server/Cargo.toml 🔗

@@ -32,4 +32,3 @@ smol.workspace = true
 tempfile.workspace = true
 url = { workspace = true, features = ["serde"] }
 util.workspace = true
-workspace-hack.workspace = true

crates/copilot/Cargo.toml 🔗

@@ -52,7 +52,6 @@ task.workspace = true
 ui.workspace = true
 util.workspace = true
 workspace.workspace = true
-workspace-hack.workspace = true
 itertools.workspace = true
 
 [target.'cfg(windows)'.dependencies]

crates/crashes/Cargo.toml 🔗

@@ -17,7 +17,6 @@ smol.workspace = true
 serde.workspace = true
 serde_json.workspace = true
 system_specs.workspace = true
-workspace-hack.workspace = true
 zstd.workspace = true
 
 [target.'cfg(target_os = "macos")'.dependencies]

crates/crashes/src/crashes.rs 🔗

@@ -33,17 +33,31 @@ const CRASH_HANDLER_CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
 static PANIC_THREAD_ID: AtomicU32 = AtomicU32::new(0);
 
 pub async fn init(crash_init: InitCrashHandler) {
-    if *RELEASE_CHANNEL == ReleaseChannel::Dev && env::var("ZED_GENERATE_MINIDUMPS").is_err() {
-        let old_hook = panic::take_hook();
-        panic::set_hook(Box::new(move |info| {
-            unsafe { env::set_var("RUST_BACKTRACE", "1") };
-            old_hook(info);
-            // prevent the macOS crash dialog from popping up
-            std::process::exit(1);
-        }));
-        return;
-    } else {
-        panic::set_hook(Box::new(panic_hook));
+    let gen_var = match env::var("ZED_GENERATE_MINIDUMPS") {
+        Ok(v) => {
+            if v == "false" || v == "0" {
+                Some(false)
+            } else {
+                Some(true)
+            }
+        }
+        Err(_) => None,
+    };
+
+    match (gen_var, *RELEASE_CHANNEL) {
+        (Some(false), _) | (None, ReleaseChannel::Dev) => {
+            let old_hook = panic::take_hook();
+            panic::set_hook(Box::new(move |info| {
+                unsafe { env::set_var("RUST_BACKTRACE", "1") };
+                old_hook(info);
+                // prevent the macOS crash dialog from popping up
+                std::process::exit(1);
+            }));
+            return;
+        }
+        (Some(true), _) | (None, _) => {
+            panic::set_hook(Box::new(panic_hook));
+        }
     }
 
     let exe = env::current_exe().expect("unable to find ourselves");

crates/dap/Cargo.toml 🔗

@@ -49,7 +49,6 @@ smol.workspace = true
 task.workspace = true
 telemetry.workspace = true
 util.workspace = true
-workspace-hack.workspace = true
 
 [target.'cfg(not(windows))'.dependencies]
 libc.workspace = true

crates/dap/src/adapters.rs 🔗

@@ -306,7 +306,7 @@ pub async fn download_adapter_from_github(
     anyhow::ensure!(
         response.status().is_success(),
         "download failed with status {}",
-        response.status().to_string()
+        response.status()
     );
 
     delegate.output_to_console("Download complete".to_owned());
@@ -356,6 +356,7 @@ pub trait DebugAdapter: 'static + Send + Sync {
         config: &DebugTaskDefinition,
         user_installed_path: Option<PathBuf>,
         user_args: Option<Vec<String>>,
+        user_env: Option<HashMap<String, String>>,
         cx: &mut AsyncApp,
     ) -> Result<DebugAdapterBinary>;
 
@@ -455,6 +456,7 @@ impl DebugAdapter for FakeAdapter {
         task_definition: &DebugTaskDefinition,
         _: Option<PathBuf>,
         _: Option<Vec<String>>,
+        _: Option<HashMap<String, String>>,
         _: &mut AsyncApp,
     ) -> Result<DebugAdapterBinary> {
         let connection = task_definition

crates/dap_adapters/Cargo.toml 🔗

@@ -39,7 +39,6 @@ shlex.workspace = true
 smol.workspace = true
 task.workspace = true
 util.workspace = true
-workspace-hack.workspace = true
 
 [dev-dependencies]
 dap = { workspace = true, features = ["test-support"] }

crates/dap_adapters/src/codelldb.rs 🔗

@@ -2,6 +2,7 @@ use std::{path::PathBuf, sync::OnceLock};
 
 use anyhow::{Context as _, Result};
 use async_trait::async_trait;
+use collections::HashMap;
 use dap::adapters::{DebugTaskDefinition, latest_github_release};
 use futures::StreamExt;
 use gpui::AsyncApp;
@@ -329,6 +330,7 @@ impl DebugAdapter for CodeLldbDebugAdapter {
         config: &DebugTaskDefinition,
         user_installed_path: Option<PathBuf>,
         user_args: Option<Vec<String>>,
+        user_env: Option<HashMap<String, String>>,
         _: &mut AsyncApp,
     ) -> Result<DebugAdapterBinary> {
         let mut command = user_installed_path
@@ -378,11 +380,6 @@ impl DebugAdapter for CodeLldbDebugAdapter {
         };
         let mut json_config = config.config.clone();
 
-        // Enable info level for CodeLLDB by default.
-        // Logs can then be viewed in our DAP logs.
-        let mut envs = collections::HashMap::default();
-        envs.insert("RUST_LOG".to_string(), "info".to_string());
-
         Ok(DebugAdapterBinary {
             command: Some(command.unwrap()),
             cwd: Some(delegate.worktree_root_path().to_path_buf()),
@@ -407,7 +404,7 @@ impl DebugAdapter for CodeLldbDebugAdapter {
             request_args: self
                 .request_args(delegate, json_config, &config.label)
                 .await?,
-            envs,
+            envs: user_env.unwrap_or_default(),
             connection: None,
         })
     }

crates/dap_adapters/src/gdb.rs 🔗

@@ -1,7 +1,8 @@
-use std::{collections::HashMap, ffi::OsStr};
+use std::ffi::OsStr;
 
 use anyhow::{Context as _, Result, bail};
 use async_trait::async_trait;
+use collections::HashMap;
 use dap::{StartDebuggingRequestArguments, adapters::DebugTaskDefinition};
 use gpui::AsyncApp;
 use task::{DebugScenario, ZedDebugConfig};
@@ -160,6 +161,7 @@ impl DebugAdapter for GdbDebugAdapter {
         config: &DebugTaskDefinition,
         user_installed_path: Option<std::path::PathBuf>,
         user_args: Option<Vec<String>>,
+        user_env: Option<HashMap<String, String>>,
         _: &mut AsyncApp,
     ) -> Result<DebugAdapterBinary> {
         let user_setting_path = user_installed_path
@@ -188,7 +190,7 @@ impl DebugAdapter for GdbDebugAdapter {
         Ok(DebugAdapterBinary {
             command: Some(gdb_path),
             arguments: user_args.unwrap_or_else(|| vec!["-i=dap".into()]),
-            envs: HashMap::default(),
+            envs: user_env.unwrap_or_default(),
             cwd: Some(delegate.worktree_root_path().to_path_buf()),
             connection: None,
             request_args: StartDebuggingRequestArguments {

crates/dap_adapters/src/go.rs 🔗

@@ -409,6 +409,7 @@ impl DebugAdapter for GoDebugAdapter {
         task_definition: &DebugTaskDefinition,
         user_installed_path: Option<PathBuf>,
         user_args: Option<Vec<String>>,
+        user_env: Option<HashMap<String, String>>,
         _cx: &mut AsyncApp,
     ) -> Result<DebugAdapterBinary> {
         let adapter_path = paths::debug_adapters_dir().join(&Self::ADAPTER_NAME);
@@ -460,7 +461,7 @@ impl DebugAdapter for GoDebugAdapter {
         let connection;
 
         let mut configuration = task_definition.config.clone();
-        let mut envs = HashMap::default();
+        let mut envs = user_env.unwrap_or_default();
 
         if let Some(configuration) = configuration.as_object_mut() {
             configuration

crates/dap_adapters/src/javascript.rs 🔗

@@ -52,12 +52,13 @@ impl JsDebugAdapter {
         task_definition: &DebugTaskDefinition,
         user_installed_path: Option<PathBuf>,
         user_args: Option<Vec<String>>,
+        user_env: Option<HashMap<String, String>>,
         _: &mut AsyncApp,
     ) -> Result<DebugAdapterBinary> {
         let tcp_connection = task_definition.tcp_connection.clone().unwrap_or_default();
         let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
 
-        let mut envs = HashMap::default();
+        let mut envs = user_env.unwrap_or_default();
 
         let mut configuration = task_definition.config.clone();
         if let Some(configuration) = configuration.as_object_mut() {
@@ -100,9 +101,9 @@ impl JsDebugAdapter {
             }
 
             if let Some(env) = configuration.get("env").cloned()
-                && let Ok(env) = serde_json::from_value(env)
+                && let Ok(env) = serde_json::from_value::<HashMap<String, String>>(env)
             {
-                envs = env;
+                envs.extend(env.into_iter());
             }
 
             configuration
@@ -504,6 +505,7 @@ impl DebugAdapter for JsDebugAdapter {
         config: &DebugTaskDefinition,
         user_installed_path: Option<PathBuf>,
         user_args: Option<Vec<String>>,
+        user_env: Option<HashMap<String, String>>,
         cx: &mut AsyncApp,
     ) -> Result<DebugAdapterBinary> {
         if self.checked.set(()).is_ok() {
@@ -521,8 +523,15 @@ impl DebugAdapter for JsDebugAdapter {
             }
         }
 
-        self.get_installed_binary(delegate, config, user_installed_path, user_args, cx)
-            .await
+        self.get_installed_binary(
+            delegate,
+            config,
+            user_installed_path,
+            user_args,
+            user_env,
+            cx,
+        )
+        .await
     }
 
     fn label_for_child_session(&self, args: &StartDebuggingRequestArguments) -> Option<String> {

crates/dap_adapters/src/python.rs 🔗

@@ -1,5 +1,6 @@
 use crate::*;
 use anyhow::{Context as _, bail};
+use collections::HashMap;
 use dap::{DebugRequest, StartDebuggingRequestArguments, adapters::DebugTaskDefinition};
 use fs::RemoveOptions;
 use futures::{StreamExt, TryStreamExt};
@@ -16,7 +17,6 @@ use std::ffi::OsString;
 use std::net::Ipv4Addr;
 use std::str::FromStr;
 use std::{
-    collections::HashMap,
     ffi::OsStr,
     path::{Path, PathBuf},
 };
@@ -312,6 +312,7 @@ impl PythonDebugAdapter {
         config: &DebugTaskDefinition,
         user_installed_path: Option<PathBuf>,
         user_args: Option<Vec<String>>,
+        user_env: Option<HashMap<String, String>>,
         python_from_toolchain: Option<String>,
     ) -> Result<DebugAdapterBinary> {
         let tcp_connection = config.tcp_connection.clone().unwrap_or_default();
@@ -349,7 +350,7 @@ impl PythonDebugAdapter {
                 timeout,
             }),
             cwd: Some(delegate.worktree_root_path().to_path_buf()),
-            envs: HashMap::default(),
+            envs: user_env.unwrap_or_default(),
             request_args: self.request_args(delegate, config).await?,
         })
     }
@@ -744,6 +745,7 @@ impl DebugAdapter for PythonDebugAdapter {
         config: &DebugTaskDefinition,
         user_installed_path: Option<PathBuf>,
         user_args: Option<Vec<String>>,
+        user_env: Option<HashMap<String, String>>,
         cx: &mut AsyncApp,
     ) -> Result<DebugAdapterBinary> {
         if let Some(local_path) = &user_installed_path {
@@ -752,7 +754,14 @@ impl DebugAdapter for PythonDebugAdapter {
                 local_path.display()
             );
             return self
-                .get_installed_binary(delegate, config, Some(local_path.clone()), user_args, None)
+                .get_installed_binary(
+                    delegate,
+                    config,
+                    Some(local_path.clone()),
+                    user_args,
+                    user_env,
+                    None,
+                )
                 .await;
         }
 
@@ -790,12 +799,13 @@ impl DebugAdapter for PythonDebugAdapter {
                     config,
                     None,
                     user_args,
+                    user_env,
                     Some(toolchain.path.to_string()),
                 )
                 .await;
         }
 
-        self.get_installed_binary(delegate, config, None, user_args, None)
+        self.get_installed_binary(delegate, config, None, user_args, user_env, None)
             .await
     }
 

crates/db/Cargo.toml 🔗

@@ -26,7 +26,6 @@ smol.workspace = true
 sqlez.workspace = true
 sqlez_macros.workspace = true
 util.workspace = true
-workspace-hack.workspace = true
 zed_env_vars.workspace = true
 
 [dev-dependencies]

crates/debug_adapter_extension/Cargo.toml 🔗

@@ -8,13 +8,13 @@ edition.workspace = true
 [dependencies]
 anyhow.workspace = true
 async-trait.workspace = true
+collections.workspace = true
 dap.workspace = true
 extension.workspace = true
 gpui.workspace = true
 serde_json.workspace = true
 util.workspace = true
 task.workspace = true
-workspace-hack = { version = "0.1", path = "../../tooling/workspace-hack" }
 
 [lints]
 workspace = true

crates/debug_adapter_extension/src/extension_dap_adapter.rs 🔗

@@ -6,6 +6,7 @@ use std::{
 
 use anyhow::{Context, Result};
 use async_trait::async_trait;
+use collections::HashMap;
 use dap::{
     StartDebuggingRequestArgumentsRequest,
     adapters::{
@@ -91,6 +92,8 @@ impl DebugAdapter for ExtensionDapAdapter {
         user_installed_path: Option<PathBuf>,
         // TODO support user args in the extension API
         _user_args: Option<Vec<String>>,
+        // TODO support user env in the extension API
+        _user_env: Option<HashMap<String, String>>,
         _cx: &mut AsyncApp,
     ) -> Result<DebugAdapterBinary> {
         self.extension

crates/debugger_tools/Cargo.toml 🔗

@@ -27,4 +27,3 @@ settings.workspace = true
 smol.workspace = true
 util.workspace = true
 workspace.workspace = true
-workspace-hack.workspace = true

crates/debugger_ui/Cargo.toml 🔗

@@ -73,7 +73,6 @@ tree-sitter.workspace = true
 ui.workspace = true
 unindent = { workspace = true, optional = true }
 util.workspace = true
-workspace-hack.workspace = true
 workspace.workspace = true
 zed_actions.workspace = true
 

crates/debugger_ui/src/debugger_panel.rs 🔗

@@ -43,6 +43,8 @@ use workspace::{
 };
 use zed_actions::ToggleFocus;
 
+const DEBUG_PANEL_KEY: &str = "DebugPanel";
+
 pub struct DebugPanel {
     size: Pixels,
     active_session: Option<Entity<DebugSession>>,
@@ -614,12 +616,11 @@ impl DebugPanel {
                 })
                 .tooltip({
                     let focus_handle = focus_handle.clone();
-                    move |window, cx| {
+                    move |_window, cx| {
                         Tooltip::for_action_in(
                             "Start Debug Session",
                             &crate::Start,
                             &focus_handle,
-                            window,
                             cx,
                         )
                     }
@@ -692,12 +693,11 @@ impl DebugPanel {
                                                 ))
                                                 .tooltip({
                                                     let focus_handle = focus_handle.clone();
-                                                    move |window, cx| {
+                                                    move |_window, cx| {
                                                         Tooltip::for_action_in(
                                                             "Pause Program",
                                                             &Pause,
                                                             &focus_handle,
-                                                            window,
                                                             cx,
                                                         )
                                                     }
@@ -717,12 +717,11 @@ impl DebugPanel {
                                                 .disabled(thread_status != ThreadStatus::Stopped)
                                                 .tooltip({
                                                     let focus_handle = focus_handle.clone();
-                                                    move |window, cx| {
+                                                    move |_window, cx| {
                                                         Tooltip::for_action_in(
                                                             "Continue Program",
                                                             &Continue,
                                                             &focus_handle,
-                                                            window,
                                                             cx,
                                                         )
                                                     }
@@ -742,12 +741,11 @@ impl DebugPanel {
                                             .disabled(thread_status != ThreadStatus::Stopped)
                                             .tooltip({
                                                 let focus_handle = focus_handle.clone();
-                                                move |window, cx| {
+                                                move |_window, cx| {
                                                     Tooltip::for_action_in(
                                                         "Step Over",
                                                         &StepOver,
                                                         &focus_handle,
-                                                        window,
                                                         cx,
                                                     )
                                                 }
@@ -768,12 +766,11 @@ impl DebugPanel {
                                         .disabled(thread_status != ThreadStatus::Stopped)
                                         .tooltip({
                                             let focus_handle = focus_handle.clone();
-                                            move |window, cx| {
+                                            move |_window, cx| {
                                                 Tooltip::for_action_in(
                                                     "Step In",
                                                     &StepInto,
                                                     &focus_handle,
-                                                    window,
                                                     cx,
                                                 )
                                             }
@@ -791,12 +788,11 @@ impl DebugPanel {
                                             .disabled(thread_status != ThreadStatus::Stopped)
                                             .tooltip({
                                                 let focus_handle = focus_handle.clone();
-                                                move |window, cx| {
+                                                move |_window, cx| {
                                                     Tooltip::for_action_in(
                                                         "Step Out",
                                                         &StepOut,
                                                         &focus_handle,
-                                                        window,
                                                         cx,
                                                     )
                                                 }
@@ -814,12 +810,11 @@ impl DebugPanel {
                                             ))
                                             .tooltip({
                                                 let focus_handle = focus_handle.clone();
-                                                move |window, cx| {
+                                                move |_window, cx| {
                                                     Tooltip::for_action_in(
                                                         "Rerun Session",
                                                         &RerunSession,
                                                         &focus_handle,
-                                                        window,
                                                         cx,
                                                     )
                                                 }
@@ -859,12 +854,11 @@ impl DebugPanel {
                                                 } else {
                                                     "Terminate All Threads"
                                                 };
-                                                move |window, cx| {
+                                                move |_window, cx| {
                                                     Tooltip::for_action_in(
                                                         label,
                                                         &Stop,
                                                         &focus_handle,
-                                                        window,
                                                         cx,
                                                     )
                                                 }
@@ -891,12 +885,11 @@ impl DebugPanel {
                                                 ))
                                                 .tooltip({
                                                     let focus_handle = focus_handle.clone();
-                                                    move |window, cx| {
+                                                    move |_window, cx| {
                                                         Tooltip::for_action_in(
                                                             "Detach",
                                                             &Detach,
                                                             &focus_handle,
-                                                            window,
                                                             cx,
                                                         )
                                                     }
@@ -1414,6 +1407,10 @@ impl Panel for DebugPanel {
         "DebugPanel"
     }
 
+    fn panel_key() -> &'static str {
+        DEBUG_PANEL_KEY
+    }
+
     fn position(&self, _window: &Window, cx: &App) -> DockPosition {
         DebuggerSettings::get_global(cx).dock.into()
     }

crates/debugger_ui/src/debugger_ui.rs 🔗

@@ -341,8 +341,10 @@ pub fn init(cx: &mut App) {
                                 maybe!({
                                     let (buffer, position, _) = editor
                                         .update(cx, |editor, cx| {
-                                            let cursor_point: language::Point =
-                                                editor.selections.newest(cx).head();
+                                            let cursor_point: language::Point = editor
+                                                .selections
+                                                .newest(&editor.display_snapshot(cx))
+                                                .head();
 
                                             editor
                                                 .buffer()
@@ -392,7 +394,10 @@ pub fn init(cx: &mut App) {
                                 let text = editor
                                     .update(cx, |editor, cx| {
                                         editor.text_for_range(
-                                            editor.selections.newest(cx).range(),
+                                            editor
+                                                .selections
+                                                .newest(&editor.display_snapshot(cx))
+                                                .range(),
                                             &mut None,
                                             window,
                                             cx,

crates/debugger_ui/src/new_process_modal.rs 🔗

@@ -96,7 +96,9 @@ impl NewProcessModal {
                     let debug_picker = cx.new(|cx| {
                         let delegate =
                             DebugDelegate::new(debug_panel.downgrade(), task_store.clone());
-                        Picker::uniform_list(delegate, window, cx).modal(false)
+                        Picker::list(delegate, window, cx)
+                            .modal(false)
+                            .list_measure_all()
                     });
 
                     let configure_mode = ConfigureMode::new(window, cx);
@@ -745,22 +747,15 @@ impl Render for NewProcessModal {
                                 == 0;
                         let secondary_action = menu::SecondaryConfirm.boxed_clone();
                         container
-                            .child(div().children(
-                                KeyBinding::for_action(&*secondary_action, window, cx).map(
-                                    |keybind| {
-                                        Button::new("edit-attach-task", "Edit in debug.json")
-                                            .label_size(LabelSize::Small)
-                                            .key_binding(keybind)
-                                            .on_click(move |_, window, cx| {
-                                                window.dispatch_action(
-                                                    secondary_action.boxed_clone(),
-                                                    cx,
-                                                )
-                                            })
-                                            .disabled(disabled)
-                                    },
-                                ),
-                            ))
+                            .child(div().child({
+                                Button::new("edit-attach-task", "Edit in debug.json")
+                                    .label_size(LabelSize::Small)
+                                    .key_binding(KeyBinding::for_action(&*secondary_action, cx))
+                                    .on_click(move |_, window, cx| {
+                                        window.dispatch_action(secondary_action.boxed_clone(), cx)
+                                    })
+                                    .disabled(disabled)
+                            }))
                             .child(
                                 h_flex()
                                     .child(div().child(self.adapter_drop_down_menu(window, cx))),
@@ -1053,7 +1048,7 @@ impl DebugDelegate {
             Some(TaskSourceKind::Lsp { language_name, .. }) => {
                 Some(format!("LSP: {language_name}"))
             }
-            Some(TaskSourceKind::Language { .. }) => None,
+            Some(TaskSourceKind::Language { name }) => Some(format!("Lang: {name}")),
             _ => context.clone().and_then(|ctx| {
                 ctx.task_context
                     .task_variables
@@ -1447,56 +1442,48 @@ impl PickerDelegate for DebugDelegate {
             .justify_between()
             .border_t_1()
             .border_color(cx.theme().colors().border_variant)
-            .children({
+            .child({
                 let action = menu::SecondaryConfirm.boxed_clone();
                 if self.matches.is_empty() {
-                    Some(
-                        Button::new("edit-debug-json", "Edit debug.json")
-                            .label_size(LabelSize::Small)
-                            .on_click(cx.listener(|_picker, _, window, cx| {
-                                window.dispatch_action(
-                                    zed_actions::OpenProjectDebugTasks.boxed_clone(),
-                                    cx,
-                                );
-                                cx.emit(DismissEvent);
-                            })),
-                    )
+                    Button::new("edit-debug-json", "Edit debug.json")
+                        .label_size(LabelSize::Small)
+                        .on_click(cx.listener(|_picker, _, window, cx| {
+                            window.dispatch_action(
+                                zed_actions::OpenProjectDebugTasks.boxed_clone(),
+                                cx,
+                            );
+                            cx.emit(DismissEvent);
+                        }))
                 } else {
-                    KeyBinding::for_action(&*action, window, cx).map(|keybind| {
-                        Button::new("edit-debug-task", "Edit in debug.json")
-                            .label_size(LabelSize::Small)
-                            .key_binding(keybind)
-                            .on_click(move |_, window, cx| {
-                                window.dispatch_action(action.boxed_clone(), cx)
-                            })
-                    })
+                    Button::new("edit-debug-task", "Edit in debug.json")
+                        .label_size(LabelSize::Small)
+                        .key_binding(KeyBinding::for_action(&*action, cx))
+                        .on_click(move |_, window, cx| {
+                            window.dispatch_action(action.boxed_clone(), cx)
+                        })
                 }
             })
             .map(|this| {
                 if (current_modifiers.alt || self.matches.is_empty()) && !self.prompt.is_empty() {
                     let action = picker::ConfirmInput { secondary: false }.boxed_clone();
-                    this.children(KeyBinding::for_action(&*action, window, cx).map(|keybind| {
+                    this.child({
                         Button::new("launch-custom", "Launch Custom")
-                            .key_binding(keybind)
+                            .key_binding(KeyBinding::for_action(&*action, cx))
                             .on_click(move |_, window, cx| {
                                 window.dispatch_action(action.boxed_clone(), cx)
                             })
-                    }))
+                    })
                 } else {
-                    this.children(KeyBinding::for_action(&menu::Confirm, window, cx).map(
-                        |keybind| {
-                            let is_recent_selected =
-                                self.divider_index >= Some(self.selected_index);
-                            let run_entry_label =
-                                if is_recent_selected { "Rerun" } else { "Spawn" };
-
-                            Button::new("spawn", run_entry_label)
-                                .key_binding(keybind)
-                                .on_click(|_, window, cx| {
-                                    window.dispatch_action(menu::Confirm.boxed_clone(), cx);
-                                })
-                        },
-                    ))
+                    this.child({
+                        let is_recent_selected = self.divider_index >= Some(self.selected_index);
+                        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);
+                            })
+                    })
                 }
             });
         Some(footer.into_any_element())

crates/debugger_ui/src/session/running.rs 🔗

@@ -386,6 +386,7 @@ pub(crate) fn new_debugger_pane(
             Default::default(),
             None,
             NoAction.boxed_clone(),
+            true,
             window,
             cx,
         );
@@ -565,14 +566,13 @@ pub(crate) fn new_debugger_pane(
                                 }))
                                 .tooltip({
                                     let focus_handle = focus_handle.clone();
-                                    move |window, cx| {
+                                    move |_window, cx| {
                                         let zoomed_text =
                                             if zoomed { "Minimize" } else { "Expand" };
                                         Tooltip::for_action_in(
                                             zoomed_text,
                                             &ToggleExpandItem,
                                             &focus_handle,
-                                            window,
                                             cx,
                                         )
                                     }

crates/debugger_ui/src/session/running/breakpoint_list.rs 🔗

@@ -607,13 +607,12 @@ impl BreakpointList {
                 .when_some(toggle_label, |this, (label, meta)| {
                     this.tooltip({
                         let focus_handle = focus_handle.clone();
-                        move |window, cx| {
+                        move |_window, cx| {
                             Tooltip::with_meta_in(
                                 label,
                                 Some(&ToggleEnableBreakpoint),
                                 meta,
                                 &focus_handle,
-                                window,
                                 cx,
                             )
                         }
@@ -634,13 +633,12 @@ impl BreakpointList {
                     .when_some(remove_breakpoint_tooltip, |this, tooltip| {
                         this.tooltip({
                             let focus_handle = focus_handle.clone();
-                            move |window, cx| {
+                            move |_window, cx| {
                                 Tooltip::with_meta_in(
                                     "Remove Breakpoint",
                                     Some(&UnsetBreakpoint),
                                     tooltip,
                                     &focus_handle,
-                                    window,
                                     cx,
                                 )
                             }
@@ -819,7 +817,7 @@ impl LineBreakpoint {
             )
             .tooltip({
                 let focus_handle = focus_handle.clone();
-                move |window, cx| {
+                move |_window, cx| {
                     Tooltip::for_action_in(
                         if is_enabled {
                             "Disable Breakpoint"
@@ -828,7 +826,6 @@ impl LineBreakpoint {
                         },
                         &ToggleEnableBreakpoint,
                         &focus_handle,
-                        window,
                         cx,
                     )
                 }
@@ -980,7 +977,7 @@ impl DataBreakpoint {
                 )
                 .tooltip({
                     let focus_handle = focus_handle.clone();
-                    move |window, cx| {
+                    move |_window, cx| {
                         Tooltip::for_action_in(
                             if is_enabled {
                                 "Disable Data Breakpoint"
@@ -989,7 +986,6 @@ impl DataBreakpoint {
                             },
                             &ToggleEnableBreakpoint,
                             &focus_handle,
-                            window,
                             cx,
                         )
                     }
@@ -1085,7 +1081,7 @@ impl ExceptionBreakpoint {
                 )
                 .tooltip({
                     let focus_handle = focus_handle.clone();
-                    move |window, cx| {
+                    move |_window, cx| {
                         Tooltip::for_action_in(
                             if is_enabled {
                                 "Disable Exception Breakpoint"
@@ -1094,7 +1090,6 @@ impl ExceptionBreakpoint {
                             },
                             &ToggleEnableBreakpoint,
                             &focus_handle,
-                            window,
                             cx,
                         )
                     }
@@ -1402,12 +1397,11 @@ impl RenderOnce for BreakpointOptionsStrip {
                         .disabled(!supports_logs)
                         .toggle_state(self.is_toggled(ActiveBreakpointStripMode::Log))
                         .on_click(self.on_click_callback(ActiveBreakpointStripMode::Log))
-                        .tooltip(|window, cx| {
+                        .tooltip(|_window, cx|  {
                             Tooltip::with_meta(
                                 "Set Log Message",
                                 None,
                                 "Set log message to display (instead of stopping) when a breakpoint is hit.",
-                                window,
                                 cx,
                             )
                         }),
@@ -1438,12 +1432,11 @@ impl RenderOnce for BreakpointOptionsStrip {
                         .disabled(!supports_condition)
                         .toggle_state(self.is_toggled(ActiveBreakpointStripMode::Condition))
                         .on_click(self.on_click_callback(ActiveBreakpointStripMode::Condition))
-                        .tooltip(|window, cx| {
+                        .tooltip(|_window, cx|  {
                             Tooltip::with_meta(
                                 "Set Condition",
                                 None,
                                 "Set condition to evaluate when a breakpoint is hit. Program execution will stop only when the condition is met.",
-                                window,
                                 cx,
                             )
                         }),
@@ -1474,12 +1467,11 @@ impl RenderOnce for BreakpointOptionsStrip {
                         .disabled(!supports_hit_condition)
                         .toggle_state(self.is_toggled(ActiveBreakpointStripMode::HitCondition))
                         .on_click(self.on_click_callback(ActiveBreakpointStripMode::HitCondition))
-                        .tooltip(|window, cx| {
+                        .tooltip(|_window, cx|  {
                             Tooltip::with_meta(
                                 "Set Hit Condition",
                                 None,
                                 "Set expression that controls how many hits of the breakpoint are ignored.",
-                                window,
                                 cx,
                             )
                         }),

crates/debugger_ui/src/session/running/console.rs 🔗

@@ -484,12 +484,11 @@ impl Render for Console {
                             .tooltip({
                                 let query_focus_handle = query_focus_handle.clone();
 
-                                move |window, cx| {
+                                move |_window, cx| {
                                     Tooltip::for_action_in(
                                         "Evaluate",
                                         &Confirm,
                                         &query_focus_handle,
-                                        window,
                                         cx,
                                     )
                                 }
@@ -966,8 +965,12 @@ mod tests {
     ) {
         cx.set_state(input);
 
-        let buffer_position =
-            cx.editor(|editor, _, cx| editor.selections.newest::<Point>(cx).start);
+        let buffer_position = cx.editor(|editor, _, cx| {
+            editor
+                .selections
+                .newest::<Point>(&editor.display_snapshot(cx))
+                .start
+        });
 
         let snapshot = &cx.buffer_snapshot();
 

crates/debugger_ui/src/session/running/stack_frame_list.rs 🔗

@@ -872,8 +872,8 @@ impl StackFrameList {
                     "filter-by-visible-worktree-stack-frame-list",
                     IconName::ListFilter,
                 )
-                .tooltip(move |window, cx| {
-                    Tooltip::for_action(tooltip_title, &ToggleUserFrames, window, cx)
+                .tooltip(move |_window, cx| {
+                    Tooltip::for_action(tooltip_title, &ToggleUserFrames, cx)
                 })
                 .toggle_state(self.list_filter == StackFrameFilter::OnlyUserFrames)
                 .icon_size(IconSize::Small)

crates/debugger_ui/src/session/running/variable_list.rs 🔗

@@ -1306,14 +1306,8 @@ impl VariableList {
                             .ok();
                         }
                     })
-                    .tooltip(move |window, cx| {
-                        Tooltip::for_action_in(
-                            "Remove Watch",
-                            &RemoveWatch,
-                            &focus_handle,
-                            window,
-                            cx,
-                        )
+                    .tooltip(move |_window, cx| {
+                        Tooltip::for_action_in("Remove Watch", &RemoveWatch, &focus_handle, cx)
                     })
                     .icon_size(ui::IconSize::Indicator),
                 ),

crates/debugger_ui/src/stack_trace_view.rs 🔗

@@ -55,7 +55,10 @@ impl StackTraceView {
         cx.subscribe_in(&editor, window, |this, editor, event, window, cx| {
             if let EditorEvent::SelectionsChanged { local: true } = event {
                 let excerpt_id = editor.update(cx, |editor, cx| {
-                    let position: Point = editor.selections.newest(cx).head();
+                    let position: Point = editor
+                        .selections
+                        .newest(&editor.display_snapshot(cx))
+                        .head();
 
                     editor
                         .snapshot(window, cx)

crates/debugger_ui/src/tests/new_process_modal.rs 🔗

@@ -231,7 +231,10 @@ async fn test_save_debug_scenario_to_file(executor: BackgroundExecutor, cx: &mut
 
     editor.update(cx, |editor, cx| {
         assert_eq!(
-            editor.selections.newest::<Point>(cx).head(),
+            editor
+                .selections
+                .newest::<Point>(&editor.display_snapshot(cx))
+                .head(),
             Point::new(5, 2)
         )
     });

crates/deepseek/Cargo.toml 🔗

@@ -22,4 +22,3 @@ http_client.workspace = true
 schemars = { workspace = true, optional = true }
 serde.workspace = true
 serde_json.workspace = true
-workspace-hack.workspace = true

crates/denoise/Cargo.toml 🔗

@@ -18,4 +18,3 @@ rodio = { workspace = true, features = ["wav_output"] }
 rustfft = { version = "6.2.0", features = ["avx"] }
 realfft = "3.4.0"
 thiserror.workspace = true
-workspace-hack.workspace = true

crates/diagnostics/Cargo.toml 🔗

@@ -34,7 +34,6 @@ theme.workspace = true
 ui.workspace = true
 util.workspace = true
 workspace.workspace = true
-workspace-hack.workspace = true
 
 [dev-dependencies]
 client = { workspace = true, features = ["test-support"] }

crates/diagnostics/src/buffer_diagnostics.rs 🔗

@@ -693,11 +693,11 @@ impl Item for BufferDiagnosticsEditor {
         _workspace_id: Option<workspace::WorkspaceId>,
         window: &mut Window,
         cx: &mut Context<Self>,
-    ) -> Option<Entity<Self>>
+    ) -> Task<Option<Entity<Self>>>
     where
         Self: Sized,
     {
-        Some(cx.new(|cx| {
+        Task::ready(Some(cx.new(|cx| {
             BufferDiagnosticsEditor::new(
                 self.project_path.clone(),
                 self.project.clone(),
@@ -706,7 +706,7 @@ impl Item for BufferDiagnosticsEditor {
                 window,
                 cx,
             )
-        }))
+        })))
     }
 
     fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) {

crates/diagnostics/src/diagnostics.rs 🔗

@@ -732,11 +732,11 @@ impl Item for ProjectDiagnosticsEditor {
         _workspace_id: Option<workspace::WorkspaceId>,
         window: &mut Window,
         cx: &mut Context<Self>,
-    ) -> Option<Entity<Self>>
+    ) -> Task<Option<Entity<Self>>>
     where
         Self: Sized,
     {
-        Some(cx.new(|cx| {
+        Task::ready(Some(cx.new(|cx| {
             ProjectDiagnosticsEditor::new(
                 self.include_warnings,
                 self.project.clone(),
@@ -744,7 +744,7 @@ impl Item for ProjectDiagnosticsEditor {
                 window,
                 cx,
             )
-        }))
+        })))
     }
 
     fn is_dirty(&self, cx: &App) -> bool {

crates/diagnostics/src/diagnostics_tests.rs 🔗

@@ -1,9 +1,9 @@
 use super::*;
 use collections::{HashMap, HashSet};
 use editor::{
-    DisplayPoint, EditorSettings,
+    DisplayPoint, EditorSettings, Inlay,
     actions::{GoToDiagnostic, GoToPreviousDiagnostic, Hover, MoveToBeginning},
-    display_map::{DisplayRow, Inlay},
+    display_map::DisplayRow,
     test::{
         editor_content_with_blocks, editor_lsp_test_context::EditorLspTestContext,
         editor_test_context::EditorTestContext,
@@ -1341,7 +1341,7 @@ async fn test_hover_diagnostic_and_info_popovers(cx: &mut gpui::TestAppContext)
             range: Some(range),
         }))
     });
-    let delay = cx.update(|_, cx| EditorSettings::get_global(cx).hover_popover_delay + 1);
+    let delay = cx.update(|_, cx| EditorSettings::get_global(cx).hover_popover_delay.0 + 1);
     cx.background_executor
         .advance_clock(Duration::from_millis(delay));
 

crates/diagnostics/src/items.rs 🔗

@@ -67,11 +67,10 @@ impl Render for DiagnosticIndicator {
             Some(
                 Button::new("diagnostic_message", SharedString::new(message))
                     .label_size(LabelSize::Small)
-                    .tooltip(|window, cx| {
+                    .tooltip(|_window, cx| {
                         Tooltip::for_action(
                             "Next Diagnostic",
                             &editor::actions::GoToDiagnostic::default(),
-                            window,
                             cx,
                         )
                     })
@@ -87,8 +86,8 @@ impl Render for DiagnosticIndicator {
             .child(
                 ButtonLike::new("diagnostic-indicator")
                     .child(diagnostic_indicator)
-                    .tooltip(|window, cx| {
-                        Tooltip::for_action("Project Diagnostics", &Deploy, window, cx)
+                    .tooltip(move |_window, cx| {
+                        Tooltip::for_action("Project Diagnostics", &Deploy, cx)
                     })
                     .on_click(cx.listener(|this, _, window, cx| {
                         if let Some(workspace) = this.workspace.upgrade() {
@@ -170,7 +169,10 @@ impl DiagnosticIndicator {
     fn update(&mut self, editor: Entity<Editor>, window: &mut Window, cx: &mut Context<Self>) {
         let (buffer, cursor_position) = editor.update(cx, |editor, cx| {
             let buffer = editor.buffer().read(cx).snapshot(cx);
-            let cursor_position = editor.selections.newest::<usize>(cx).head();
+            let cursor_position = editor
+                .selections
+                .newest::<usize>(&editor.display_snapshot(cx))
+                .head();
             (buffer, cursor_position)
         });
         let new_diagnostic = buffer

crates/docs_preprocessor/Cargo.toml 🔗

@@ -17,7 +17,6 @@ serde.workspace = true
 serde_json.workspace = true
 settings.workspace = true
 util.workspace = true
-workspace-hack.workspace = true
 zed.workspace = true
 zlog.workspace = true
 task.workspace = true

crates/docs_preprocessor/src/main.rs 🔗

@@ -203,6 +203,10 @@ fn template_big_table_of_actions(book: &mut Book) {
     });
 }
 
+fn format_binding(binding: String) -> String {
+    binding.replace("\\", "\\\\")
+}
+
 fn template_and_validate_keybindings(book: &mut Book, errors: &mut HashSet<PreprocessorError>) {
     let regex = Regex::new(r"\{#kb (.*?)\}").unwrap();
 
@@ -223,7 +227,10 @@ fn template_and_validate_keybindings(book: &mut Book, errors: &mut HashSet<Prepr
                     return "<div>No default binding</div>".to_string();
                 }
 
-                format!("<kbd class=\"keybinding\">{macos_binding}|{linux_binding}</kbd>")
+                let formatted_macos_binding = format_binding(macos_binding);
+                let formatted_linux_binding = format_binding(linux_binding);
+
+                format!("<kbd class=\"keybinding\">{formatted_macos_binding}|{formatted_linux_binding}</kbd>")
             })
             .into_owned()
     });

crates/edit_prediction/Cargo.toml 🔗

@@ -15,4 +15,3 @@ path = "src/edit_prediction.rs"
 client.workspace = true
 gpui.workspace = true
 language.workspace = true
-workspace-hack.workspace = true

crates/edit_prediction_button/Cargo.toml 🔗

@@ -32,7 +32,6 @@ settings.workspace = true
 supermaven.workspace = true
 telemetry.workspace = true
 ui.workspace = true
-workspace-hack.workspace = true
 workspace.workspace = true
 zed_actions.workspace = true
 zeta.workspace = true

crates/edit_prediction_button/src/edit_prediction_button.rs 🔗

@@ -123,8 +123,8 @@ impl Render for EditPredictionButton {
                                     });
                                 }
                             }))
-                            .tooltip(|window, cx| {
-                                Tooltip::for_action("GitHub Copilot", &ToggleMenu, window, cx)
+                            .tooltip(|_window, cx| {
+                                Tooltip::for_action("GitHub Copilot", &ToggleMenu, cx)
                             }),
                     );
                 }
@@ -146,9 +146,7 @@ impl Render for EditPredictionButton {
                         .anchor(Corner::BottomRight)
                         .trigger_with_tooltip(
                             IconButton::new("copilot-icon", icon),
-                            |window, cx| {
-                                Tooltip::for_action("GitHub Copilot", &ToggleMenu, window, cx)
-                            },
+                            |_window, cx| Tooltip::for_action("GitHub Copilot", &ToggleMenu, cx),
                         )
                         .with_handle(self.popover_menu_handle.clone()),
                 )
@@ -220,12 +218,7 @@ impl Render for EditPredictionButton {
                             IconButton::new("supermaven-icon", icon),
                             move |window, cx| {
                                 if has_menu {
-                                    Tooltip::for_action(
-                                        tooltip_text.clone(),
-                                        &ToggleMenu,
-                                        window,
-                                        cx,
-                                    )
+                                    Tooltip::for_action(tooltip_text.clone(), &ToggleMenu, cx)
                                 } else {
                                     Tooltip::text(tooltip_text.clone())(window, cx)
                                 }
@@ -288,9 +281,7 @@ impl Render for EditPredictionButton {
                                             cx.theme().colors().status_bar_background,
                                         ))
                                 }),
-                            move |window, cx| {
-                                Tooltip::for_action("Codestral", &ToggleMenu, window, cx)
-                            },
+                            move |_window, cx| Tooltip::for_action("Codestral", &ToggleMenu, cx),
                         )
                         .with_handle(self.popover_menu_handle.clone()),
                 )
@@ -317,14 +308,8 @@ impl Render for EditPredictionButton {
                             .shape(IconButtonShape::Square)
                             .indicator(Indicator::dot().color(Color::Muted))
                             .indicator_border_color(Some(cx.theme().colors().status_bar_background))
-                            .tooltip(move |window, cx| {
-                                Tooltip::with_meta(
-                                    "Edit Predictions",
-                                    None,
-                                    tooltip_meta,
-                                    window,
-                                    cx,
-                                )
+                            .tooltip(move |_window, cx| {
+                                Tooltip::with_meta("Edit Predictions", None, tooltip_meta, cx)
                             })
                             .on_click(cx.listener(move |_, _, window, cx| {
                                 telemetry::event!(
@@ -365,16 +350,15 @@ impl Render for EditPredictionButton {
                         },
                     )
                     .when(!self.popover_menu_handle.is_deployed(), |element| {
-                        element.tooltip(move |window, cx| {
+                        element.tooltip(move |_window, cx| {
                             if enabled {
                                 if show_editor_predictions {
-                                    Tooltip::for_action("Edit Prediction", &ToggleMenu, window, cx)
+                                    Tooltip::for_action("Edit Prediction", &ToggleMenu, cx)
                                 } else {
                                     Tooltip::with_meta(
                                         "Edit Prediction",
                                         Some(&ToggleMenu),
                                         "Hidden For This File",
-                                        window,
                                         cx,
                                     )
                                 }
@@ -383,7 +367,6 @@ impl Render for EditPredictionButton {
                                     "Edit Prediction",
                                     Some(&ToggleMenu),
                                     "Disabled For This File",
-                                    window,
                                     cx,
                                 )
                             }

crates/edit_prediction_context/Cargo.toml 🔗

@@ -33,7 +33,6 @@ strum.workspace = true
 text.workspace = true
 tree-sitter.workspace = true
 util.workspace = true
-workspace-hack.workspace = true
 
 [dev-dependencies]
 clap.workspace = true

crates/edit_prediction_context/src/edit_prediction_context.rs 🔗

@@ -27,9 +27,9 @@ pub use predict_edits_v3::Line;
 #[derive(Clone, Debug, PartialEq)]
 pub struct EditPredictionContextOptions {
     pub use_imports: bool,
-    pub use_references: bool,
     pub excerpt: EditPredictionExcerptOptions,
     pub score: EditPredictionScoreOptions,
+    pub max_retrieved_declarations: u8,
 }
 
 #[derive(Clone, Debug)]
@@ -118,7 +118,7 @@ impl EditPredictionContext {
         )?;
         let excerpt_text = excerpt.text(buffer);
 
-        let declarations = if options.use_references
+        let declarations = if options.max_retrieved_declarations > 0
             && let Some(index_state) = index_state
         {
             let excerpt_occurrences =
@@ -136,7 +136,7 @@ impl EditPredictionContext {
 
             let references = get_references(&excerpt, &excerpt_text, buffer);
 
-            scored_declarations(
+            let mut declarations = scored_declarations(
                 &options.score,
                 &index_state,
                 &excerpt,
@@ -146,7 +146,10 @@ impl EditPredictionContext {
                 references,
                 cursor_offset_in_file,
                 buffer,
-            )
+            );
+            // TODO [zeta2] if we need this when we ship, we should probably do it in a smarter way
+            declarations.truncate(options.max_retrieved_declarations as usize);
+            declarations
         } else {
             vec![]
         };
@@ -200,7 +203,6 @@ mod tests {
                     buffer_snapshot,
                     EditPredictionContextOptions {
                         use_imports: true,
-                        use_references: true,
                         excerpt: EditPredictionExcerptOptions {
                             max_bytes: 60,
                             min_bytes: 10,
@@ -209,6 +211,7 @@ mod tests {
                         score: EditPredictionScoreOptions {
                             omit_excerpt_overlaps: true,
                         },
+                        max_retrieved_declarations: u8::MAX,
                     },
                     Some(index.clone()),
                     cx,

crates/edit_prediction_context/src/syntax_index.rs 🔗

@@ -854,7 +854,7 @@ mod tests {
     }
 
     #[gpui::test]
-    async fn test_declarations_limt(cx: &mut TestAppContext) {
+    async fn test_declarations_limit(cx: &mut TestAppContext) {
         let (_, index, rust_lang_id) = init_test(cx).await;
 
         let index_state = index.read_with(cx, |index, _cx| index.state().clone());

crates/editor/Cargo.toml 🔗

@@ -64,6 +64,7 @@ project.workspace = true
 rand.workspace = true
 regex.workspace = true
 rpc.workspace = true
+rope.workspace = true
 schemars.workspace = true
 serde.workspace = true
 serde_json.workspace = true
@@ -92,7 +93,6 @@ uuid.workspace = true
 vim_mode_setting.workspace = true
 workspace.workspace = true
 zed_actions.workspace = true
-workspace-hack.workspace = true
 
 [dev-dependencies]
 criterion.workspace = true

crates/editor/src/actions.rs 🔗

@@ -460,6 +460,8 @@ actions!(
         /// Expands all diff hunks in the editor.
         #[action(deprecated_aliases = ["editor::ExpandAllHunkDiffs"])]
         ExpandAllDiffHunks,
+        /// Collapses all diff hunks in the editor.
+        CollapseAllDiffHunks,
         /// Expands macros recursively at cursor position.
         ExpandMacroRecursively,
         /// Finds all references to the symbol at cursor.

crates/editor/src/display_map.rs 🔗

@@ -27,7 +27,7 @@ mod tab_map;
 mod wrap_map;
 
 use crate::{
-    EditorStyle, InlayId, RowExt, hover_links::InlayHighlight, movement::TextLayoutDetails,
+    EditorStyle, RowExt, hover_links::InlayHighlight, inlays::Inlay, movement::TextLayoutDetails,
 };
 pub use block_map::{
     Block, BlockChunks as DisplayChunks, BlockContext, BlockId, BlockMap, BlockPlacement,
@@ -42,7 +42,6 @@ pub use fold_map::{
     ChunkRenderer, ChunkRendererContext, ChunkRendererId, Fold, FoldId, FoldPlaceholder, FoldPoint,
 };
 use gpui::{App, Context, Entity, Font, HighlightStyle, LineLayout, Pixels, UnderlineStyle};
-pub use inlay_map::Inlay;
 use inlay_map::InlaySnapshot;
 pub use inlay_map::{InlayOffset, InlayPoint};
 pub use invisibles::{is_invisible, replacement};
@@ -50,9 +49,10 @@ use language::{
     OffsetUtf16, Point, Subscription as BufferSubscription, language_settings::language_settings,
 };
 use multi_buffer::{
-    Anchor, AnchorRangeExt, ExcerptId, MultiBuffer, MultiBufferPoint, MultiBufferRow,
-    MultiBufferSnapshot, RowInfo, ToOffset, ToPoint,
+    Anchor, AnchorRangeExt, MultiBuffer, MultiBufferPoint, MultiBufferRow, MultiBufferSnapshot,
+    RowInfo, ToOffset, ToPoint,
 };
+use project::InlayId;
 use project::project_settings::DiagnosticSeverity;
 use serde::Deserialize;
 
@@ -594,21 +594,6 @@ impl DisplayMap {
         self.block_map.read(snapshot, edits);
     }
 
-    pub fn remove_inlays_for_excerpts(&mut self, excerpts_removed: &[ExcerptId]) {
-        let to_remove = self
-            .inlay_map
-            .current_inlays()
-            .filter_map(|inlay| {
-                if excerpts_removed.contains(&inlay.position.excerpt_id) {
-                    Some(inlay.id)
-                } else {
-                    None
-                }
-            })
-            .collect::<Vec<_>>();
-        self.inlay_map.splice(&to_remove, Vec::new());
-    }
-
     fn tab_size(buffer: &Entity<MultiBuffer>, cx: &App) -> NonZeroU32 {
         let buffer = buffer.read(cx).as_singleton().map(|buffer| buffer.read(cx));
         let language = buffer

crates/editor/src/display_map/block_map.rs 🔗

@@ -26,8 +26,8 @@ use sum_tree::{Bias, ContextLessSummary, Dimensions, SumTree, TreeMap};
 use text::{BufferId, Edit};
 use ui::ElementId;
 
-const NEWLINES: &[u8; u128::BITS as usize] = &[b'\n'; _];
-const BULLETS: &[u8; u128::BITS as usize] = &[b'*'; _];
+const NEWLINES: &[u8; rope::Chunk::MASK_BITS] = &[b'\n'; _];
+const BULLETS: &[u8; rope::Chunk::MASK_BITS] = &[b'*'; _];
 
 /// Tracks custom blocks such as diagnostics that should be displayed within buffer.
 ///
@@ -1186,18 +1186,14 @@ impl BlockMapWriter<'_> {
         self.0.sync(wrap_snapshot, edits);
     }
 
-    pub fn remove_intersecting_replace_blocks<T>(
+    pub fn remove_intersecting_replace_blocks(
         &mut self,
-        ranges: impl IntoIterator<Item = Range<T>>,
+        ranges: impl IntoIterator<Item = Range<usize>>,
         inclusive: bool,
-    ) where
-        T: ToOffset,
-    {
+    ) {
         let wrap_snapshot = self.0.wrap_snapshot.borrow();
         let mut blocks_to_remove = HashSet::default();
         for range in ranges {
-            let range = range.start.to_offset(wrap_snapshot.buffer_snapshot())
-                ..range.end.to_offset(wrap_snapshot.buffer_snapshot());
             for block in self.blocks_intersecting_buffer_range(range, inclusive) {
                 if matches!(block.placement, BlockPlacement::Replace(_)) {
                     blocks_to_remove.insert(block.id);
@@ -1521,10 +1517,11 @@ impl BlockSnapshot {
     }
 
     pub(super) fn line_len(&self, row: BlockRow) -> u32 {
-        let mut cursor = self.transforms.cursor::<Dimensions<BlockRow, WrapRow>>(());
-        cursor.seek(&BlockRow(row.0), Bias::Right);
-        if let Some(transform) = cursor.item() {
-            let Dimensions(output_start, input_start, _) = cursor.start();
+        let (start, _, item) =
+            self.transforms
+                .find::<Dimensions<BlockRow, WrapRow>, _>((), &row, Bias::Right);
+        if let Some(transform) = item {
+            let Dimensions(output_start, input_start, _) = start;
             let overshoot = row.0 - output_start.0;
             if transform.block.is_some() {
                 0
@@ -1539,15 +1536,13 @@ impl BlockSnapshot {
     }
 
     pub(super) fn is_block_line(&self, row: BlockRow) -> bool {
-        let mut cursor = self.transforms.cursor::<Dimensions<BlockRow, WrapRow>>(());
-        cursor.seek(&row, Bias::Right);
-        cursor.item().is_some_and(|t| t.block.is_some())
+        let (_, _, item) = self.transforms.find::<BlockRow, _>((), &row, Bias::Right);
+        item.is_some_and(|t| t.block.is_some())
     }
 
     pub(super) fn is_folded_buffer_header(&self, row: BlockRow) -> bool {
-        let mut cursor = self.transforms.cursor::<Dimensions<BlockRow, WrapRow>>(());
-        cursor.seek(&row, Bias::Right);
-        let Some(transform) = cursor.item() else {
+        let (_, _, item) = self.transforms.find::<BlockRow, _>((), &row, Bias::Right);
+        let Some(transform) = item else {
             return false;
         };
         matches!(transform.block, Some(Block::FoldedBuffer { .. }))
@@ -1557,9 +1552,10 @@ impl BlockSnapshot {
         let wrap_point = self
             .wrap_snapshot
             .make_wrap_point(Point::new(row.0, 0), Bias::Left);
-        let mut cursor = self.transforms.cursor::<Dimensions<WrapRow, BlockRow>>(());
-        cursor.seek(&WrapRow(wrap_point.row()), Bias::Right);
-        cursor.item().is_some_and(|transform| {
+        let (_, _, item) =
+            self.transforms
+                .find::<WrapRow, _>((), &WrapRow(wrap_point.row()), Bias::Right);
+        item.is_some_and(|transform| {
             transform
                 .block
                 .as_ref()
@@ -1627,13 +1623,16 @@ impl BlockSnapshot {
     }
 
     pub fn to_block_point(&self, wrap_point: WrapPoint) -> BlockPoint {
-        let mut cursor = self.transforms.cursor::<Dimensions<WrapRow, BlockRow>>(());
-        cursor.seek(&WrapRow(wrap_point.row()), Bias::Right);
-        if let Some(transform) = cursor.item() {
+        let (start, _, item) = self.transforms.find::<Dimensions<WrapRow, BlockRow>, _>(
+            (),
+            &WrapRow(wrap_point.row()),
+            Bias::Right,
+        );
+        if let Some(transform) = item {
             if transform.block.is_some() {
-                BlockPoint::new(cursor.start().1.0, 0)
+                BlockPoint::new(start.1.0, 0)
             } else {
-                let Dimensions(input_start_row, output_start_row, _) = cursor.start();
+                let Dimensions(input_start_row, output_start_row, _) = start;
                 let input_start = Point::new(input_start_row.0, 0);
                 let output_start = Point::new(output_start_row.0, 0);
                 let input_overshoot = wrap_point.0 - input_start;
@@ -1645,26 +1644,29 @@ impl BlockSnapshot {
     }
 
     pub fn to_wrap_point(&self, block_point: BlockPoint, bias: Bias) -> WrapPoint {
-        let mut cursor = self.transforms.cursor::<Dimensions<BlockRow, WrapRow>>(());
-        cursor.seek(&BlockRow(block_point.row), Bias::Right);
-        if let Some(transform) = cursor.item() {
+        let (start, end, item) = self.transforms.find::<Dimensions<BlockRow, WrapRow>, _>(
+            (),
+            &BlockRow(block_point.row),
+            Bias::Right,
+        );
+        if let Some(transform) = item {
             match transform.block.as_ref() {
                 Some(block) => {
                     if block.place_below() {
-                        let wrap_row = cursor.start().1.0 - 1;
+                        let wrap_row = start.1.0 - 1;
                         WrapPoint::new(wrap_row, self.wrap_snapshot.line_len(wrap_row))
                     } else if block.place_above() {
-                        WrapPoint::new(cursor.start().1.0, 0)
+                        WrapPoint::new(start.1.0, 0)
                     } else if bias == Bias::Left {
-                        WrapPoint::new(cursor.start().1.0, 0)
+                        WrapPoint::new(start.1.0, 0)
                     } else {
-                        let wrap_row = cursor.end().1.0 - 1;
+                        let wrap_row = end.1.0 - 1;
                         WrapPoint::new(wrap_row, self.wrap_snapshot.line_len(wrap_row))
                     }
                 }
                 None => {
-                    let overshoot = block_point.row - cursor.start().0.0;
-                    let wrap_row = cursor.start().1.0 + overshoot;
+                    let overshoot = block_point.row - start.0.0;
+                    let wrap_row = start.1.0 + overshoot;
                     WrapPoint::new(wrap_row, block_point.column)
                 }
             }
@@ -1777,11 +1779,11 @@ impl<'a> Iterator for BlockChunks<'a> {
 
         if self.masked {
             // Not great for multibyte text because to keep cursor math correct we
-            // need to have the same number of bytes in the input as output.
+            // need to have the same number of chars in the input as output.
             let chars_count = prefix.chars().count();
             let bullet_len = chars_count;
             prefix = unsafe { std::str::from_utf8_unchecked(&BULLETS[..bullet_len]) };
-            chars = 1u128.unbounded_shl(bullet_len as u32) - 1;
+            chars = 1u128.unbounded_shl(bullet_len as u32).wrapping_sub(1);
             tabs = 0;
         }
 
@@ -3564,8 +3566,12 @@ mod tests {
 
         let mut writer = block_map.write(wraps_snapshot.clone(), Default::default());
         writer.remove_intersecting_replace_blocks(
-            [buffer_snapshot.anchor_after(Point::new(1, 0))
-                ..buffer_snapshot.anchor_after(Point::new(1, 0))],
+            [buffer_snapshot
+                .anchor_after(Point::new(1, 0))
+                .to_offset(&buffer_snapshot)
+                ..buffer_snapshot
+                    .anchor_after(Point::new(1, 0))
+                    .to_offset(&buffer_snapshot)],
             false,
         );
         let blocks_snapshot = block_map.read(wraps_snapshot, Default::default());

crates/editor/src/display_map/custom_highlights.rs 🔗

@@ -132,37 +132,31 @@ impl<'a> Iterator for CustomHighlightsChunks<'a> {
             }
         }
 
-        let chunk = self
-            .buffer_chunk
-            .get_or_insert_with(|| self.buffer_chunks.next().unwrap_or_default());
-        if chunk.text.is_empty() {
+        let chunk = match &mut self.buffer_chunk {
+            Some(it) => it,
+            slot => slot.insert(self.buffer_chunks.next()?),
+        };
+        while chunk.text.is_empty() {
             *chunk = self.buffer_chunks.next()?;
         }
 
         let split_idx = chunk.text.len().min(next_highlight_endpoint - self.offset);
         let (prefix, suffix) = chunk.text.split_at(split_idx);
-
-        let (chars, tabs) = if split_idx == 128 {
-            let output = (chunk.chars, chunk.tabs);
-            chunk.chars = 0;
-            chunk.tabs = 0;
-            output
-        } else {
-            let mask = (1 << split_idx) - 1;
-            let output = (chunk.chars & mask, chunk.tabs & mask);
-            chunk.chars = chunk.chars >> split_idx;
-            chunk.tabs = chunk.tabs >> split_idx;
-            output
-        };
-
-        chunk.text = suffix;
         self.offset += prefix.len();
+
+        let mask = 1u128.unbounded_shl(split_idx as u32).wrapping_sub(1);
+        let chars = chunk.chars & mask;
+        let tabs = chunk.tabs & mask;
         let mut prefix = Chunk {
             text: prefix,
             chars,
             tabs,
             ..chunk.clone()
         };
+
+        chunk.chars = chunk.chars.unbounded_shr(split_idx as u32);
+        chunk.tabs = chunk.tabs.unbounded_shr(split_idx as u32);
+        chunk.text = suffix;
         if !self.active_highlights.is_empty() {
             prefix.highlight_style = self
                 .active_highlights

crates/editor/src/display_map/fold_map.rs 🔗

@@ -1,4 +1,4 @@
-use crate::{InlayId, display_map::inlay_map::InlayChunk};
+use crate::display_map::inlay_map::InlayChunk;
 
 use super::{
     Highlights,
@@ -9,6 +9,7 @@ use language::{Edit, HighlightId, Point, TextSummary};
 use multi_buffer::{
     Anchor, AnchorRangeExt, MultiBufferRow, MultiBufferSnapshot, RowInfo, ToOffset,
 };
+use project::InlayId;
 use std::{
     any::TypeId,
     cmp::{self, Ordering},
@@ -98,28 +99,26 @@ impl FoldPoint {
     }
 
     pub fn to_inlay_point(self, snapshot: &FoldSnapshot) -> InlayPoint {
-        let mut cursor = snapshot
+        let (start, _, _) = snapshot
             .transforms
-            .cursor::<Dimensions<FoldPoint, InlayPoint>>(());
-        cursor.seek(&self, Bias::Right);
-        let overshoot = self.0 - cursor.start().0.0;
-        InlayPoint(cursor.start().1.0 + overshoot)
+            .find::<Dimensions<FoldPoint, InlayPoint>, _>((), &self, Bias::Right);
+        let overshoot = self.0 - start.0.0;
+        InlayPoint(start.1.0 + overshoot)
     }
 
     pub fn to_offset(self, snapshot: &FoldSnapshot) -> FoldOffset {
-        let mut cursor = snapshot
+        let (start, _, item) = snapshot
             .transforms
-            .cursor::<Dimensions<FoldPoint, TransformSummary>>(());
-        cursor.seek(&self, Bias::Right);
-        let overshoot = self.0 - cursor.start().1.output.lines;
-        let mut offset = cursor.start().1.output.len;
+            .find::<Dimensions<FoldPoint, TransformSummary>, _>((), &self, Bias::Right);
+        let overshoot = self.0 - start.1.output.lines;
+        let mut offset = start.1.output.len;
         if !overshoot.is_zero() {
-            let transform = cursor.item().expect("display point out of range");
+            let transform = item.expect("display point out of range");
             assert!(transform.placeholder.is_none());
             let end_inlay_offset = snapshot
                 .inlay_snapshot
-                .to_offset(InlayPoint(cursor.start().1.input.lines + overshoot));
-            offset += end_inlay_offset.0 - cursor.start().1.input.len;
+                .to_offset(InlayPoint(start.1.input.lines + overshoot));
+            offset += end_inlay_offset.0 - start.1.input.len;
         }
         FoldOffset(offset)
     }
@@ -706,19 +705,18 @@ impl FoldSnapshot {
     }
 
     pub fn to_fold_point(&self, point: InlayPoint, bias: Bias) -> FoldPoint {
-        let mut cursor = self
+        let (start, end, item) = self
             .transforms
-            .cursor::<Dimensions<InlayPoint, FoldPoint>>(());
-        cursor.seek(&point, Bias::Right);
-        if cursor.item().is_some_and(|t| t.is_fold()) {
-            if bias == Bias::Left || point == cursor.start().0 {
-                cursor.start().1
+            .find::<Dimensions<InlayPoint, FoldPoint>, _>((), &point, Bias::Right);
+        if item.is_some_and(|t| t.is_fold()) {
+            if bias == Bias::Left || point == start.0 {
+                start.1
             } else {
-                cursor.end().1
+                end.1
             }
         } else {
-            let overshoot = point.0 - cursor.start().0.0;
-            FoldPoint(cmp::min(cursor.start().1.0 + overshoot, cursor.end().1.0))
+            let overshoot = point.0 - start.0.0;
+            FoldPoint(cmp::min(start.1.0 + overshoot, end.1.0))
         }
     }
 
@@ -787,9 +785,10 @@ impl FoldSnapshot {
     {
         let buffer_offset = offset.to_offset(&self.inlay_snapshot.buffer);
         let inlay_offset = self.inlay_snapshot.to_inlay_offset(buffer_offset);
-        let mut cursor = self.transforms.cursor::<InlayOffset>(());
-        cursor.seek(&inlay_offset, Bias::Right);
-        cursor.item().is_some_and(|t| t.placeholder.is_some())
+        let (_, _, item) = self
+            .transforms
+            .find::<InlayOffset, _>((), &inlay_offset, Bias::Right);
+        item.is_some_and(|t| t.placeholder.is_some())
     }
 
     pub fn is_line_folded(&self, buffer_row: MultiBufferRow) -> bool {
@@ -891,23 +890,22 @@ impl FoldSnapshot {
     }
 
     pub fn clip_point(&self, point: FoldPoint, bias: Bias) -> FoldPoint {
-        let mut cursor = self
+        let (start, end, item) = self
             .transforms
-            .cursor::<Dimensions<FoldPoint, InlayPoint>>(());
-        cursor.seek(&point, Bias::Right);
-        if let Some(transform) = cursor.item() {
-            let transform_start = cursor.start().0.0;
+            .find::<Dimensions<FoldPoint, InlayPoint>, _>((), &point, Bias::Right);
+        if let Some(transform) = item {
+            let transform_start = start.0.0;
             if transform.placeholder.is_some() {
                 if point.0 == transform_start || matches!(bias, Bias::Left) {
                     FoldPoint(transform_start)
                 } else {
-                    FoldPoint(cursor.end().0.0)
+                    FoldPoint(end.0.0)
                 }
             } else {
                 let overshoot = InlayPoint(point.0 - transform_start);
-                let inlay_point = cursor.start().1 + overshoot;
+                let inlay_point = start.1 + overshoot;
                 let clipped_inlay_point = self.inlay_snapshot.clip_point(inlay_point, bias);
-                FoldPoint(cursor.start().0.0 + (clipped_inlay_point - cursor.start().1).0)
+                FoldPoint(start.0.0 + (clipped_inlay_point - start.1).0)
             }
         } else {
             FoldPoint(self.transforms.summary().output.lines)
@@ -1439,14 +1437,15 @@ impl<'a> Iterator for FoldChunks<'a> {
             let transform_end = self.transform_cursor.end().1;
             let chunk_end = buffer_chunk_end.min(transform_end);
 
-            chunk.text = &chunk.text
-                [(self.inlay_offset - buffer_chunk_start).0..(chunk_end - buffer_chunk_start).0];
+            let bit_start = (self.inlay_offset - buffer_chunk_start).0;
+            let bit_end = (chunk_end - buffer_chunk_start).0;
+            chunk.text = &chunk.text[bit_start..bit_end];
 
             let bit_end = (chunk_end - buffer_chunk_start).0;
             let mask = 1u128.unbounded_shl(bit_end as u32).wrapping_sub(1);
 
-            chunk.tabs = (chunk.tabs >> (self.inlay_offset - buffer_chunk_start).0) & mask;
-            chunk.chars = (chunk.chars >> (self.inlay_offset - buffer_chunk_start).0) & mask;
+            chunk.tabs = (chunk.tabs >> bit_start) & mask;
+            chunk.chars = (chunk.chars >> bit_start) & mask;
 
             if chunk_end == transform_end {
                 self.transform_cursor.next();
@@ -1480,28 +1479,26 @@ pub struct FoldOffset(pub usize);
 
 impl FoldOffset {
     pub fn to_point(self, snapshot: &FoldSnapshot) -> FoldPoint {
-        let mut cursor = snapshot
+        let (start, _, item) = snapshot
             .transforms
-            .cursor::<Dimensions<FoldOffset, TransformSummary>>(());
-        cursor.seek(&self, Bias::Right);
-        let overshoot = if cursor.item().is_none_or(|t| t.is_fold()) {
-            Point::new(0, (self.0 - cursor.start().0.0) as u32)
+            .find::<Dimensions<FoldOffset, TransformSummary>, _>((), &self, Bias::Right);
+        let overshoot = if item.is_none_or(|t| t.is_fold()) {
+            Point::new(0, (self.0 - start.0.0) as u32)
         } else {
-            let inlay_offset = cursor.start().1.input.len + self.0 - cursor.start().0.0;
+            let inlay_offset = start.1.input.len + self.0 - start.0.0;
             let inlay_point = snapshot.inlay_snapshot.to_point(InlayOffset(inlay_offset));
-            inlay_point.0 - cursor.start().1.input.lines
+            inlay_point.0 - start.1.input.lines
         };
-        FoldPoint(cursor.start().1.output.lines + overshoot)
+        FoldPoint(start.1.output.lines + overshoot)
     }
 
     #[cfg(test)]
     pub fn to_inlay_offset(self, snapshot: &FoldSnapshot) -> InlayOffset {
-        let mut cursor = snapshot
+        let (start, _, _) = snapshot
             .transforms
-            .cursor::<Dimensions<FoldOffset, InlayOffset>>(());
-        cursor.seek(&self, Bias::Right);
-        let overshoot = self.0 - cursor.start().0.0;
-        InlayOffset(cursor.start().1.0 + overshoot)
+            .find::<Dimensions<FoldOffset, InlayOffset>, _>((), &self, Bias::Right);
+        let overshoot = self.0 - start.0.0;
+        InlayOffset(start.1.0 + overshoot)
     }
 }
 

crates/editor/src/display_map/inlay_map.rs 🔗

@@ -1,17 +1,18 @@
-use crate::{ChunkRenderer, HighlightStyles, InlayId};
+use crate::{
+    ChunkRenderer, HighlightStyles,
+    inlays::{Inlay, InlayContent},
+};
 use collections::BTreeSet;
-use gpui::{Hsla, Rgba};
 use language::{Chunk, Edit, Point, TextSummary};
-use multi_buffer::{
-    Anchor, MultiBufferRow, MultiBufferRows, MultiBufferSnapshot, RowInfo, ToOffset,
-};
+use multi_buffer::{MultiBufferRow, MultiBufferRows, MultiBufferSnapshot, RowInfo, ToOffset};
+use project::InlayId;
 use std::{
     cmp,
     ops::{Add, AddAssign, Range, Sub, SubAssign},
-    sync::{Arc, OnceLock},
+    sync::Arc,
 };
 use sum_tree::{Bias, Cursor, Dimensions, SumTree};
-use text::{ChunkBitmaps, Patch, Rope};
+use text::{ChunkBitmaps, Patch};
 use ui::{ActiveTheme, IntoElement as _, ParentElement as _, Styled as _, div};
 
 use super::{Highlights, custom_highlights::CustomHighlightsChunks, fold_map::ChunkRendererId};
@@ -37,85 +38,6 @@ enum Transform {
     Inlay(Inlay),
 }
 
-#[derive(Debug, Clone)]
-pub struct Inlay {
-    pub id: InlayId,
-    pub position: Anchor,
-    pub content: InlayContent,
-}
-
-#[derive(Debug, Clone)]
-pub enum InlayContent {
-    Text(text::Rope),
-    Color(Hsla),
-}
-
-impl Inlay {
-    pub fn hint(id: u32, position: Anchor, hint: &project::InlayHint) -> Self {
-        let mut text = hint.text();
-        if hint.padding_right && text.reversed_chars_at(text.len()).next() != Some(' ') {
-            text.push(" ");
-        }
-        if hint.padding_left && text.chars_at(0).next() != Some(' ') {
-            text.push_front(" ");
-        }
-        Self {
-            id: InlayId::Hint(id),
-            position,
-            content: InlayContent::Text(text),
-        }
-    }
-
-    #[cfg(any(test, feature = "test-support"))]
-    pub fn mock_hint(id: u32, position: Anchor, text: impl Into<Rope>) -> Self {
-        Self {
-            id: InlayId::Hint(id),
-            position,
-            content: InlayContent::Text(text.into()),
-        }
-    }
-
-    pub fn color(id: u32, position: Anchor, color: Rgba) -> Self {
-        Self {
-            id: InlayId::Color(id),
-            position,
-            content: InlayContent::Color(color.into()),
-        }
-    }
-
-    pub fn edit_prediction<T: Into<Rope>>(id: u32, position: Anchor, text: T) -> Self {
-        Self {
-            id: InlayId::EditPrediction(id),
-            position,
-            content: InlayContent::Text(text.into()),
-        }
-    }
-
-    pub fn debugger<T: Into<Rope>>(id: u32, position: Anchor, text: T) -> Self {
-        Self {
-            id: InlayId::DebuggerValue(id),
-            position,
-            content: InlayContent::Text(text.into()),
-        }
-    }
-
-    pub fn text(&self) -> &Rope {
-        static COLOR_TEXT: OnceLock<Rope> = OnceLock::new();
-        match &self.content {
-            InlayContent::Text(text) => text,
-            InlayContent::Color(_) => COLOR_TEXT.get_or_init(|| Rope::from("◼")),
-        }
-    }
-
-    #[cfg(any(test, feature = "test-support"))]
-    pub fn get_color(&self) -> Option<Hsla> {
-        match self.content {
-            InlayContent::Color(color) => Some(color),
-            _ => None,
-        }
-    }
-}
-
 impl sum_tree::Item for Transform {
     type Summary = TransformSummary;
 
@@ -325,21 +247,16 @@ impl<'a> Iterator for InlayChunks<'a> {
                 };
 
                 let (prefix, suffix) = chunk.text.split_at(split_index);
+                self.output_offset.0 += prefix.len();
 
-                let (chars, tabs) = if split_index == 128 {
-                    let output = (chunk.chars, chunk.tabs);
-                    chunk.chars = 0;
-                    chunk.tabs = 0;
-                    output
-                } else {
-                    let mask = (1 << split_index) - 1;
-                    let output = (chunk.chars & mask, chunk.tabs & mask);
-                    chunk.chars = chunk.chars >> split_index;
-                    chunk.tabs = chunk.tabs >> split_index;
-                    output
-                };
+                let mask = 1u128.unbounded_shl(split_index as u32).wrapping_sub(1);
+                let chars = chunk.chars & mask;
+                let tabs = chunk.tabs & mask;
+
+                chunk.chars = chunk.chars.unbounded_shr(split_index as u32);
+                chunk.tabs = chunk.tabs.unbounded_shr(split_index as u32);
                 chunk.text = suffix;
-                self.output_offset.0 += prefix.len();
+
                 InlayChunk {
                     chunk: Chunk {
                         text: prefix,
@@ -457,18 +374,12 @@ impl<'a> Iterator for InlayChunks<'a> {
                 let (chunk, remainder) = inlay_chunk.split_at(split_index);
                 *inlay_chunk = remainder;
 
-                let (chars, tabs) = if split_index == 128 {
-                    let output = (*chars, *tabs);
-                    *chars = 0;
-                    *tabs = 0;
-                    output
-                } else {
-                    let mask = (1 << split_index as u32) - 1;
-                    let output = (*chars & mask, *tabs & mask);
-                    *chars = *chars >> split_index;
-                    *tabs = *tabs >> split_index;
-                    output
-                };
+                let mask = 1u128.unbounded_shl(split_index as u32).wrapping_sub(1);
+                let new_chars = *chars & mask;
+                let new_tabs = *tabs & mask;
+
+                *chars = chars.unbounded_shr(split_index as u32);
+                *tabs = tabs.unbounded_shr(split_index as u32);
 
                 if inlay_chunk.is_empty() {
                     self.inlay_chunk = None;
@@ -479,8 +390,8 @@ impl<'a> Iterator for InlayChunks<'a> {
                 InlayChunk {
                     chunk: Chunk {
                         text: chunk,
-                        chars,
-                        tabs,
+                        chars: new_chars,
+                        tabs: new_tabs,
                         highlight_style,
                         is_inlay: true,
                         ..Chunk::default()
@@ -761,7 +672,7 @@ impl InlayMap {
     #[cfg(test)]
     pub(crate) fn randomly_mutate(
         &mut self,
-        next_inlay_id: &mut u32,
+        next_inlay_id: &mut usize,
         rng: &mut rand::rngs::StdRng,
     ) -> (InlaySnapshot, Vec<InlayEdit>) {
         use rand::prelude::*;
@@ -825,22 +736,21 @@ impl InlayMap {
 
 impl InlaySnapshot {
     pub fn to_point(&self, offset: InlayOffset) -> InlayPoint {
-        let mut cursor = self
+        let (start, _, item) = self
             .transforms
-            .cursor::<Dimensions<InlayOffset, InlayPoint, usize>>(());
-        cursor.seek(&offset, Bias::Right);
-        let overshoot = offset.0 - cursor.start().0.0;
-        match cursor.item() {
+            .find::<Dimensions<InlayOffset, InlayPoint, usize>, _>((), &offset, Bias::Right);
+        let overshoot = offset.0 - start.0.0;
+        match item {
             Some(Transform::Isomorphic(_)) => {
-                let buffer_offset_start = cursor.start().2;
+                let buffer_offset_start = start.2;
                 let buffer_offset_end = buffer_offset_start + overshoot;
                 let buffer_start = self.buffer.offset_to_point(buffer_offset_start);
                 let buffer_end = self.buffer.offset_to_point(buffer_offset_end);
-                InlayPoint(cursor.start().1.0 + (buffer_end - buffer_start))
+                InlayPoint(start.1.0 + (buffer_end - buffer_start))
             }
             Some(Transform::Inlay(inlay)) => {
                 let overshoot = inlay.text().offset_to_point(overshoot);
-                InlayPoint(cursor.start().1.0 + overshoot)
+                InlayPoint(start.1.0 + overshoot)
             }
             None => self.max_point(),
         }
@@ -855,47 +765,48 @@ impl InlaySnapshot {
     }
 
     pub fn to_offset(&self, point: InlayPoint) -> InlayOffset {
-        let mut cursor = self
+        let (start, _, item) = self
             .transforms
-            .cursor::<Dimensions<InlayPoint, InlayOffset, Point>>(());
-        cursor.seek(&point, Bias::Right);
-        let overshoot = point.0 - cursor.start().0.0;
-        match cursor.item() {
+            .find::<Dimensions<InlayPoint, InlayOffset, Point>, _>((), &point, Bias::Right);
+        let overshoot = point.0 - start.0.0;
+        match item {
             Some(Transform::Isomorphic(_)) => {
-                let buffer_point_start = cursor.start().2;
+                let buffer_point_start = start.2;
                 let buffer_point_end = buffer_point_start + overshoot;
                 let buffer_offset_start = self.buffer.point_to_offset(buffer_point_start);
                 let buffer_offset_end = self.buffer.point_to_offset(buffer_point_end);
-                InlayOffset(cursor.start().1.0 + (buffer_offset_end - buffer_offset_start))
+                InlayOffset(start.1.0 + (buffer_offset_end - buffer_offset_start))
             }
             Some(Transform::Inlay(inlay)) => {
                 let overshoot = inlay.text().point_to_offset(overshoot);
-                InlayOffset(cursor.start().1.0 + overshoot)
+                InlayOffset(start.1.0 + overshoot)
             }
             None => self.len(),
         }
     }
     pub fn to_buffer_point(&self, point: InlayPoint) -> Point {
-        let mut cursor = self.transforms.cursor::<Dimensions<InlayPoint, Point>>(());
-        cursor.seek(&point, Bias::Right);
-        match cursor.item() {
+        let (start, _, item) =
+            self.transforms
+                .find::<Dimensions<InlayPoint, Point>, _>((), &point, Bias::Right);
+        match item {
             Some(Transform::Isomorphic(_)) => {
-                let overshoot = point.0 - cursor.start().0.0;
-                cursor.start().1 + overshoot
+                let overshoot = point.0 - start.0.0;
+                start.1 + overshoot
             }
-            Some(Transform::Inlay(_)) => cursor.start().1,
+            Some(Transform::Inlay(_)) => start.1,
             None => self.buffer.max_point(),
         }
     }
     pub fn to_buffer_offset(&self, offset: InlayOffset) -> usize {
-        let mut cursor = self.transforms.cursor::<Dimensions<InlayOffset, usize>>(());
-        cursor.seek(&offset, Bias::Right);
-        match cursor.item() {
+        let (start, _, item) =
+            self.transforms
+                .find::<Dimensions<InlayOffset, usize>, _>((), &offset, Bias::Right);
+        match item {
             Some(Transform::Isomorphic(_)) => {
-                let overshoot = offset - cursor.start().0;
-                cursor.start().1 + overshoot.0
+                let overshoot = offset - start.0;
+                start.1 + overshoot.0
             }
-            Some(Transform::Inlay(_)) => cursor.start().1,
+            Some(Transform::Inlay(_)) => start.1,
             None => self.buffer.len(),
         }
     }
@@ -1256,17 +1167,18 @@ const fn is_utf8_char_boundary(byte: u8) -> bool {
 mod tests {
     use super::*;
     use crate::{
-        InlayId, MultiBuffer,
+        MultiBuffer,
         display_map::{HighlightKey, InlayHighlights, TextHighlights},
         hover_links::InlayHighlight,
     };
     use gpui::{App, HighlightStyle};
+    use multi_buffer::Anchor;
     use project::{InlayHint, InlayHintLabel, ResolveState};
     use rand::prelude::*;
     use settings::SettingsStore;
     use std::{any::TypeId, cmp::Reverse, env, sync::Arc};
     use sum_tree::TreeMap;
-    use text::Patch;
+    use text::{Patch, Rope};
     use util::RandomCharIter;
     use util::post_inc;
 
@@ -1274,11 +1186,11 @@ mod tests {
     fn test_inlay_properties_label_padding() {
         assert_eq!(
             Inlay::hint(
-                0,
+                InlayId::Hint(0),
                 Anchor::min(),
                 &InlayHint {
                     label: InlayHintLabel::String("a".to_string()),
-                    position: text::Anchor::default(),
+                    position: text::Anchor::MIN,
                     padding_left: false,
                     padding_right: false,
                     tooltip: None,
@@ -1294,11 +1206,11 @@ mod tests {
 
         assert_eq!(
             Inlay::hint(
-                0,
+                InlayId::Hint(0),
                 Anchor::min(),
                 &InlayHint {
                     label: InlayHintLabel::String("a".to_string()),
-                    position: text::Anchor::default(),
+                    position: text::Anchor::MIN,
                     padding_left: true,
                     padding_right: true,
                     tooltip: None,
@@ -1314,11 +1226,11 @@ mod tests {
 
         assert_eq!(
             Inlay::hint(
-                0,
+                InlayId::Hint(0),
                 Anchor::min(),
                 &InlayHint {
                     label: InlayHintLabel::String(" a ".to_string()),
-                    position: text::Anchor::default(),
+                    position: text::Anchor::MIN,
                     padding_left: false,
                     padding_right: false,
                     tooltip: None,
@@ -1334,11 +1246,11 @@ mod tests {
 
         assert_eq!(
             Inlay::hint(
-                0,
+                InlayId::Hint(0),
                 Anchor::min(),
                 &InlayHint {
                     label: InlayHintLabel::String(" a ".to_string()),
-                    position: text::Anchor::default(),
+                    position: text::Anchor::MIN,
                     padding_left: true,
                     padding_right: true,
                     tooltip: None,
@@ -1357,11 +1269,11 @@ mod tests {
     fn test_inlay_hint_padding_with_multibyte_chars() {
         assert_eq!(
             Inlay::hint(
-                0,
+                InlayId::Hint(0),
                 Anchor::min(),
                 &InlayHint {
                     label: InlayHintLabel::String("🎨".to_string()),
-                    position: text::Anchor::default(),
+                    position: text::Anchor::MIN,
                     padding_left: true,
                     padding_right: true,
                     tooltip: None,

crates/editor/src/display_map/tab_map.rs 🔗

@@ -11,7 +11,7 @@ use sum_tree::Bias;
 const MAX_EXPANSION_COLUMN: u32 = 256;
 
 // Handles a tab width <= 128
-const SPACES: &[u8; u128::BITS as usize] = &[b' '; _];
+const SPACES: &[u8; rope::Chunk::MASK_BITS] = &[b' '; _];
 const MAX_TABS: NonZeroU32 = NonZeroU32::new(SPACES.len() as u32).unwrap();
 
 /// Keeps track of hard tabs in a text buffer.
@@ -569,56 +569,47 @@ impl<'a> Iterator for TabChunks<'a> {
         //todo(improve performance by using tab cursor)
         for (ix, c) in self.chunk.text.char_indices() {
             match c {
+                '\t' if ix > 0 => {
+                    let (prefix, suffix) = self.chunk.text.split_at(ix);
+
+                    let mask = 1u128.unbounded_shl(ix as u32).wrapping_sub(1);
+                    let chars = self.chunk.chars & mask;
+                    let tabs = self.chunk.tabs & mask;
+                    self.chunk.tabs = self.chunk.tabs.unbounded_shr(ix as u32);
+                    self.chunk.chars = self.chunk.chars.unbounded_shr(ix as u32);
+                    self.chunk.text = suffix;
+                    return Some(Chunk {
+                        text: prefix,
+                        chars,
+                        tabs,
+                        ..self.chunk.clone()
+                    });
+                }
                 '\t' => {
-                    if ix > 0 {
-                        let (prefix, suffix) = self.chunk.text.split_at(ix);
-
-                        let (chars, tabs) = if ix == 128 {
-                            let output = (self.chunk.chars, self.chunk.tabs);
-                            self.chunk.chars = 0;
-                            self.chunk.tabs = 0;
-                            output
-                        } else {
-                            let mask = (1 << ix) - 1;
-                            let output = (self.chunk.chars & mask, self.chunk.tabs & mask);
-                            self.chunk.chars = self.chunk.chars >> ix;
-                            self.chunk.tabs = self.chunk.tabs >> ix;
-                            output
-                        };
-
-                        self.chunk.text = suffix;
-                        return Some(Chunk {
-                            text: prefix,
-                            chars,
-                            tabs,
-                            ..self.chunk.clone()
-                        });
+                    self.chunk.text = &self.chunk.text[1..];
+                    self.chunk.tabs >>= 1;
+                    self.chunk.chars >>= 1;
+                    let tab_size = if self.input_column < self.max_expansion_column {
+                        self.tab_size.get()
                     } else {
-                        self.chunk.text = &self.chunk.text[1..];
-                        self.chunk.tabs >>= 1;
-                        self.chunk.chars >>= 1;
-                        let tab_size = if self.input_column < self.max_expansion_column {
-                            self.tab_size.get()
-                        } else {
-                            1
-                        };
-                        let mut len = tab_size - self.column % tab_size;
-                        let next_output_position = cmp::min(
-                            self.output_position + Point::new(0, len),
-                            self.max_output_position,
-                        );
-                        len = next_output_position.column - self.output_position.column;
-                        self.column += len;
-                        self.input_column += 1;
-                        self.output_position = next_output_position;
-                        return Some(Chunk {
-                            text: unsafe { std::str::from_utf8_unchecked(&SPACES[..len as usize]) },
-                            is_tab: true,
-                            chars: 1u128.unbounded_shl(len) - 1,
-                            tabs: 0,
-                            ..self.chunk.clone()
-                        });
-                    }
+                        1
+                    };
+                    let mut len = tab_size - self.column % tab_size;
+                    let next_output_position = cmp::min(
+                        self.output_position + Point::new(0, len),
+                        self.max_output_position,
+                    );
+                    len = next_output_position.column - self.output_position.column;
+                    self.column += len;
+                    self.input_column += 1;
+                    self.output_position = next_output_position;
+                    return Some(Chunk {
+                        text: unsafe { std::str::from_utf8_unchecked(&SPACES[..len as usize]) },
+                        is_tab: true,
+                        chars: 1u128.unbounded_shl(len) - 1,
+                        tabs: 0,
+                        ..self.chunk.clone()
+                    });
                 }
                 '\n' => {
                     self.column = 0;

crates/editor/src/display_map/wrap_map.rs 🔗

@@ -568,14 +568,17 @@ impl WrapSnapshot {
             let mut old_start = old_cursor.start().output.lines;
             old_start += tab_edit.old.start.0 - old_cursor.start().input.lines;
 
+            // todo(lw): Should these be seek_forward?
             old_cursor.seek(&tab_edit.old.end, Bias::Right);
             let mut old_end = old_cursor.start().output.lines;
             old_end += tab_edit.old.end.0 - old_cursor.start().input.lines;
 
+            // todo(lw): Should these be seek_forward?
             new_cursor.seek(&tab_edit.new.start, Bias::Right);
             let mut new_start = new_cursor.start().output.lines;
             new_start += tab_edit.new.start.0 - new_cursor.start().input.lines;
 
+            // todo(lw): Should these be seek_forward?
             new_cursor.seek(&tab_edit.new.end, Bias::Right);
             let mut new_end = new_cursor.start().output.lines;
             new_end += tab_edit.new.end.0 - new_cursor.start().input.lines;
@@ -628,24 +631,22 @@ impl WrapSnapshot {
     }
 
     pub fn line_len(&self, row: u32) -> u32 {
-        let mut cursor = self
-            .transforms
-            .cursor::<Dimensions<WrapPoint, TabPoint>>(());
-        cursor.seek(&WrapPoint::new(row + 1, 0), Bias::Left);
-        if cursor
-            .item()
-            .is_some_and(|transform| transform.is_isomorphic())
-        {
-            let overshoot = row - cursor.start().0.row();
-            let tab_row = cursor.start().1.row() + overshoot;
+        let (start, _, item) = self.transforms.find::<Dimensions<WrapPoint, TabPoint>, _>(
+            (),
+            &WrapPoint::new(row + 1, 0),
+            Bias::Left,
+        );
+        if item.is_some_and(|transform| transform.is_isomorphic()) {
+            let overshoot = row - start.0.row();
+            let tab_row = start.1.row() + overshoot;
             let tab_line_len = self.tab_snapshot.line_len(tab_row);
             if overshoot == 0 {
-                cursor.start().0.column() + (tab_line_len - cursor.start().1.column())
+                start.0.column() + (tab_line_len - start.1.column())
             } else {
                 tab_line_len
             }
         } else {
-            cursor.start().0.column()
+            start.0.column()
         }
     }
 
@@ -711,9 +712,10 @@ impl WrapSnapshot {
     }
 
     pub fn soft_wrap_indent(&self, row: u32) -> Option<u32> {
-        let mut cursor = self.transforms.cursor::<WrapPoint>(());
-        cursor.seek(&WrapPoint::new(row + 1, 0), Bias::Right);
-        cursor.item().and_then(|transform| {
+        let (.., item) =
+            self.transforms
+                .find::<WrapPoint, _>((), &WrapPoint::new(row + 1, 0), Bias::Right);
+        item.and_then(|transform| {
             if transform.is_isomorphic() {
                 None
             } else {
@@ -749,13 +751,12 @@ impl WrapSnapshot {
     }
 
     pub fn to_tab_point(&self, point: WrapPoint) -> TabPoint {
-        let mut cursor = self
-            .transforms
-            .cursor::<Dimensions<WrapPoint, TabPoint>>(());
-        cursor.seek(&point, Bias::Right);
-        let mut tab_point = cursor.start().1.0;
-        if cursor.item().is_some_and(|t| t.is_isomorphic()) {
-            tab_point += point.0 - cursor.start().0.0;
+        let (start, _, item) =
+            self.transforms
+                .find::<Dimensions<WrapPoint, TabPoint>, _>((), &point, Bias::Right);
+        let mut tab_point = start.1.0;
+        if item.is_some_and(|t| t.is_isomorphic()) {
+            tab_point += point.0 - start.0.0;
         }
         TabPoint(tab_point)
     }
@@ -769,19 +770,19 @@ impl WrapSnapshot {
     }
 
     pub fn tab_point_to_wrap_point(&self, point: TabPoint) -> WrapPoint {
-        let mut cursor = self
-            .transforms
-            .cursor::<Dimensions<TabPoint, WrapPoint>>(());
-        cursor.seek(&point, Bias::Right);
-        WrapPoint(cursor.start().1.0 + (point.0 - cursor.start().0.0))
+        let (start, ..) =
+            self.transforms
+                .find::<Dimensions<TabPoint, WrapPoint>, _>((), &point, Bias::Right);
+        WrapPoint(start.1.0 + (point.0 - start.0.0))
     }
 
     pub fn clip_point(&self, mut point: WrapPoint, bias: Bias) -> WrapPoint {
         if bias == Bias::Left {
-            let mut cursor = self.transforms.cursor::<WrapPoint>(());
-            cursor.seek(&point, Bias::Right);
-            if cursor.item().is_some_and(|t| !t.is_isomorphic()) {
-                point = *cursor.start();
+            let (start, _, item) = self
+                .transforms
+                .find::<WrapPoint, _>((), &point, Bias::Right);
+            if item.is_some_and(|t| !t.is_isomorphic()) {
+                point = start;
                 *point.column_mut() -= 1;
             }
         }
@@ -971,18 +972,11 @@ impl<'a> Iterator for WrapChunks<'a> {
 
         let (prefix, suffix) = self.input_chunk.text.split_at(input_len);
 
-        let (chars, tabs) = if input_len == 128 {
-            let output = (self.input_chunk.chars, self.input_chunk.tabs);
-            self.input_chunk.chars = 0;
-            self.input_chunk.tabs = 0;
-            output
-        } else {
-            let mask = (1 << input_len) - 1;
-            let output = (self.input_chunk.chars & mask, self.input_chunk.tabs & mask);
-            self.input_chunk.chars = self.input_chunk.chars >> input_len;
-            self.input_chunk.tabs = self.input_chunk.tabs >> input_len;
-            output
-        };
+        let mask = 1u128.unbounded_shl(input_len as u32).wrapping_sub(1);
+        let chars = self.input_chunk.chars & mask;
+        let tabs = self.input_chunk.tabs & mask;
+        self.input_chunk.tabs = self.input_chunk.tabs.unbounded_shr(input_len as u32);
+        self.input_chunk.chars = self.input_chunk.chars.unbounded_shr(input_len as u32);
 
         self.input_chunk.text = suffix;
         Some(Chunk {

crates/editor/src/editor.rs 🔗

@@ -7,7 +7,6 @@
 //! * [`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).
 //!
@@ -24,7 +23,7 @@ mod highlight_matching_bracket;
 mod hover_links;
 pub mod hover_popover;
 mod indent_guides;
-mod inlay_hint_cache;
+mod inlays;
 pub mod items;
 mod jsx_tag_auto_close;
 mod linked_editing_ranges;
@@ -61,6 +60,7 @@ pub use element::{
 };
 pub use git::blame::BlameRenderer;
 pub use hover_popover::hover_markdown_style;
+pub use inlays::Inlay;
 pub use items::MAX_TAB_TITLE_LEN;
 pub use lsp::CompletionContext;
 pub use lsp_ext::lsp_tasks;
@@ -82,7 +82,7 @@ use anyhow::{Context as _, Result, anyhow};
 use blink_manager::BlinkManager;
 use buffer_diff::DiffHunkStatus;
 use client::{Collaborator, ParticipantIndex, parse_zed_link};
-use clock::{AGENT_REPLICA_ID, ReplicaId};
+use clock::ReplicaId;
 use code_context_menus::{
     AvailableCodeAction, CodeActionContents, CodeActionsItem, CodeActionsMenu, CodeContextMenu,
     CompletionsMenu, ContextMenuOrigin,
@@ -112,10 +112,10 @@ use gpui::{
     div, point, prelude::*, pulsating_between, px, relative, size,
 };
 use highlight_matching_bracket::refresh_matching_bracket_highlights;
-use hover_links::{HoverLink, HoveredLinkState, InlayHighlight, find_file};
+use hover_links::{HoverLink, HoveredLinkState, find_file};
 use hover_popover::{HoverState, hide_hover};
 use indent_guides::ActiveIndentGuidesState;
-use inlay_hint_cache::{InlayHintCache, InlaySplice, InvalidationStrategy};
+use inlays::{InlaySplice, inlay_hints::InlayHintRefreshReason};
 use itertools::{Either, Itertools};
 use language::{
     AutoindentMode, BlockCommentConfig, BracketMatch, BracketPair, Buffer, BufferRow,
@@ -124,8 +124,8 @@ use language::{
     IndentSize, Language, OffsetRangeExt, Point, Runnable, RunnableRange, Selection, SelectionGoal,
     TextObject, TransactionId, TreeSitterOptions, WordsQuery,
     language_settings::{
-        self, InlayHintSettings, LspInsertMode, RewrapBehavior, WordsCompletionMode,
-        all_language_settings, language_settings,
+        self, LspInsertMode, RewrapBehavior, WordsCompletionMode, all_language_settings,
+        language_settings,
     },
     point_from_lsp, point_to_lsp, text_diff_with_options,
 };
@@ -140,15 +140,14 @@ use mouse_context_menu::MouseContextMenu;
 use movement::TextLayoutDetails;
 use multi_buffer::{
     ExcerptInfo, ExpandExcerptDirection, MultiBufferDiffHunk, MultiBufferPoint, MultiBufferRow,
-    ToOffsetUtf16,
 };
 use parking_lot::Mutex;
 use persistence::DB;
 use project::{
     BreakpointWithPosition, CodeAction, Completion, CompletionDisplayOptions, CompletionIntent,
-    CompletionResponse, CompletionSource, DisableAiSettings, DocumentHighlight, InlayHint,
-    Location, LocationLink, PrepareRenameResponse, Project, ProjectItem, ProjectPath,
-    ProjectTransaction, TaskSourceKind,
+    CompletionResponse, CompletionSource, DisableAiSettings, DocumentHighlight, InlayHint, InlayId,
+    InvalidationStrategy, Location, LocationLink, PrepareRenameResponse, Project, ProjectItem,
+    ProjectPath, ProjectTransaction, TaskSourceKind,
     debugger::{
         breakpoint_store::{
             Breakpoint, BreakpointEditAction, BreakpointSessionState, BreakpointState,
@@ -156,8 +155,11 @@ use project::{
         },
         session::{Session, SessionEvent},
     },
-    git_store::{GitStoreEvent, RepositoryEvent},
-    lsp_store::{CompletionDocumentation, FormatTrigger, LspFormatTarget, OpenLspBufferHandle},
+    git_store::GitStoreEvent,
+    lsp_store::{
+        CacheInlayHints, CompletionDocumentation, FormatTrigger, LspFormatTarget,
+        OpenLspBufferHandle,
+    },
     project_settings::{DiagnosticSeverity, GoToDiagnosticSeverityFilter, ProjectSettings},
 };
 use rand::seq::SliceRandom;
@@ -178,7 +180,7 @@ use std::{
     iter::{self, Peekable},
     mem,
     num::NonZeroU32,
-    ops::{ControlFlow, Deref, DerefMut, Not, Range, RangeInclusive},
+    ops::{Deref, DerefMut, Not, Range, RangeInclusive},
     path::{Path, PathBuf},
     rc::Rc,
     sync::Arc,
@@ -208,6 +210,10 @@ use crate::{
     code_context_menus::CompletionsMenuSource,
     editor_settings::MultiCursorModifier,
     hover_links::{find_url, find_url_from_range},
+    inlays::{
+        InlineValueCache,
+        inlay_hints::{LspInlayHintData, inlay_hint_settings},
+    },
     scroll::{ScrollOffset, ScrollPixelOffset},
     signature_help::{SignatureHelpHiddenBy, SignatureHelpState},
 };
@@ -261,42 +267,6 @@ impl ReportEditorEvent {
     }
 }
 
-struct InlineValueCache {
-    enabled: bool,
-    inlays: Vec<InlayId>,
-    refresh_task: Task<Option<()>>,
-}
-
-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 {
-    EditPrediction(u32),
-    DebuggerValue(u32),
-    // LSP
-    Hint(u32),
-    Color(u32),
-}
-
-impl InlayId {
-    fn id(&self) -> u32 {
-        match self {
-            Self::EditPrediction(id) => *id,
-            Self::DebuggerValue(id) => *id,
-            Self::Hint(id) => *id,
-            Self::Color(id) => *id,
-        }
-    }
-}
-
 pub enum ActiveDebugLine {}
 pub enum DebugStackFrameLine {}
 enum DocumentHighlightRead {}
@@ -358,6 +328,7 @@ pub fn init(cx: &mut App) {
     cx.observe_new(
         |workspace: &mut Workspace, _: Option<&mut Window>, _cx: &mut Context<Workspace>| {
             workspace.register_action(Editor::new_file);
+            workspace.register_action(Editor::new_file_split);
             workspace.register_action(Editor::new_file_vertical);
             workspace.register_action(Editor::new_file_horizontal);
             workspace.register_action(Editor::cancel_language_server_work);
@@ -1123,9 +1094,8 @@ pub struct Editor {
     edit_prediction_preview: EditPredictionPreview,
     edit_prediction_indent_conflict: bool,
     edit_prediction_requires_modifier_in_indent_conflict: bool,
-    inlay_hint_cache: InlayHintCache,
-    next_inlay_id: u32,
-    next_color_inlay_id: u32,
+    next_inlay_id: usize,
+    next_color_inlay_id: usize,
     _subscriptions: Vec<Subscription>,
     pixel_position_of_newest_cursor: Option<gpui::Point<Pixels>>,
     gutter_dimensions: GutterDimensions,
@@ -1192,10 +1162,19 @@ pub struct Editor {
     colors: Option<LspColorData>,
     post_scroll_update: Task<()>,
     refresh_colors_task: Task<()>,
+    inlay_hints: Option<LspInlayHintData>,
     folding_newlines: Task<()>,
     pub lookup_key: Option<Box<dyn Any + Send + Sync>>,
 }
 
+fn debounce_value(debounce_ms: u64) -> Option<Duration> {
+    if debounce_ms > 0 {
+        Some(Duration::from_millis(debounce_ms))
+    } else {
+        None
+    }
+}
+
 #[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
 enum NextScrollCursorCenterTopBottom {
     #[default]
@@ -1301,7 +1280,7 @@ enum SelectionHistoryMode {
 
 #[derive(Clone, PartialEq, Eq, Hash)]
 struct HoveredCursor {
-    replica_id: u16,
+    replica_id: ReplicaId,
     selection_id: usize,
 }
 
@@ -1620,31 +1599,6 @@ pub enum GotoDefinitionKind {
     Implementation,
 }
 
-#[derive(Debug, Clone)]
-enum InlayHintRefreshReason {
-    ModifiersChanged(bool),
-    Toggle(bool),
-    SettingsChange(InlayHintSettings),
-    NewLinesShown,
-    BufferEdited(HashSet<Arc<Language>>),
-    RefreshRequested,
-    ExcerptsRemoved(Vec<ExcerptId>),
-}
-
-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(HashSet<Entity<Buffer>>),
     Ranges(Vec<Range<MultiBufferPoint>>),
@@ -1880,11 +1834,20 @@ impl Editor {
                     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::RefreshInlayHints(server_id) => {
+                        editor.refresh_inlay_hints(
+                            InlayHintRefreshReason::RefreshRequested(*server_id),
+                            cx,
+                        );
+                    }
+                    project::Event::LanguageServerRemoved(..) => {
+                        if editor.tasks_update_task.is_none() {
+                            editor.tasks_update_task = Some(editor.refresh_runnables(window, cx));
+                        }
+                        editor.registered_buffers.clear();
+                        editor.register_visible_buffers(cx);
                     }
-                    project::Event::LanguageServerAdded(..)
-                    | project::Event::LanguageServerRemoved(..) => {
+                    project::Event::LanguageServerAdded(..) => {
                         if editor.tasks_update_task.is_none() {
                             editor.tasks_update_task = Some(editor.refresh_runnables(window, cx));
                         }
@@ -1912,17 +1875,12 @@ impl Editor {
                     project::Event::LanguageServerBufferRegistered { buffer_id, .. } => {
                         let buffer_id = *buffer_id;
                         if editor.buffer().read(cx).buffer(buffer_id).is_some() {
-                            let registered = editor.register_buffer(buffer_id, cx);
-                            if registered {
-                                editor.update_lsp_data(Some(buffer_id), window, cx);
-                                editor.refresh_inlay_hints(
-                                    InlayHintRefreshReason::RefreshRequested,
-                                    cx,
-                                );
-                                refresh_linked_ranges(editor, window, cx);
-                                editor.refresh_code_actions(window, cx);
-                                editor.refresh_document_highlights(cx);
-                            }
+                            editor.register_buffer(buffer_id, cx);
+                            editor.update_lsp_data(Some(buffer_id), window, cx);
+                            editor.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
+                            refresh_linked_ranges(editor, window, cx);
+                            editor.refresh_code_actions(window, cx);
+                            editor.refresh_document_highlights(cx);
                         }
                     }
 
@@ -2019,14 +1977,7 @@ impl Editor {
             let git_store = project.read(cx).git_store().clone();
             let project = project.clone();
             project_subscriptions.push(cx.subscribe(&git_store, move |this, _, event, cx| {
-                if let GitStoreEvent::RepositoryUpdated(
-                    _,
-                    RepositoryEvent::Updated {
-                        new_instance: true, ..
-                    },
-                    _,
-                ) = event
-                {
+                if let GitStoreEvent::RepositoryAdded = event {
                     this.load_diff_task = Some(
                         update_uncommitted_diff_for_buffer(
                             cx.entity(),
@@ -2193,7 +2144,6 @@ impl Editor {
             diagnostics_enabled: full_mode,
             word_completions_enabled: full_mode,
             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,
@@ -2259,6 +2209,7 @@ impl Editor {
             pull_diagnostics_task: Task::ready(()),
             colors: None,
             refresh_colors_task: Task::ready(()),
+            inlay_hints: None,
             next_color_inlay_id: 0,
             post_scroll_update: Task::ready(()),
             linked_edit_ranges: Default::default(),
@@ -2323,21 +2274,22 @@ impl Editor {
                 }
                 EditorEvent::Edited { .. } => {
                     if !vim_enabled(cx) {
-                        let (map, selections) = editor.selections.all_adjusted_display(cx);
+                        let display_map = editor.display_snapshot(cx);
+                        let selections = editor.selections.all_adjusted_display(&display_map);
                         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()
+                                        p.to_display_point(&display_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))
+                            .map(|s| display_map.display_point_to_anchor(s.head(), Bias::Left))
                             .collect();
                         editor
                             .change_list
@@ -2395,19 +2347,25 @@ impl Editor {
 
             editor.go_to_active_debug_line(window, cx);
 
-            if let Some(buffer) = multi_buffer.read(cx).as_singleton() {
-                editor.register_buffer(buffer.read(cx).remote_id(), cx);
-            }
-
             editor.minimap =
                 editor.create_minimap(EditorSettings::get_global(cx).minimap, window, cx);
             editor.colors = Some(LspColorData::new(cx));
+            editor.inlay_hints = Some(LspInlayHintData::new(inlay_hint_settings));
+
+            if let Some(buffer) = multi_buffer.read(cx).as_singleton() {
+                editor.register_buffer(buffer.read(cx).remote_id(), cx);
+            }
+            editor.update_lsp_data(None, window, cx);
             editor.report_editor_event(ReportEditorEvent::EditorOpened, None, cx);
         }
 
         editor
     }
 
+    pub fn display_snapshot(&self, cx: &mut App) -> DisplaySnapshot {
+        self.selections.display_map(cx)
+    }
+
     pub fn deploy_mouse_context_menu(
         &mut self,
         position: gpui::Point<Pixels>,
@@ -2443,7 +2401,7 @@ impl Editor {
         }
 
         self.selections
-            .disjoint_in_range::<usize>(range.clone(), cx)
+            .disjoint_in_range::<usize>(range.clone(), &self.display_snapshot(cx))
             .into_iter()
             .any(|selection| {
                 // This is needed to cover a corner case, if we just check for an existing
@@ -2454,15 +2412,15 @@ impl Editor {
             })
     }
 
-    pub fn key_context(&self, window: &Window, cx: &App) -> KeyContext {
+    pub fn key_context(&self, window: &mut Window, cx: &mut App) -> KeyContext {
         self.key_context_internal(self.has_active_edit_prediction(), window, cx)
     }
 
     fn key_context_internal(
         &self,
         has_active_edit_prediction: bool,
-        window: &Window,
-        cx: &App,
+        window: &mut Window,
+        cx: &mut App,
     ) -> KeyContext {
         let mut key_context = KeyContext::new_with_defaults();
         key_context.add("Editor");
@@ -2512,12 +2470,15 @@ impl Editor {
         }
 
         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())
-            {
-                key_context.set("extension", extension.to_string());
+            if let Some(extension) = singleton_buffer.read(cx).file().and_then(|file| {
+                Some(
+                    file.full_path(cx)
+                        .extension()?
+                        .to_string_lossy()
+                        .into_owned(),
+                )
+            }) {
+                key_context.set("extension", extension);
             }
         } else {
             key_context.add("multibuffer");
@@ -2536,6 +2497,17 @@ impl Editor {
             key_context.add("selection_mode");
         }
 
+        let disjoint = self.selections.disjoint_anchors();
+        let snapshot = self.snapshot(window, cx);
+        let snapshot = snapshot.buffer_snapshot();
+        if self.mode == EditorMode::SingleLine
+            && let [selection] = disjoint
+            && selection.start == selection.end
+            && selection.end.to_offset(snapshot) == snapshot.len()
+        {
+            key_context.add("end_of_input");
+        }
+
         key_context
     }
 
@@ -2589,8 +2561,8 @@ impl Editor {
     pub fn accept_edit_prediction_keybind(
         &self,
         accept_partial: bool,
-        window: &Window,
-        cx: &App,
+        window: &mut Window,
+        cx: &mut App,
     ) -> AcceptEditPredictionBinding {
         let key_context = self.key_context_internal(true, window, cx);
         let in_conflict = self.edit_prediction_in_conflict();
@@ -2669,6 +2641,15 @@ impl Editor {
         Self::new_file_in_direction(workspace, SplitDirection::horizontal(cx), window, cx)
     }
 
+    fn new_file_split(
+        workspace: &mut Workspace,
+        action: &workspace::NewFileSplit,
+        window: &mut Window,
+        cx: &mut Context<Workspace>,
+    ) {
+        Self::new_file_in_direction(workspace, action.0, window, cx)
+    }
+
     fn new_file_in_direction(
         workspace: &mut Workspace,
         direction: SplitDirection,
@@ -2723,7 +2704,7 @@ impl Editor {
         self.buffer().read(cx).title(cx)
     }
 
-    pub fn snapshot(&self, window: &mut Window, cx: &mut App) -> EditorSnapshot {
+    pub fn snapshot(&self, window: &Window, cx: &mut App) -> EditorSnapshot {
         let git_blame_gutter_max_author_length = self
             .render_git_blame_gutter(cx)
             .then(|| {
@@ -3039,7 +3020,7 @@ impl Editor {
         // Copy selections to primary selection buffer
         #[cfg(any(target_os = "linux", target_os = "freebsd"))]
         if local {
-            let selections = self.selections.all::<usize>(cx);
+            let selections = self.selections.all::<usize>(&self.display_snapshot(cx));
             let buffer_handle = self.buffer.read(cx).read(cx);
 
             let mut text = String::new();
@@ -3498,7 +3479,7 @@ impl Editor {
         cx: &mut Context<Self>,
     ) {
         let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
-        let tail = self.selections.newest::<usize>(cx).tail();
+        let tail = self.selections.newest::<usize>(&display_map).tail();
         let click_count = click_count.max(match self.selections.select_mode() {
             SelectMode::Character => 1,
             SelectMode::Word(_) => 2,
@@ -3617,7 +3598,7 @@ impl Editor {
 
         let point_to_delete: Option<usize> = {
             let selected_points: Vec<Selection<Point>> =
-                self.selections.disjoint_in_range(start..end, cx);
+                self.selections.disjoint_in_range(start..end, &display_map);
 
             if !add || click_count > 1 {
                 None
@@ -3693,7 +3674,7 @@ impl Editor {
             );
         };
 
-        let tail = self.selections.newest::<Point>(cx).tail();
+        let tail = self.selections.newest::<Point>(&display_map).tail();
         let selection_anchor = display_map.buffer_snapshot().anchor_before(tail);
         self.columnar_selection_state = match mode {
             ColumnarMode::FromMouse => Some(ColumnarSelectionState::FromMouse {
@@ -3820,7 +3801,7 @@ impl Editor {
     fn end_selection(&mut self, window: &mut Window, cx: &mut Context<Self>) {
         self.columnar_selection_state.take();
         if let Some(pending_mode) = self.selections.pending_mode() {
-            let selections = self.selections.all::<usize>(cx);
+            let selections = self.selections.all::<usize>(&self.display_snapshot(cx));
             self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
                 s.select(selections);
                 s.clear_pending();
@@ -3909,9 +3890,9 @@ impl Editor {
         cx.notify();
     }
 
-    pub fn has_non_empty_selection(&self, cx: &mut App) -> bool {
+    pub fn has_non_empty_selection(&self, snapshot: &DisplaySnapshot) -> bool {
         self.selections
-            .all_adjusted(cx)
+            .all_adjusted(snapshot)
             .iter()
             .any(|selection| !selection.is_empty())
     }
@@ -3962,6 +3943,10 @@ impl Editor {
             return true;
         }
 
+        if self.hide_blame_popover(true, cx) {
+            return true;
+        }
+
         if hide_hover(self, cx) {
             return true;
         }
@@ -4060,7 +4045,7 @@ impl Editor {
 
         self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx);
 
-        let selections = self.selections.all_adjusted(cx);
+        let selections = self.selections.all_adjusted(&self.display_snapshot(cx));
         let mut bracket_inserted = false;
         let mut edits = Vec::new();
         let mut linked_edits = HashMap::<_, Vec<_>>::default();
@@ -4410,7 +4395,7 @@ impl Editor {
             let trigger_in_words =
                 this.show_edit_predictions_in_menu() || !had_active_edit_prediction;
             if this.hard_wrap.is_some() {
-                let latest: Range<Point> = this.selections.newest(cx).range();
+                let latest: Range<Point> = this.selections.newest(&map).range();
                 if latest.is_empty()
                     && this
                         .buffer()
@@ -4486,7 +4471,7 @@ impl Editor {
         self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx);
         self.transact(window, cx, |this, window, cx| {
             let (edits_with_flags, selection_info): (Vec<_>, Vec<_>) = {
-                let selections = this.selections.all::<usize>(cx);
+                let selections = this.selections.all::<usize>(&this.display_snapshot(cx));
                 let multi_buffer = this.buffer.read(cx);
                 let buffer = multi_buffer.snapshot(cx);
                 selections
@@ -4778,7 +4763,12 @@ impl Editor {
         let mut edits = Vec::new();
         let mut rows = Vec::new();
 
-        for (rows_inserted, selection) in self.selections.all_adjusted(cx).into_iter().enumerate() {
+        for (rows_inserted, selection) in self
+            .selections
+            .all_adjusted(&self.display_snapshot(cx))
+            .into_iter()
+            .enumerate()
+        {
             let cursor = selection.head();
             let row = cursor.row;
 
@@ -4838,7 +4828,7 @@ impl Editor {
         let mut rows = Vec::new();
         let mut rows_inserted = 0;
 
-        for selection in self.selections.all_adjusted(cx) {
+        for selection in self.selections.all_adjusted(&self.display_snapshot(cx)) {
             let cursor = selection.head();
             let row = cursor.row;
 
@@ -4910,7 +4900,7 @@ impl Editor {
 
         let text: Arc<str> = text.into();
         self.transact(window, cx, |this, window, cx| {
-            let old_selections = this.selections.all_adjusted(cx);
+            let old_selections = this.selections.all_adjusted(&this.display_snapshot(cx));
             let selection_anchors = this.buffer.update(cx, |buffer, cx| {
                 let anchors = {
                     let snapshot = buffer.read(cx);
@@ -5022,7 +5012,7 @@ impl Editor {
     /// 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<Self>) {
-        let selections = self.selections.all::<usize>(cx);
+        let selections = self.selections.all::<usize>(&self.display_snapshot(cx));
         let buffer = self.buffer.read(cx).read(cx);
         let new_selections = self
             .selections_with_autoclose_regions(selections, &buffer)
@@ -5163,179 +5153,8 @@ impl Editor {
         }
     }
 
-    pub fn toggle_inline_values(
-        &mut self,
-        _: &ToggleInlineValues,
-        _: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        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>,
-    ) {
-        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
-    }
-
-    #[cfg(any(test, feature = "test-support"))]
-    pub fn inline_value_inlays(&self, cx: &App) -> Vec<Inlay> {
-        self.display_map
-            .read(cx)
-            .current_inlays()
-            .filter(|inlay| matches!(inlay.id, InlayId::DebuggerValue(_)))
-            .cloned()
-            .collect()
-    }
-
-    #[cfg(any(test, feature = "test-support"))]
-    pub fn all_inlays(&self, cx: &App) -> Vec<Inlay> {
-        self.display_map
-            .read(cx)
-            .current_inlays()
-            .cloned()
-            .collect()
-    }
-
-    fn refresh_inlay_hints(&mut self, reason: InlayHintRefreshReason, cx: &mut Context<Self>) {
-        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.clear_inlay_hints(cx);
-                            return;
-                        }
-                    }
-                    None => return,
-                }
-            }
-            InlayHintRefreshReason::Toggle(enabled) => {
-                if self.inlay_hint_cache.toggle(enabled) {
-                    if enabled {
-                        (InvalidationStrategy::RefreshRequested, None)
-                    } else {
-                        self.clear_inlay_hints(cx);
-                        return;
-                    }
-                } else {
-                    return;
-                }
-            }
-            InlayHintRefreshReason::SettingsChange(new_settings) => {
-                match self.inlay_hint_cache.update_settings(
-                    &self.buffer,
-                    new_settings,
-                    self.visible_inlay_hints(cx).cloned().collect::<Vec<_>>(),
-                    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)
-            }
-        };
-
-        let mut visible_excerpts = self.visible_excerpts(required_languages.as_ref(), cx);
-        visible_excerpts.retain(|_, (buffer, _, _)| {
-            self.registered_buffers
-                .contains_key(&buffer.read(cx).remote_id())
-        });
-
-        if let Some(InlaySplice {
-            to_remove,
-            to_insert,
-        }) = self.inlay_hint_cache.spawn_hint_refresh(
-            reason_description,
-            visible_excerpts,
-            invalidate_cache,
-            ignore_debounce,
-            cx,
-        ) {
-            self.splice_inlays(&to_remove, to_insert, cx);
-        }
-    }
-
-    pub fn clear_inlay_hints(&self, cx: &mut Context<Editor>) {
-        self.splice_inlays(
-            &self
-                .visible_inlay_hints(cx)
-                .map(|inlay| inlay.id)
-                .collect::<Vec<_>>(),
-            Vec::new(),
-            cx,
-        );
-    }
-
-    fn visible_inlay_hints<'a>(
-        &'a self,
-        cx: &'a Context<Editor>,
-    ) -> impl Iterator<Item = &'a Inlay> {
-        self.display_map
-            .read(cx)
-            .current_inlays()
-            .filter(move |inlay| matches!(inlay.id, InlayId::Hint(_)))
-    }
-
     pub fn visible_excerpts(
         &self,
-        restrict_to_languages: Option<&HashSet<Arc<Language>>>,
         cx: &mut Context<Editor>,
     ) -> HashMap<ExcerptId, (Entity<Buffer>, clock::Global, Range<usize>)> {
         let Some(project) = self.project() else {
@@ -5354,9 +5173,8 @@ impl Editor {
                 + 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)
+            .range_to_buffer_ranges(multi_buffer_visible_start..multi_buffer_visible_end)
             .into_iter()
             .filter(|(_, excerpt_visible_range, _)| !excerpt_visible_range.is_empty())
             .filter_map(|(buffer, excerpt_visible_range, excerpt_id)| {
@@ -5366,23 +5184,17 @@ impl Editor {
                     .read(cx)
                     .entry_for_id(buffer_file.project_entry_id()?)?;
                 if worktree_entry.is_ignored {
-                    return None;
-                }
-
-                let language = buffer.language()?;
-                if let Some(restrict_to_languages) = restrict_to_languages
-                    && !restrict_to_languages.contains(language)
-                {
-                    return None;
+                    None
+                } else {
+                    Some((
+                        excerpt_id,
+                        (
+                            multi_buffer.buffer(buffer.remote_id()).unwrap(),
+                            buffer.version().clone(),
+                            excerpt_visible_range,
+                        ),
+                    ))
                 }
-                Some((
-                    excerpt_id,
-                    (
-                        multi_buffer.buffer(buffer.remote_id()).unwrap(),
-                        buffer.version().clone(),
-                        excerpt_visible_range,
-                    ),
-                ))
             })
             .collect()
     }
@@ -5398,18 +5210,6 @@ impl Editor {
         }
     }
 
-    pub fn splice_inlays(
-        &self,
-        to_remove: &[InlayId],
-        to_insert: Vec<Inlay>,
-        cx: &mut Context<Self>,
-    ) {
-        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,
@@ -6029,7 +5829,7 @@ impl Editor {
         let prefix = &old_text[..old_text.len().saturating_sub(lookahead)];
         let suffix = &old_text[lookbehind.min(old_text.len())..];
 
-        let selections = self.selections.all::<usize>(cx);
+        let selections = self.selections.all::<usize>(&self.display_snapshot(cx));
         let mut ranges = Vec::new();
         let mut linked_edits = HashMap::<_, Vec<_>>::default();
 
@@ -6188,7 +5988,10 @@ impl Editor {
             Some(CodeActionSource::Indicator(row)) | Some(CodeActionSource::RunMenu(row)) => {
                 DisplayPoint::new(*row, 0).to_point(&snapshot)
             }
-            _ => self.selections.newest::<Point>(cx).head(),
+            _ => self
+                .selections
+                .newest::<Point>(&snapshot.display_snapshot)
+                .head(),
         };
         let Some((buffer, buffer_row)) = snapshot
             .buffer_snapshot()
@@ -6604,7 +6407,7 @@ impl Editor {
             .when(show_tooltip, |this| {
                 this.tooltip({
                     let focus_handle = self.focus_handle.clone();
-                    move |window, cx| {
+                    move |_window, cx| {
                         Tooltip::for_action_in(
                             "Toggle Code Actions",
                             &ToggleCodeActions {
@@ -6612,7 +6415,6 @@ impl Editor {
                                 quick_launch: false,
                             },
                             &focus_handle,
-                            window,
                             cx,
                         )
                     }
@@ -6650,7 +6452,9 @@ impl Editor {
                     if newest_selection.head().diff_base_anchor.is_some() {
                         return None;
                     }
-                    let newest_selection_adjusted = this.selections.newest_adjusted(cx);
+                    let display_snapshot = this.display_snapshot(cx);
+                    let newest_selection_adjusted =
+                        this.selections.newest_adjusted(&display_snapshot);
                     let buffer = this.buffer.read(cx);
 
                     let (start_buffer, start) =
@@ -6725,7 +6529,10 @@ impl Editor {
 
     pub fn blame_hover(&mut self, _: &BlameHover, window: &mut Window, cx: &mut Context<Self>) {
         let snapshot = self.snapshot(window, cx);
-        let cursor = self.selections.newest::<Point>(cx).head();
+        let cursor = self
+            .selections
+            .newest::<Point>(&snapshot.display_snapshot)
+            .head();
         let Some((buffer, point, _)) = snapshot.buffer_snapshot().point_to_buffer_point(cursor)
         else {
             return;
@@ -6771,7 +6578,7 @@ impl Editor {
         if let Some(state) = &mut self.inline_blame_popover {
             state.hide_task.take();
         } else {
-            let blame_popover_delay = EditorSettings::get_global(cx).hover_popover_delay;
+            let blame_popover_delay = EditorSettings::get_global(cx).hover_popover_delay.0;
             let blame_entry = blame_entry.clone();
             let show_task = cx.spawn(async move |editor, cx| {
                 if !ignore_timeout {
@@ -6817,13 +6624,15 @@ impl Editor {
         }
     }
 
-    fn hide_blame_popover(&mut self, cx: &mut Context<Self>) {
+    fn hide_blame_popover(&mut self, ignore_timeout: bool, cx: &mut Context<Self>) -> bool {
         self.inline_blame_popover_show_task.take();
         if let Some(state) = &mut self.inline_blame_popover {
             let hide_task = cx.spawn(async move |editor, cx| {
-                cx.background_executor()
-                    .timer(std::time::Duration::from_millis(100))
-                    .await;
+                if !ignore_timeout {
+                    cx.background_executor()
+                        .timer(std::time::Duration::from_millis(100))
+                        .await;
+                }
                 editor
                     .update(cx, |editor, cx| {
                         editor.inline_blame_popover.take();
@@ -6832,6 +6641,9 @@ impl Editor {
                     .ok();
             });
             state.hide_task = Some(hide_task);
+            true
+        } else {
+            false
         }
     }
 
@@ -6862,7 +6674,7 @@ impl Editor {
             return None;
         }
 
-        let debounce = EditorSettings::get_global(cx).lsp_highlight_debounce;
+        let debounce = EditorSettings::get_global(cx).lsp_highlight_debounce.0;
         self.document_highlights_task = Some(cx.spawn(async move |this, cx| {
             cx.background_executor()
                 .timer(Duration::from_millis(debounce))
@@ -6913,7 +6725,8 @@ impl Editor {
                                 continue;
                             }
 
-                            let range = Anchor::range_in_buffer(excerpt_id, buffer_id, start..end);
+                            let range =
+                                Anchor::range_in_buffer(excerpt_id, buffer_id, *start..*end);
                             if highlight.kind == lsp::DocumentHighlightKind::WRITE {
                                 write_ranges.push(range);
                             } else {
@@ -7597,7 +7410,10 @@ impl Editor {
 
                 // Find an insertion that starts at the cursor position.
                 let snapshot = self.buffer.read(cx).snapshot(cx);
-                let cursor_offset = self.selections.newest::<usize>(cx).head();
+                let cursor_offset = self
+                    .selections
+                    .newest::<usize>(&self.display_snapshot(cx))
+                    .head();
                 let insertion = edits.iter().find_map(|(range, text)| {
                     let range = range.to_offset(&snapshot);
                     if range.is_empty() && range.start == cursor_offset {
@@ -8463,13 +8279,12 @@ impl Editor {
                     cx,
                 );
             }))
-            .tooltip(move |window, cx| {
+            .tooltip(move |_window, cx| {
                 Tooltip::with_meta_in(
                     primary_action_text,
                     Some(&ToggleBreakpoint),
                     meta.clone(),
                     &focus_handle,
-                    window,
                     cx,
                 )
             })

crates/editor/src/editor_settings.rs 🔗

@@ -1,16 +1,14 @@
 use core::num;
-use std::num::NonZeroU32;
 
 use gpui::App;
 use language::CursorShape;
 use project::project_settings::DiagnosticSeverity;
+use settings::Settings;
 pub use settings::{
-    CurrentLineHighlight, DisplayIn, DocumentColorsRenderMode, DoubleClickInMultibuffer,
+    CurrentLineHighlight, DelayMs, DisplayIn, DocumentColorsRenderMode, DoubleClickInMultibuffer,
     GoToDefinitionFallback, HideMouseMode, MinimapThumb, MinimapThumbBorder, MultiCursorModifier,
     ScrollBeyondLastLine, ScrollbarDiagnostics, SeedQuerySetting, ShowMinimap, SnippetSortOrder,
-    VsCodeSettings,
 };
-use settings::{Settings, SettingsContent};
 use ui::scrollbars::{ScrollbarVisibility, ShowScrollbar};
 
 /// Imports from the VSCode settings at
@@ -22,9 +20,9 @@ pub struct EditorSettings {
     pub current_line_highlight: CurrentLineHighlight,
     pub selection_highlight: bool,
     pub rounded_selection: bool,
-    pub lsp_highlight_debounce: u64,
+    pub lsp_highlight_debounce: DelayMs,
     pub hover_popover_enabled: bool,
-    pub hover_popover_delay: u64,
+    pub hover_popover_delay: DelayMs,
     pub toolbar: Toolbar,
     pub scrollbar: Scrollbar,
     pub minimap: Minimap,
@@ -149,7 +147,7 @@ pub struct DragAndDropSelection {
     /// The delay in milliseconds that must elapse before drag and drop is allowed. Otherwise, a new text selection is created.
     ///
     /// Default: 300
-    pub delay: u64,
+    pub delay: DelayMs,
 }
 
 /// Default options for buffer and project search items.
@@ -270,208 +268,4 @@ impl Settings for EditorSettings {
             minimum_contrast_for_highlights: editor.minimum_contrast_for_highlights.unwrap().0,
         }
     }
-
-    fn import_from_vscode(vscode: &VsCodeSettings, current: &mut SettingsContent) {
-        vscode.enum_setting(
-            "editor.cursorBlinking",
-            &mut current.editor.cursor_blink,
-            |s| match s {
-                "blink" | "phase" | "expand" | "smooth" => Some(true),
-                "solid" => Some(false),
-                _ => None,
-            },
-        );
-        vscode.enum_setting(
-            "editor.cursorStyle",
-            &mut current.editor.cursor_shape,
-            |s| match s {
-                "block" => Some(settings::CursorShape::Block),
-                "block-outline" => Some(settings::CursorShape::Hollow),
-                "line" | "line-thin" => Some(settings::CursorShape::Bar),
-                "underline" | "underline-thin" => Some(settings::CursorShape::Underline),
-                _ => None,
-            },
-        );
-
-        vscode.enum_setting(
-            "editor.renderLineHighlight",
-            &mut current.editor.current_line_highlight,
-            |s| match s {
-                "gutter" => Some(CurrentLineHighlight::Gutter),
-                "line" => Some(CurrentLineHighlight::Line),
-                "all" => Some(CurrentLineHighlight::All),
-                _ => None,
-            },
-        );
-
-        vscode.bool_setting(
-            "editor.selectionHighlight",
-            &mut current.editor.selection_highlight,
-        );
-        vscode.bool_setting(
-            "editor.roundedSelection",
-            &mut current.editor.rounded_selection,
-        );
-        vscode.bool_setting(
-            "editor.hover.enabled",
-            &mut current.editor.hover_popover_enabled,
-        );
-        vscode.u64_setting(
-            "editor.hover.delay",
-            &mut current.editor.hover_popover_delay,
-        );
-
-        let mut gutter = settings::GutterContent::default();
-        vscode.enum_setting(
-            "editor.showFoldingControls",
-            &mut gutter.folds,
-            |s| match s {
-                "always" | "mouseover" => Some(true),
-                "never" => Some(false),
-                _ => None,
-            },
-        );
-        vscode.enum_setting(
-            "editor.lineNumbers",
-            &mut gutter.line_numbers,
-            |s| match s {
-                "on" | "relative" => Some(true),
-                "off" => Some(false),
-                _ => None,
-            },
-        );
-        if let Some(old_gutter) = current.editor.gutter.as_mut() {
-            if gutter.folds.is_some() {
-                old_gutter.folds = gutter.folds
-            }
-            if gutter.line_numbers.is_some() {
-                old_gutter.line_numbers = gutter.line_numbers
-            }
-        } else if gutter != settings::GutterContent::default() {
-            current.editor.gutter = Some(gutter)
-        }
-        if let Some(b) = vscode.read_bool("editor.scrollBeyondLastLine") {
-            current.editor.scroll_beyond_last_line = Some(if b {
-                ScrollBeyondLastLine::OnePage
-            } else {
-                ScrollBeyondLastLine::Off
-            })
-        }
-
-        let mut scrollbar_axes = settings::ScrollbarAxesContent::default();
-        vscode.enum_setting(
-            "editor.scrollbar.horizontal",
-            &mut scrollbar_axes.horizontal,
-            |s| match s {
-                "auto" | "visible" => Some(true),
-                "hidden" => Some(false),
-                _ => None,
-            },
-        );
-        vscode.enum_setting(
-            "editor.scrollbar.vertical",
-            &mut scrollbar_axes.horizontal,
-            |s| match s {
-                "auto" | "visible" => Some(true),
-                "hidden" => Some(false),
-                _ => None,
-            },
-        );
-
-        if scrollbar_axes != settings::ScrollbarAxesContent::default() {
-            let scrollbar_settings = current.editor.scrollbar.get_or_insert_default();
-            let axes_settings = scrollbar_settings.axes.get_or_insert_default();
-
-            if let Some(vertical) = scrollbar_axes.vertical {
-                axes_settings.vertical = Some(vertical);
-            }
-            if let Some(horizontal) = scrollbar_axes.horizontal {
-                axes_settings.horizontal = Some(horizontal);
-            }
-        }
-
-        // TODO: check if this does the int->float conversion?
-        vscode.f32_setting(
-            "editor.cursorSurroundingLines",
-            &mut current.editor.vertical_scroll_margin,
-        );
-        vscode.f32_setting(
-            "editor.mouseWheelScrollSensitivity",
-            &mut current.editor.scroll_sensitivity,
-        );
-        vscode.f32_setting(
-            "editor.fastScrollSensitivity",
-            &mut current.editor.fast_scroll_sensitivity,
-        );
-        if Some("relative") == vscode.read_string("editor.lineNumbers") {
-            current.editor.relative_line_numbers = Some(true);
-        }
-
-        vscode.enum_setting(
-            "editor.find.seedSearchStringFromSelection",
-            &mut current.editor.seed_search_query_from_cursor,
-            |s| match s {
-                "always" => Some(SeedQuerySetting::Always),
-                "selection" => Some(SeedQuerySetting::Selection),
-                "never" => Some(SeedQuerySetting::Never),
-                _ => None,
-            },
-        );
-        vscode.bool_setting("search.smartCase", &mut current.editor.use_smartcase_search);
-        vscode.enum_setting(
-            "editor.multiCursorModifier",
-            &mut current.editor.multi_cursor_modifier,
-            |s| match s {
-                "ctrlCmd" => Some(MultiCursorModifier::CmdOrCtrl),
-                "alt" => Some(MultiCursorModifier::Alt),
-                _ => None,
-            },
-        );
-
-        vscode.bool_setting(
-            "editor.parameterHints.enabled",
-            &mut current.editor.auto_signature_help,
-        );
-        vscode.bool_setting(
-            "editor.parameterHints.enabled",
-            &mut current.editor.show_signature_help_after_edits,
-        );
-
-        if let Some(use_ignored) = vscode.read_bool("search.useIgnoreFiles") {
-            let search = current.editor.search.get_or_insert_default();
-            search.include_ignored = Some(use_ignored);
-        }
-
-        let mut minimap = settings::MinimapContent::default();
-        let minimap_enabled = vscode.read_bool("editor.minimap.enabled").unwrap_or(true);
-        let autohide = vscode.read_bool("editor.minimap.autohide");
-        let mut max_width_columns: Option<u32> = None;
-        vscode.u32_setting("editor.minimap.maxColumn", &mut max_width_columns);
-        if minimap_enabled {
-            if let Some(false) = autohide {
-                minimap.show = Some(ShowMinimap::Always);
-            } else {
-                minimap.show = Some(ShowMinimap::Auto);
-            }
-        } else {
-            minimap.show = Some(ShowMinimap::Never);
-        }
-        if let Some(max_width_columns) = max_width_columns {
-            minimap.max_width_columns = NonZeroU32::new(max_width_columns);
-        }
-
-        vscode.enum_setting(
-            "editor.minimap.showSlider",
-            &mut minimap.thumb,
-            |s| match s {
-                "always" => Some(MinimapThumb::Always),
-                "mouseover" => Some(MinimapThumb::Hover),
-                _ => None,
-            },
-        );
-
-        if minimap != settings::MinimapContent::default() {
-            current.editor.minimap = Some(minimap)
-        }
-    }
 }

crates/editor/src/editor_tests.rs 🔗

@@ -31,6 +31,7 @@ use language::{
     tree_sitter_python,
 };
 use language_settings::Formatter;
+use languages::rust_lang;
 use lsp::CompletionParams;
 use multi_buffer::{IndentGuide, PathKey};
 use parking_lot::Mutex;
@@ -50,7 +51,7 @@ use std::{
     iter,
     sync::atomic::{self, AtomicUsize},
 };
-use test::{build_editor_with_project, editor_lsp_test_context::rust_lang};
+use test::build_editor_with_project;
 use text::ToPoint as _;
 use unindent::Unindent;
 use util::{
@@ -62,7 +63,7 @@ use util::{
 use workspace::{
     CloseActiveItem, CloseAllItems, CloseOtherItems, MoveItemToPaneInDirection, NavigationEntry,
     OpenOptions, ViewId,
-    invalid_buffer_view::InvalidBufferView,
+    invalid_item_view::InvalidItemView,
     item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions},
     register_project_item,
 };
@@ -219,7 +220,10 @@ fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
         editor.insert("cd", window, cx);
         editor.end_transaction_at(now, cx);
         assert_eq!(editor.text(cx), "12cd56");
-        assert_eq!(editor.selections.ranges(cx), vec![4..4]);
+        assert_eq!(
+            editor.selections.ranges(&editor.display_snapshot(cx)),
+            vec![4..4]
+        );
 
         editor.start_transaction_at(now, window, cx);
         editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
@@ -228,7 +232,10 @@ fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
         editor.insert("e", window, cx);
         editor.end_transaction_at(now, cx);
         assert_eq!(editor.text(cx), "12cde6");
-        assert_eq!(editor.selections.ranges(cx), vec![5..5]);
+        assert_eq!(
+            editor.selections.ranges(&editor.display_snapshot(cx)),
+            vec![5..5]
+        );
 
         now += group_interval + Duration::from_millis(1);
         editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
@@ -244,30 +251,45 @@ fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
         });
 
         assert_eq!(editor.text(cx), "ab2cde6");
-        assert_eq!(editor.selections.ranges(cx), vec![3..3]);
+        assert_eq!(
+            editor.selections.ranges(&editor.display_snapshot(cx)),
+            vec![3..3]
+        );
 
         // Last transaction happened past the group interval in a different editor.
         // Undo it individually and don't restore selections.
         editor.undo(&Undo, window, cx);
         assert_eq!(editor.text(cx), "12cde6");
-        assert_eq!(editor.selections.ranges(cx), vec![2..2]);
+        assert_eq!(
+            editor.selections.ranges(&editor.display_snapshot(cx)),
+            vec![2..2]
+        );
 
         // First two transactions happened within the group interval in this editor.
         // Undo them together and restore selections.
         editor.undo(&Undo, window, cx);
         editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
         assert_eq!(editor.text(cx), "123456");
-        assert_eq!(editor.selections.ranges(cx), vec![0..0]);
+        assert_eq!(
+            editor.selections.ranges(&editor.display_snapshot(cx)),
+            vec![0..0]
+        );
 
         // Redo the first two transactions together.
         editor.redo(&Redo, window, cx);
         assert_eq!(editor.text(cx), "12cde6");
-        assert_eq!(editor.selections.ranges(cx), vec![5..5]);
+        assert_eq!(
+            editor.selections.ranges(&editor.display_snapshot(cx)),
+            vec![5..5]
+        );
 
         // Redo the last transaction on its own.
         editor.redo(&Redo, window, cx);
         assert_eq!(editor.text(cx), "ab2cde6");
-        assert_eq!(editor.selections.ranges(cx), vec![6..6]);
+        assert_eq!(
+            editor.selections.ranges(&editor.display_snapshot(cx)),
+            vec![6..6]
+        );
 
         // Test empty transactions.
         editor.start_transaction_at(now, window, cx);
@@ -770,10 +792,14 @@ fn test_clone(cx: &mut TestAppContext) {
     );
     assert_set_eq!(
         cloned_editor
-            .update(cx, |editor, _, cx| editor.selections.ranges::<Point>(cx))
+            .update(cx, |editor, _, cx| editor
+                .selections
+                .ranges::<Point>(&editor.display_snapshot(cx)))
             .unwrap(),
         editor
-            .update(cx, |editor, _, cx| editor.selections.ranges(cx))
+            .update(cx, |editor, _, cx| editor
+                .selections
+                .ranges(&editor.display_snapshot(cx)))
             .unwrap()
     );
     assert_set_eq!(
@@ -3161,7 +3187,7 @@ fn test_newline_with_old_selections(cx: &mut TestAppContext) {
             );
         });
         assert_eq!(
-            editor.selections.ranges(cx),
+            editor.selections.ranges(&editor.display_snapshot(cx)),
             &[
                 Point::new(1, 2)..Point::new(1, 2),
                 Point::new(2, 2)..Point::new(2, 2),
@@ -3183,7 +3209,7 @@ fn test_newline_with_old_selections(cx: &mut TestAppContext) {
 
         // The selections are moved after the inserted newlines
         assert_eq!(
-            editor.selections.ranges(cx),
+            editor.selections.ranges(&editor.display_snapshot(cx)),
             &[
                 Point::new(2, 0)..Point::new(2, 0),
                 Point::new(4, 0)..Point::new(4, 0),
@@ -3673,13 +3699,19 @@ fn test_insert_with_old_selections(cx: &mut TestAppContext) {
             buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
             assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
         });
-        assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
+        assert_eq!(
+            editor.selections.ranges(&editor.display_snapshot(cx)),
+            &[2..2, 7..7, 12..12],
+        );
 
         editor.insert("Z", window, cx);
         assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
 
         // The selections are moved after the inserted characters
-        assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
+        assert_eq!(
+            editor.selections.ranges(&editor.display_snapshot(cx)),
+            &[3..3, 9..9, 15..15],
+        );
     });
 }
 
@@ -4439,7 +4471,9 @@ fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
         let buffer = buffer.read(cx).as_singleton().unwrap();
 
         assert_eq!(
-            editor.selections.ranges::<Point>(cx),
+            editor
+                .selections
+                .ranges::<Point>(&editor.display_snapshot(cx)),
             &[Point::new(0, 0)..Point::new(0, 0)]
         );
 
@@ -4447,7 +4481,9 @@ fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
         editor.join_lines(&JoinLines, window, cx);
         assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
         assert_eq!(
-            editor.selections.ranges::<Point>(cx),
+            editor
+                .selections
+                .ranges::<Point>(&editor.display_snapshot(cx)),
             &[Point::new(0, 3)..Point::new(0, 3)]
         );
 
@@ -4458,7 +4494,9 @@ fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
         editor.join_lines(&JoinLines, window, cx);
         assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
         assert_eq!(
-            editor.selections.ranges::<Point>(cx),
+            editor
+                .selections
+                .ranges::<Point>(&editor.display_snapshot(cx)),
             &[Point::new(0, 11)..Point::new(0, 11)]
         );
 
@@ -4466,7 +4504,9 @@ fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
         editor.undo(&Undo, window, cx);
         assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
         assert_eq!(
-            editor.selections.ranges::<Point>(cx),
+            editor
+                .selections
+                .ranges::<Point>(&editor.display_snapshot(cx)),
             &[Point::new(0, 5)..Point::new(2, 2)]
         );
 
@@ -4477,7 +4517,9 @@ fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
         editor.join_lines(&JoinLines, window, cx);
         assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
         assert_eq!(
-            editor.selections.ranges::<Point>(cx),
+            editor
+                .selections
+                .ranges::<Point>(&editor.display_snapshot(cx)),
             [Point::new(2, 3)..Point::new(2, 3)]
         );
 
@@ -4485,7 +4527,9 @@ fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
         editor.join_lines(&JoinLines, window, cx);
         assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
         assert_eq!(
-            editor.selections.ranges::<Point>(cx),
+            editor
+                .selections
+                .ranges::<Point>(&editor.display_snapshot(cx)),
             [Point::new(2, 3)..Point::new(2, 3)]
         );
 
@@ -4493,7 +4537,9 @@ fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
         editor.join_lines(&JoinLines, window, cx);
         assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
         assert_eq!(
-            editor.selections.ranges::<Point>(cx),
+            editor
+                .selections
+                .ranges::<Point>(&editor.display_snapshot(cx)),
             [Point::new(2, 3)..Point::new(2, 3)]
         );
 
@@ -4550,7 +4596,9 @@ fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
         assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
 
         assert_eq!(
-            editor.selections.ranges::<Point>(cx),
+            editor
+                .selections
+                .ranges::<Point>(&editor.display_snapshot(cx)),
             [
                 Point::new(0, 7)..Point::new(0, 7),
                 Point::new(1, 3)..Point::new(1, 3)
@@ -5908,15 +5956,24 @@ fn test_transpose(cx: &mut TestAppContext) {
         });
         editor.transpose(&Default::default(), window, cx);
         assert_eq!(editor.text(cx), "bac");
-        assert_eq!(editor.selections.ranges(cx), [2..2]);
+        assert_eq!(
+            editor.selections.ranges(&editor.display_snapshot(cx)),
+            [2..2]
+        );
 
         editor.transpose(&Default::default(), window, cx);
         assert_eq!(editor.text(cx), "bca");
-        assert_eq!(editor.selections.ranges(cx), [3..3]);
+        assert_eq!(
+            editor.selections.ranges(&editor.display_snapshot(cx)),
+            [3..3]
+        );
 
         editor.transpose(&Default::default(), window, cx);
         assert_eq!(editor.text(cx), "bac");
-        assert_eq!(editor.selections.ranges(cx), [3..3]);
+        assert_eq!(
+            editor.selections.ranges(&editor.display_snapshot(cx)),
+            [3..3]
+        );
 
         editor
     });
@@ -5929,22 +5986,34 @@ fn test_transpose(cx: &mut TestAppContext) {
         });
         editor.transpose(&Default::default(), window, cx);
         assert_eq!(editor.text(cx), "acb\nde");
-        assert_eq!(editor.selections.ranges(cx), [3..3]);
+        assert_eq!(
+            editor.selections.ranges(&editor.display_snapshot(cx)),
+            [3..3]
+        );
 
         editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
             s.select_ranges([4..4])
         });
         editor.transpose(&Default::default(), window, cx);
         assert_eq!(editor.text(cx), "acbd\ne");
-        assert_eq!(editor.selections.ranges(cx), [5..5]);
+        assert_eq!(
+            editor.selections.ranges(&editor.display_snapshot(cx)),
+            [5..5]
+        );
 
         editor.transpose(&Default::default(), window, cx);
         assert_eq!(editor.text(cx), "acbde\n");
-        assert_eq!(editor.selections.ranges(cx), [6..6]);
+        assert_eq!(
+            editor.selections.ranges(&editor.display_snapshot(cx)),
+            [6..6]
+        );
 
         editor.transpose(&Default::default(), window, cx);
         assert_eq!(editor.text(cx), "acbd\ne");
-        assert_eq!(editor.selections.ranges(cx), [6..6]);
+        assert_eq!(
+            editor.selections.ranges(&editor.display_snapshot(cx)),
+            [6..6]
+        );
 
         editor
     });
@@ -5957,23 +6026,38 @@ fn test_transpose(cx: &mut TestAppContext) {
         });
         editor.transpose(&Default::default(), window, cx);
         assert_eq!(editor.text(cx), "bacd\ne");
-        assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
+        assert_eq!(
+            editor.selections.ranges(&editor.display_snapshot(cx)),
+            [2..2, 3..3, 5..5]
+        );
 
         editor.transpose(&Default::default(), window, cx);
         assert_eq!(editor.text(cx), "bcade\n");
-        assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
+        assert_eq!(
+            editor.selections.ranges(&editor.display_snapshot(cx)),
+            [3..3, 4..4, 6..6]
+        );
 
         editor.transpose(&Default::default(), window, cx);
         assert_eq!(editor.text(cx), "bcda\ne");
-        assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
+        assert_eq!(
+            editor.selections.ranges(&editor.display_snapshot(cx)),
+            [4..4, 6..6]
+        );
 
         editor.transpose(&Default::default(), window, cx);
         assert_eq!(editor.text(cx), "bcade\n");
-        assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
+        assert_eq!(
+            editor.selections.ranges(&editor.display_snapshot(cx)),
+            [4..4, 6..6]
+        );
 
         editor.transpose(&Default::default(), window, cx);
         assert_eq!(editor.text(cx), "bcaed\n");
-        assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
+        assert_eq!(
+            editor.selections.ranges(&editor.display_snapshot(cx)),
+            [5..5, 6..6]
+        );
 
         editor
     });
@@ -5986,15 +6070,24 @@ fn test_transpose(cx: &mut TestAppContext) {
         });
         editor.transpose(&Default::default(), window, cx);
         assert_eq!(editor.text(cx), "🏀🍐✋");
-        assert_eq!(editor.selections.ranges(cx), [8..8]);
+        assert_eq!(
+            editor.selections.ranges(&editor.display_snapshot(cx)),
+            [8..8]
+        );
 
         editor.transpose(&Default::default(), window, cx);
         assert_eq!(editor.text(cx), "🏀✋🍐");
-        assert_eq!(editor.selections.ranges(cx), [11..11]);
+        assert_eq!(
+            editor.selections.ranges(&editor.display_snapshot(cx)),
+            [11..11]
+        );
 
         editor.transpose(&Default::default(), window, cx);
         assert_eq!(editor.text(cx), "🏀🍐✋");
-        assert_eq!(editor.selections.ranges(cx), [11..11]);
+        assert_eq!(
+            editor.selections.ranges(&editor.display_snapshot(cx)),
+            [11..11]
+        );
 
         editor
     });
@@ -9540,7 +9633,7 @@ async fn test_autoindent(cx: &mut TestAppContext) {
         editor.newline(&Newline, window, cx);
         assert_eq!(editor.text(cx), "fn a(\n    \n) {\n    \n}\n");
         assert_eq!(
-            editor.selections.ranges(cx),
+            editor.selections.ranges(&editor.display_snapshot(cx)),
             &[
                 Point::new(1, 4)..Point::new(1, 4),
                 Point::new(3, 4)..Point::new(3, 4),
@@ -9616,7 +9709,7 @@ async fn test_autoindent_disabled(cx: &mut TestAppContext) {
             )
         );
         assert_eq!(
-            editor.selections.ranges(cx),
+            editor.selections.ranges(&editor.display_snapshot(cx)),
             &[
                 Point::new(1, 0)..Point::new(1, 0),
                 Point::new(3, 0)..Point::new(3, 0),
@@ -10255,7 +10348,9 @@ async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
     // Precondition: different languages are active at different locations.
     cx.update_editor(|editor, window, cx| {
         let snapshot = editor.snapshot(window, cx);
-        let cursors = editor.selections.ranges::<usize>(cx);
+        let cursors = editor
+            .selections
+            .ranges::<usize>(&editor.display_snapshot(cx));
         let languages = cursors
             .iter()
             .map(|c| snapshot.language_at(c.start).unwrap().name())
@@ -10700,7 +10795,9 @@ async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
             .unindent()
         );
         assert_eq!(
-            editor.selections.ranges::<Point>(cx),
+            editor
+                .selections
+                .ranges::<Point>(&editor.display_snapshot(cx)),
             [
                 Point::new(0, 4)..Point::new(0, 4),
                 Point::new(1, 4)..Point::new(1, 4),
@@ -10720,7 +10817,9 @@ async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
             .unindent()
         );
         assert_eq!(
-            editor.selections.ranges::<Point>(cx),
+            editor
+                .selections
+                .ranges::<Point>(&editor.display_snapshot(cx)),
             [
                 Point::new(0, 2)..Point::new(0, 2),
                 Point::new(1, 2)..Point::new(1, 2),
@@ -10739,7 +10838,9 @@ async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
             .unindent()
         );
         assert_eq!(
-            editor.selections.ranges::<Point>(cx),
+            editor
+                .selections
+                .ranges::<Point>(&editor.display_snapshot(cx)),
             [
                 Point::new(0, 1)..Point::new(0, 1),
                 Point::new(1, 1)..Point::new(1, 1),
@@ -10945,7 +11046,12 @@ async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
         fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
             let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
             assert_eq!(editor.text(cx), expected_text);
-            assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
+            assert_eq!(
+                editor
+                    .selections
+                    .ranges::<usize>(&editor.display_snapshot(cx)),
+                selection_ranges
+            );
         }
 
         assert(
@@ -10976,7 +11082,7 @@ async fn test_snippets(cx: &mut TestAppContext) {
         let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
         let insertion_ranges = editor
             .selections
-            .all(cx)
+            .all(&editor.display_snapshot(cx))
             .iter()
             .map(|s| s.range())
             .collect::<Vec<_>>();
@@ -11056,7 +11162,7 @@ async fn test_snippet_indentation(cx: &mut TestAppContext) {
         .unwrap();
         let insertion_ranges = editor
             .selections
-            .all(cx)
+            .all(&editor.display_snapshot(cx))
             .iter()
             .map(|s| s.range())
             .collect::<Vec<_>>();
@@ -12557,18 +12663,6 @@ async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
     )
     .await;
 
-    cx.run_until_parked();
-    // Set up a buffer white some trailing whitespace and no trailing newline.
-    cx.set_state(
-        &[
-            "one ",   //
-            "twoˇ",   //
-            "three ", //
-            "four",   //
-        ]
-        .join("\n"),
-    );
-
     // Record which buffer changes have been sent to the language server
     let buffer_changes = Arc::new(Mutex::new(Vec::new()));
     cx.lsp
@@ -12583,7 +12677,6 @@ async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
                 );
             }
         });
-
     // Handle formatting requests to the language server.
     cx.lsp
         .set_request_handler::<lsp::request::Formatting, _, _>({
@@ -12632,6 +12725,18 @@ async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
             }
         });
 
+    // Set up a buffer white some trailing whitespace and no trailing newline.
+    cx.set_state(
+        &[
+            "one ",   //
+            "twoˇ",   //
+            "three ", //
+            "four",   //
+        ]
+        .join("\n"),
+    );
+    cx.run_until_parked();
+
     // Submit a format request.
     let format = cx
         .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
@@ -16064,7 +16169,7 @@ fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
         editor.handle_input("X", window, cx);
         assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
         assert_eq!(
-            editor.selections.ranges(cx),
+            editor.selections.ranges(&editor.display_snapshot(cx)),
             [
                 Point::new(0, 1)..Point::new(0, 1),
                 Point::new(1, 1)..Point::new(1, 1),
@@ -16078,7 +16183,7 @@ fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
         editor.backspace(&Default::default(), window, cx);
         assert_eq!(editor.text(cx), "Xa\nbbb");
         assert_eq!(
-            editor.selections.ranges(cx),
+            editor.selections.ranges(&editor.display_snapshot(cx)),
             [Point::new(1, 0)..Point::new(1, 0)]
         );
 
@@ -16088,7 +16193,7 @@ fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
         editor.backspace(&Default::default(), window, cx);
         assert_eq!(editor.text(cx), "X\nbb");
         assert_eq!(
-            editor.selections.ranges(cx),
+            editor.selections.ranges(&editor.display_snapshot(cx)),
             [Point::new(0, 1)..Point::new(0, 1)]
         );
     });
@@ -16146,7 +16251,10 @@ fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
             false,
         );
         assert_eq!(editor.text(cx), expected_text);
-        assert_eq!(editor.selections.ranges(cx), expected_selections);
+        assert_eq!(
+            editor.selections.ranges(&editor.display_snapshot(cx)),
+            expected_selections
+        );
 
         editor.newline(&Newline, window, cx);
         let (expected_text, expected_selections) = marked_text_ranges(
@@ -16163,7 +16271,10 @@ fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
             false,
         );
         assert_eq!(editor.text(cx), expected_text);
-        assert_eq!(editor.selections.ranges(cx), expected_selections);
+        assert_eq!(
+            editor.selections.ranges(&editor.display_snapshot(cx)),
+            expected_selections
+        );
     });
 }
 
@@ -16204,7 +16315,7 @@ fn test_refresh_selections(cx: &mut TestAppContext) {
             cx,
         );
         assert_eq!(
-            editor.selections.ranges(cx),
+            editor.selections.ranges(&editor.display_snapshot(cx)),
             [
                 Point::new(1, 3)..Point::new(1, 3),
                 Point::new(2, 1)..Point::new(2, 1),
@@ -16217,7 +16328,7 @@ fn test_refresh_selections(cx: &mut TestAppContext) {
     _ = editor.update(cx, |editor, window, cx| {
         editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
         assert_eq!(
-            editor.selections.ranges(cx),
+            editor.selections.ranges(&editor.display_snapshot(cx)),
             [
                 Point::new(1, 3)..Point::new(1, 3),
                 Point::new(2, 1)..Point::new(2, 1),
@@ -16231,7 +16342,7 @@ fn test_refresh_selections(cx: &mut TestAppContext) {
     _ = editor.update(cx, |editor, window, cx| {
         // Removing an excerpt causes the first selection to become degenerate.
         assert_eq!(
-            editor.selections.ranges(cx),
+            editor.selections.ranges(&editor.display_snapshot(cx)),
             [
                 Point::new(0, 0)..Point::new(0, 0),
                 Point::new(0, 1)..Point::new(0, 1)
@@ -16242,7 +16353,7 @@ fn test_refresh_selections(cx: &mut TestAppContext) {
         // location.
         editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
         assert_eq!(
-            editor.selections.ranges(cx),
+            editor.selections.ranges(&editor.display_snapshot(cx)),
             [
                 Point::new(0, 1)..Point::new(0, 1),
                 Point::new(0, 3)..Point::new(0, 3)
@@ -16286,7 +16397,7 @@ fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
             cx,
         );
         assert_eq!(
-            editor.selections.ranges(cx),
+            editor.selections.ranges(&editor.display_snapshot(cx)),
             [Point::new(1, 3)..Point::new(1, 3)]
         );
         editor
@@ -16297,14 +16408,14 @@ fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
     });
     _ = editor.update(cx, |editor, window, cx| {
         assert_eq!(
-            editor.selections.ranges(cx),
+            editor.selections.ranges(&editor.display_snapshot(cx)),
             [Point::new(0, 0)..Point::new(0, 0)]
         );
 
         // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
         editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
         assert_eq!(
-            editor.selections.ranges(cx),
+            editor.selections.ranges(&editor.display_snapshot(cx)),
             [Point::new(0, 3)..Point::new(0, 3)]
         );
         assert!(editor.selections.pending_anchor().is_some());
@@ -16554,7 +16665,10 @@ async fn test_following(cx: &mut TestAppContext) {
         .await
         .unwrap();
     _ = follower.update(cx, |follower, _, cx| {
-        assert_eq!(follower.selections.ranges(cx), vec![1..1]);
+        assert_eq!(
+            follower.selections.ranges(&follower.display_snapshot(cx)),
+            vec![1..1]
+        );
     });
     assert!(*is_still_following.borrow());
     assert_eq!(*follower_edit_event_count.borrow(), 0);
@@ -16607,7 +16721,10 @@ async fn test_following(cx: &mut TestAppContext) {
         .unwrap();
     _ = follower.update(cx, |follower, _, cx| {
         assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
-        assert_eq!(follower.selections.ranges(cx), vec![0..0]);
+        assert_eq!(
+            follower.selections.ranges(&follower.display_snapshot(cx)),
+            vec![0..0]
+        );
     });
     assert!(*is_still_following.borrow());
 
@@ -16631,7 +16748,10 @@ async fn test_following(cx: &mut TestAppContext) {
         .await
         .unwrap();
     _ = follower.update(cx, |follower, _, cx| {
-        assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
+        assert_eq!(
+            follower.selections.ranges(&follower.display_snapshot(cx)),
+            vec![0..0, 1..1]
+        );
     });
     assert!(*is_still_following.borrow());
 
@@ -16652,7 +16772,10 @@ async fn test_following(cx: &mut TestAppContext) {
         .await
         .unwrap();
     _ = follower.update(cx, |follower, _, cx| {
-        assert_eq!(follower.selections.ranges(cx), vec![0..2]);
+        assert_eq!(
+            follower.selections.ranges(&follower.display_snapshot(cx)),
+            vec![0..2]
+        );
     });
 
     // Scrolling locally breaks the follow
@@ -22828,11 +22951,11 @@ fn add_log_breakpoint_at_cursor(
         .first()
         .and_then(|(anchor, bp)| bp.as_ref().map(|bp| (*anchor, bp.clone())))
         .unwrap_or_else(|| {
-            let cursor_position: Point = editor.selections.newest(cx).head();
+            let snapshot = editor.snapshot(window, cx);
+            let cursor_position: Point =
+                editor.selections.newest(&snapshot.display_snapshot).head();
 
-            let breakpoint_position = editor
-                .snapshot(window, cx)
-                .display_snapshot
+            let breakpoint_position = snapshot
                 .buffer_snapshot()
                 .anchor_before(Point::new(cursor_position.row, 0));
 
@@ -23779,7 +23902,7 @@ println!("5");
             assert_eq!(
                 editor
                     .selections
-                    .all::<Point>(cx)
+                    .all::<Point>(&editor.display_snapshot(cx))
                     .into_iter()
                     .map(|s| s.range())
                     .collect::<Vec<_>>(),
@@ -23822,7 +23945,7 @@ println!("5");
             assert_eq!(
                 editor
                     .selections
-                    .all::<Point>(cx)
+                    .all::<Point>(&editor.display_snapshot(cx))
                     .into_iter()
                     .map(|s| s.range())
                     .collect::<Vec<_>>(),
@@ -23948,7 +24071,7 @@ println!("5");
             assert_eq!(
                 editor
                     .selections
-                    .all::<Point>(cx)
+                    .all::<Point>(&editor.display_snapshot(cx))
                     .into_iter()
                     .map(|s| s.range())
                     .collect::<Vec<_>>(),
@@ -23974,7 +24097,7 @@ println!("5");
             assert_eq!(
                 editor
                     .selections
-                    .all::<Point>(cx)
+                    .all::<Point>(&editor.display_snapshot(cx))
                     .into_iter()
                     .map(|s| s.range())
                     .collect::<Vec<_>>(),
@@ -25378,7 +25501,7 @@ fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Cont
     let (text, ranges) = marked_text_ranges(marked_text, true);
     assert_eq!(editor.text(cx), text);
     assert_eq!(
-        editor.selections.ranges(cx),
+        editor.selections.ranges(&editor.display_snapshot(cx)),
         ranges,
         "Assert selections are {}",
         marked_text
@@ -26295,7 +26418,7 @@ async fn test_non_utf_8_opens(cx: &mut TestAppContext) {
 
     assert_eq!(
         handle.to_any().entity_type(),
-        TypeId::of::<InvalidBufferView>()
+        TypeId::of::<InvalidItemView>()
     );
 }
 
@@ -26882,3 +27005,24 @@ async fn test_copy_line_without_trailing_newline(cx: &mut TestAppContext) {
 
     cx.assert_editor_state("line1\nline2\nˇ");
 }
+
+#[gpui::test]
+async fn test_end_of_editor_context(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
+
+    let mut cx = EditorTestContext::new(cx).await;
+
+    cx.set_state("line1\nline2ˇ");
+    cx.update_editor(|e, window, cx| {
+        e.set_mode(EditorMode::SingleLine);
+        assert!(e.key_context(window, cx).contains("end_of_input"));
+    });
+    cx.set_state("ˇline1\nline2");
+    cx.update_editor(|e, window, cx| {
+        assert!(!e.key_context(window, cx).contains("end_of_input"));
+    });
+    cx.set_state("line1ˇ\nline2");
+    cx.update_editor(|e, window, cx| {
+        assert!(!e.key_context(window, cx).contains("end_of_input"));
+    });
+}

crates/editor/src/element.rs 🔗

@@ -493,6 +493,7 @@ impl EditorElement {
         register_action(editor, window, Editor::stage_and_next);
         register_action(editor, window, Editor::unstage_and_next);
         register_action(editor, window, Editor::expand_all_diff_hunks);
+        register_action(editor, window, Editor::collapse_all_diff_hunks);
         register_action(editor, window, Editor::go_to_previous_change);
         register_action(editor, window, Editor::go_to_next_change);
 
@@ -651,7 +652,6 @@ impl EditorElement {
     fn mouse_left_down(
         editor: &mut Editor,
         event: &MouseDownEvent,
-        hovered_hunk: Option<Range<Anchor>>,
         position_map: &PositionMap,
         line_numbers: &HashMap<MultiBufferRow, LineNumberLayout>,
         window: &mut Window,
@@ -667,7 +667,20 @@ impl EditorElement {
         let mut click_count = event.click_count;
         let mut modifiers = event.modifiers;
 
-        if let Some(hovered_hunk) = hovered_hunk {
+        if let Some(hovered_hunk) =
+            position_map
+                .display_hunks
+                .iter()
+                .find_map(|(hunk, hunk_hitbox)| match hunk {
+                    DisplayDiffHunk::Folded { .. } => None,
+                    DisplayDiffHunk::Unfolded {
+                        multi_buffer_range, ..
+                    } => hunk_hitbox
+                        .as_ref()
+                        .is_some_and(|hitbox| hitbox.is_hovered(window))
+                        .then(|| multi_buffer_range.clone()),
+                })
+        {
             editor.toggle_single_diff_hunk(hovered_hunk, cx);
             cx.notify();
             return;
@@ -1070,7 +1083,10 @@ impl EditorElement {
                     ref mouse_down_time,
                 } => {
                     let drag_and_drop_delay = Duration::from_millis(
-                        EditorSettings::get_global(cx).drag_and_drop_selection.delay,
+                        EditorSettings::get_global(cx)
+                            .drag_and_drop_selection
+                            .delay
+                            .0,
                     );
                     if mouse_down_time.elapsed() >= drag_and_drop_delay {
                         let drop_cursor = Selection {
@@ -1191,10 +1207,10 @@ impl EditorElement {
             if mouse_over_inline_blame || mouse_over_popover {
                 editor.show_blame_popover(*buffer_id, blame_entry, event.position, false, cx);
             } else if !keyboard_grace {
-                editor.hide_blame_popover(cx);
+                editor.hide_blame_popover(false, cx);
             }
         } else {
-            editor.hide_blame_popover(cx);
+            editor.hide_blame_popover(false, cx);
         }
 
         let breakpoint_indicator = if gutter_hovered {
@@ -1377,7 +1393,7 @@ impl EditorElement {
         editor_with_selections.update(cx, |editor, cx| {
             if editor.show_local_selections {
                 let mut layouts = Vec::new();
-                let newest = editor.selections.newest(cx);
+                let newest = editor.selections.newest(&editor.display_snapshot(cx));
                 for selection in local_selections.iter().cloned() {
                     let is_empty = selection.start == selection.end;
                     let is_newest = selection == newest;
@@ -1406,7 +1422,11 @@ impl EditorElement {
                     layouts.push(layout);
                 }
 
-                let player = editor.current_user_player_color(cx);
+                let mut player = editor.current_user_player_color(cx);
+                if !editor.is_focused(window) {
+                    const UNFOCUS_EDITOR_SELECTION_OPACITY: f32 = 0.5;
+                    player.selection = player.selection.opacity(UNFOCUS_EDITOR_SELECTION_OPACITY);
+                }
                 selections.push((player, layouts));
 
                 if let SelectionDragState::Dragging {
@@ -3195,7 +3215,9 @@ impl EditorElement {
 
         let (newest_selection_head, is_relative) = self.editor.update(cx, |editor, cx| {
             let newest_selection_head = newest_selection_head.unwrap_or_else(|| {
-                let newest = editor.selections.newest::<Point>(cx);
+                let newest = editor
+                    .selections
+                    .newest::<Point>(&editor.display_snapshot(cx));
                 SelectionLayout::new(
                     newest,
                     editor.selections.line_mode(),
@@ -3892,7 +3914,7 @@ impl EditorElement {
                                         .children(toggle_chevron_icon)
                                         .tooltip({
                                             let focus_handle = focus_handle.clone();
-                                            move |window, cx| {
+                                            move |_window, cx| {
                                                 Tooltip::with_meta_in(
                                                     "Toggle Excerpt Fold",
                                                     Some(&ToggleFold),
@@ -3905,7 +3927,6 @@ impl EditorElement {
                                                         )
                                                     ),
                                                     &focus_handle,
-                                                    window,
                                                     cx,
                                                 )
                                             }
@@ -4006,15 +4027,11 @@ impl EditorElement {
                                             .id("jump-to-file-button")
                                             .gap_2p5()
                                             .child(Label::new("Jump To File"))
-                                            .children(
-                                                KeyBinding::for_action_in(
-                                                    &OpenExcerpts,
-                                                    &focus_handle,
-                                                    window,
-                                                    cx,
-                                                )
-                                                .map(|binding| binding.into_any_element()),
-                                            ),
+                                            .child(KeyBinding::for_action_in(
+                                                &OpenExcerpts,
+                                                &focus_handle,
+                                                cx,
+                                            )),
                                     )
                                 },
                             )
@@ -6170,7 +6187,10 @@ impl EditorElement {
                 } = &editor.selection_drag_state
                 {
                     let drag_and_drop_delay = Duration::from_millis(
-                        EditorSettings::get_global(cx).drag_and_drop_selection.delay,
+                        EditorSettings::get_global(cx)
+                            .drag_and_drop_selection
+                            .delay
+                            .0,
                     );
                     if mouse_down_time.elapsed() >= drag_and_drop_delay {
                         window.set_cursor_style(
@@ -7212,16 +7232,9 @@ impl EditorElement {
                             * ScrollPixelOffset::from(max_glyph_advance)
                             - ScrollPixelOffset::from(delta.x * scroll_sensitivity))
                             / ScrollPixelOffset::from(max_glyph_advance);
-
-                        let scale_factor = window.scale_factor();
-                        let y = (current_scroll_position.y
-                            * ScrollPixelOffset::from(line_height)
-                            * ScrollPixelOffset::from(scale_factor)
+                        let y = (current_scroll_position.y * ScrollPixelOffset::from(line_height)
                             - ScrollPixelOffset::from(delta.y * scroll_sensitivity))
-                        .round()
-                            / ScrollPixelOffset::from(line_height)
-                            / ScrollPixelOffset::from(scale_factor);
-
+                            / ScrollPixelOffset::from(line_height);
                         let mut scroll_position =
                             point(x, y).clamp(&point(0., 0.), &position_map.scroll_max);
                         let forbid_vertical_scroll = editor.scroll_manager.forbid_vertical_scroll();
@@ -7254,26 +7267,6 @@ impl EditorElement {
         window.on_mouse_event({
             let position_map = layout.position_map.clone();
             let editor = self.editor.clone();
-            let diff_hunk_range =
-                layout
-                    .display_hunks
-                    .iter()
-                    .find_map(|(hunk, hunk_hitbox)| match hunk {
-                        DisplayDiffHunk::Folded { .. } => None,
-                        DisplayDiffHunk::Unfolded {
-                            multi_buffer_range, ..
-                        } => {
-                            if hunk_hitbox
-                                .as_ref()
-                                .map(|hitbox| hitbox.is_hovered(window))
-                                .unwrap_or(false)
-                            {
-                                Some(multi_buffer_range.clone())
-                            } else {
-                                None
-                            }
-                        }
-                    });
             let line_numbers = layout.line_numbers.clone();
 
             move |event: &MouseDownEvent, phase, window, cx| {
@@ -7290,7 +7283,6 @@ impl EditorElement {
                             Self::mouse_left_down(
                                 editor,
                                 event,
-                                diff_hunk_range.clone(),
                                 &position_map,
                                 line_numbers.as_ref(),
                                 window,
@@ -7465,7 +7457,7 @@ impl EditorElement {
                         let clipped_start = range.start.max(&buffer_range.start, buffer);
                         let clipped_end = range.end.min(&buffer_range.end, buffer);
                         let range = buffer_snapshot
-                            .anchor_range_in_excerpt(excerpt_id, clipped_start..clipped_end)?;
+                            .anchor_range_in_excerpt(excerpt_id, *clipped_start..*clipped_end)?;
                         let start = range.start.to_display_point(display_snapshot);
                         let end = range.end.to_display_point(display_snapshot);
                         let selection_layout = SelectionLayout {
@@ -8793,7 +8785,8 @@ impl Element for EditorElement {
                         .editor_with_selections(cx)
                         .map(|editor| {
                             editor.update(cx, |editor, cx| {
-                                let all_selections = editor.selections.all::<Point>(cx);
+                                let all_selections =
+                                    editor.selections.all::<Point>(&snapshot.display_snapshot);
                                 let selected_buffer_ids =
                                     if editor.buffer_kind(cx) == ItemBufferKind::Singleton {
                                         Vec::new()
@@ -8815,10 +8808,12 @@ impl Element for EditorElement {
                                         selected_buffer_ids
                                     };
 
-                                let mut selections = editor
-                                    .selections
-                                    .disjoint_in_range(start_anchor..end_anchor, cx);
-                                selections.extend(editor.selections.pending(cx));
+                                let mut selections = editor.selections.disjoint_in_range(
+                                    start_anchor..end_anchor,
+                                    &snapshot.display_snapshot,
+                                );
+                                selections
+                                    .extend(editor.selections.pending(&snapshot.display_snapshot));
 
                                 (selections, selected_buffer_ids)
                             })
@@ -8928,10 +8923,20 @@ impl Element for EditorElement {
                         cx,
                     );
 
+                    let merged_highlighted_ranges =
+                        if let Some((_, colors)) = document_colors.as_ref() {
+                            &highlighted_ranges
+                                .clone()
+                                .into_iter()
+                                .chain(colors.clone())
+                                .collect()
+                        } else {
+                            &highlighted_ranges
+                        };
                     let bg_segments_per_row = Self::bg_segments_per_row(
                         start_row..end_row,
                         &selections,
-                        &highlighted_ranges,
+                        &merged_highlighted_ranges,
                         self.style.background,
                     );
 

crates/editor/src/git/blame.rs 🔗

@@ -16,7 +16,7 @@ use markdown::Markdown;
 use multi_buffer::{MultiBuffer, RowInfo};
 use project::{
     Project, ProjectItem as _,
-    git_store::{GitStoreEvent, Repository, RepositoryEvent},
+    git_store::{GitStoreEvent, Repository},
 };
 use smallvec::SmallVec;
 use std::{sync::Arc, time::Duration};
@@ -235,8 +235,8 @@ impl GitBlame {
         let git_store = project.read(cx).git_store().clone();
         let git_store_subscription =
             cx.subscribe(&git_store, move |this, _, event, cx| match event {
-                GitStoreEvent::RepositoryUpdated(_, RepositoryEvent::Updated { .. }, _)
-                | GitStoreEvent::RepositoryAdded(_)
+                GitStoreEvent::RepositoryUpdated(_, _, _)
+                | GitStoreEvent::RepositoryAdded
                 | GitStoreEvent::RepositoryRemoved(_) => {
                     log::debug!("Status of git repositories updated. Regenerating blame data...",);
                     this.generate(cx);

crates/editor/src/hover_links.rs 🔗

@@ -1,19 +1,14 @@
 use crate::{
     Anchor, Editor, EditorSettings, EditorSnapshot, FindAllReferences, GoToDefinition,
-    GoToDefinitionSplit, GoToTypeDefinition, GoToTypeDefinitionSplit, GotoDefinitionKind, InlayId,
-    Navigated, PointForPosition, SelectPhase,
-    editor_settings::GoToDefinitionFallback,
-    hover_popover::{self, InlayHover},
+    GoToDefinitionSplit, GoToTypeDefinition, GoToTypeDefinitionSplit, GotoDefinitionKind,
+    Navigated, PointForPosition, SelectPhase, editor_settings::GoToDefinitionFallback,
     scroll::ScrollAmount,
 };
 use gpui::{App, AsyncWindowContext, Context, Entity, Modifiers, Task, Window, px};
 use language::{Bias, ToOffset};
 use linkify::{LinkFinder, LinkKind};
 use lsp::LanguageServerId;
-use project::{
-    HoverBlock, HoverBlockKind, InlayHintLabelPartTooltip, InlayHintTooltip, LocationLink, Project,
-    ResolveState, ResolvedPath,
-};
+use project::{InlayId, LocationLink, Project, ResolvedPath};
 use settings::Settings;
 use std::ops::Range;
 use theme::ActiveTheme as _;
@@ -138,10 +133,9 @@ impl Editor {
                 show_link_definition(modifiers.shift, self, trigger_point, snapshot, window, cx);
             }
             None => {
-                update_inlay_link_and_hover_points(
+                self.update_inlay_link_and_hover_points(
                     snapshot,
                     point_for_position,
-                    self,
                     hovered_link_modifier,
                     modifiers.shift,
                     window,
@@ -283,182 +277,6 @@ impl Editor {
     }
 }
 
-pub fn update_inlay_link_and_hover_points(
-    snapshot: &EditorSnapshot,
-    point_for_position: PointForPosition,
-    editor: &mut Editor,
-    secondary_held: bool,
-    shift_held: bool,
-    window: &mut Window,
-    cx: &mut Context<Editor>,
-) {
-    let hovered_offset = if point_for_position.column_overshoot_after_line_end == 0 {
-        Some(snapshot.display_point_to_inlay_offset(point_for_position.exact_unclipped, Bias::Left))
-    } else {
-        None
-    };
-    let mut go_to_definition_updated = false;
-    let mut hover_updated = false;
-    if let Some(hovered_offset) = hovered_offset {
-        let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
-        let previous_valid_anchor =
-            buffer_snapshot.anchor_before(point_for_position.previous_valid.to_point(snapshot));
-        let next_valid_anchor =
-            buffer_snapshot.anchor_after(point_for_position.next_valid.to_point(snapshot));
-        if let Some(hovered_hint) = editor
-            .visible_inlay_hints(cx)
-            .skip_while(|hint| {
-                hint.position
-                    .cmp(&previous_valid_anchor, &buffer_snapshot)
-                    .is_lt()
-            })
-            .take_while(|hint| {
-                hint.position
-                    .cmp(&next_valid_anchor, &buffer_snapshot)
-                    .is_le()
-            })
-            .max_by_key(|hint| hint.id)
-        {
-            let inlay_hint_cache = editor.inlay_hint_cache();
-            let excerpt_id = previous_valid_anchor.excerpt_id;
-            if let Some(cached_hint) = inlay_hint_cache.hint_by_id(excerpt_id, hovered_hint.id) {
-                match cached_hint.resolve_state {
-                    ResolveState::CanResolve(_, _) => {
-                        if let Some(buffer_id) = snapshot
-                            .buffer_snapshot()
-                            .buffer_id_for_anchor(previous_valid_anchor)
-                        {
-                            inlay_hint_cache.spawn_hint_resolve(
-                                buffer_id,
-                                excerpt_id,
-                                hovered_hint.id,
-                                window,
-                                cx,
-                            );
-                        }
-                    }
-                    ResolveState::Resolved => {
-                        let mut extra_shift_left = 0;
-                        let mut extra_shift_right = 0;
-                        if cached_hint.padding_left {
-                            extra_shift_left += 1;
-                            extra_shift_right += 1;
-                        }
-                        if cached_hint.padding_right {
-                            extra_shift_right += 1;
-                        }
-                        match cached_hint.label {
-                            project::InlayHintLabel::String(_) => {
-                                if let Some(tooltip) = cached_hint.tooltip {
-                                    hover_popover::hover_at_inlay(
-                                        editor,
-                                        InlayHover {
-                                            tooltip: match tooltip {
-                                                InlayHintTooltip::String(text) => HoverBlock {
-                                                    text,
-                                                    kind: HoverBlockKind::PlainText,
-                                                },
-                                                InlayHintTooltip::MarkupContent(content) => {
-                                                    HoverBlock {
-                                                        text: content.value,
-                                                        kind: content.kind,
-                                                    }
-                                                }
-                                            },
-                                            range: InlayHighlight {
-                                                inlay: hovered_hint.id,
-                                                inlay_position: hovered_hint.position,
-                                                range: extra_shift_left
-                                                    ..hovered_hint.text().len() + extra_shift_right,
-                                            },
-                                        },
-                                        window,
-                                        cx,
-                                    );
-                                    hover_updated = true;
-                                }
-                            }
-                            project::InlayHintLabel::LabelParts(label_parts) => {
-                                let hint_start =
-                                    snapshot.anchor_to_inlay_offset(hovered_hint.position);
-                                if let Some((hovered_hint_part, part_range)) =
-                                    hover_popover::find_hovered_hint_part(
-                                        label_parts,
-                                        hint_start,
-                                        hovered_offset,
-                                    )
-                                {
-                                    let highlight_start =
-                                        (part_range.start - hint_start).0 + extra_shift_left;
-                                    let highlight_end =
-                                        (part_range.end - hint_start).0 + extra_shift_right;
-                                    let highlight = InlayHighlight {
-                                        inlay: hovered_hint.id,
-                                        inlay_position: hovered_hint.position,
-                                        range: highlight_start..highlight_end,
-                                    };
-                                    if let Some(tooltip) = hovered_hint_part.tooltip {
-                                        hover_popover::hover_at_inlay(
-                                            editor,
-                                            InlayHover {
-                                                tooltip: match tooltip {
-                                                    InlayHintLabelPartTooltip::String(text) => {
-                                                        HoverBlock {
-                                                            text,
-                                                            kind: HoverBlockKind::PlainText,
-                                                        }
-                                                    }
-                                                    InlayHintLabelPartTooltip::MarkupContent(
-                                                        content,
-                                                    ) => HoverBlock {
-                                                        text: content.value,
-                                                        kind: content.kind,
-                                                    },
-                                                },
-                                                range: highlight.clone(),
-                                            },
-                                            window,
-                                            cx,
-                                        );
-                                        hover_updated = true;
-                                    }
-                                    if let Some((language_server_id, location)) =
-                                        hovered_hint_part.location
-                                        && secondary_held
-                                        && !editor.has_pending_nonempty_selection()
-                                    {
-                                        go_to_definition_updated = true;
-                                        show_link_definition(
-                                            shift_held,
-                                            editor,
-                                            TriggerPoint::InlayHint(
-                                                highlight,
-                                                location,
-                                                language_server_id,
-                                            ),
-                                            snapshot,
-                                            window,
-                                            cx,
-                                        );
-                                    }
-                                }
-                            }
-                        };
-                    }
-                    ResolveState::Resolving => {}
-                }
-            }
-        }
-    }
-
-    if !go_to_definition_updated {
-        editor.hide_hovered_link(cx)
-    }
-    if !hover_updated {
-        hover_popover::hover_at(editor, None, window, cx);
-    }
-}
-
 pub fn show_link_definition(
     shift_held: bool,
     editor: &mut Editor,
@@ -912,7 +730,7 @@ mod tests {
         DisplayPoint,
         display_map::ToDisplayPoint,
         editor_tests::init_test,
-        inlay_hint_cache::tests::{cached_hint_labels, visible_hint_labels},
+        inlays::inlay_hints::tests::{cached_hint_labels, visible_hint_labels},
         test::editor_lsp_test_context::EditorLspTestContext,
     };
     use futures::StreamExt;
@@ -1343,7 +1161,7 @@ mod tests {
         cx.background_executor.run_until_parked();
         cx.update_editor(|editor, _window, cx| {
             let expected_layers = vec![hint_label.to_string()];
-            assert_eq!(expected_layers, cached_hint_labels(editor));
+            assert_eq!(expected_layers, cached_hint_labels(editor, cx));
             assert_eq!(expected_layers, visible_hint_labels(editor, cx));
         });
 

crates/editor/src/hover_popover.rs 🔗

@@ -154,7 +154,7 @@ pub fn hover_at_inlay(
             hide_hover(editor, cx);
         }
 
-        let hover_popover_delay = EditorSettings::get_global(cx).hover_popover_delay;
+        let hover_popover_delay = EditorSettings::get_global(cx).hover_popover_delay.0;
 
         let task = cx.spawn_in(window, async move |this, cx| {
             async move {
@@ -275,7 +275,7 @@ fn show_hover(
         return None;
     }
 
-    let hover_popover_delay = EditorSettings::get_global(cx).hover_popover_delay;
+    let hover_popover_delay = EditorSettings::get_global(cx).hover_popover_delay.0;
     let all_diagnostics_active = editor.active_diagnostics == ActiveDiagnostic::All;
     let active_group_id = if let ActiveDiagnostic::Group(group) = &editor.active_diagnostics {
         Some(group.group_id)
@@ -986,17 +986,17 @@ impl DiagnosticPopover {
 mod tests {
     use super::*;
     use crate::{
-        InlayId, PointForPosition,
+        PointForPosition,
         actions::ConfirmCompletion,
         editor_tests::{handle_completion_request, init_test},
-        hover_links::update_inlay_link_and_hover_points,
-        inlay_hint_cache::tests::{cached_hint_labels, visible_hint_labels},
+        inlays::inlay_hints::tests::{cached_hint_labels, visible_hint_labels},
         test::editor_lsp_test_context::EditorLspTestContext,
     };
     use collections::BTreeSet;
     use gpui::App;
     use indoc::indoc;
     use markdown::parser::MarkdownEvent;
+    use project::InlayId;
     use settings::InlayHintSettingsContent;
     use smol::stream::StreamExt;
     use std::sync::atomic;
@@ -1004,7 +1004,7 @@ mod tests {
     use text::Bias;
 
     fn get_hover_popover_delay(cx: &gpui::TestAppContext) -> u64 {
-        cx.read(|cx: &App| -> u64 { EditorSettings::get_global(cx).hover_popover_delay })
+        cx.read(|cx: &App| -> u64 { EditorSettings::get_global(cx).hover_popover_delay.0 })
     }
 
     impl InfoPopover {
@@ -1648,7 +1648,7 @@ mod tests {
         cx.background_executor.run_until_parked();
         cx.update_editor(|editor, _, cx| {
             let expected_layers = vec![entire_hint_label.to_string()];
-            assert_eq!(expected_layers, cached_hint_labels(editor));
+            assert_eq!(expected_layers, cached_hint_labels(editor, cx));
             assert_eq!(expected_layers, visible_hint_labels(editor, cx));
         });
 
@@ -1687,10 +1687,9 @@ mod tests {
             }
         });
         cx.update_editor(|editor, window, cx| {
-            update_inlay_link_and_hover_points(
+            editor.update_inlay_link_and_hover_points(
                 &editor.snapshot(window, cx),
                 new_type_hint_part_hover_position,
-                editor,
                 true,
                 false,
                 window,
@@ -1758,10 +1757,9 @@ mod tests {
         cx.background_executor.run_until_parked();
 
         cx.update_editor(|editor, window, cx| {
-            update_inlay_link_and_hover_points(
+            editor.update_inlay_link_and_hover_points(
                 &editor.snapshot(window, cx),
                 new_type_hint_part_hover_position,
-                editor,
                 true,
                 false,
                 window,
@@ -1813,10 +1811,9 @@ mod tests {
             }
         });
         cx.update_editor(|editor, window, cx| {
-            update_inlay_link_and_hover_points(
+            editor.update_inlay_link_and_hover_points(
                 &editor.snapshot(window, cx),
                 struct_hint_part_hover_position,
-                editor,
                 true,
                 false,
                 window,

crates/editor/src/indent_guides.rs 🔗

@@ -69,7 +69,7 @@ impl Editor {
         window: &mut Window,
         cx: &mut Context<Editor>,
     ) -> Option<HashSet<usize>> {
-        let selection = self.selections.newest::<Point>(cx);
+        let selection = self.selections.newest::<Point>(&self.display_snapshot(cx));
         let cursor_row = MultiBufferRow(selection.head().row);
 
         let state = &mut self.active_indent_guides_state;

crates/editor/src/inlays.rs 🔗

@@ -0,0 +1,193 @@
+//! The logic, responsible for managing [`Inlay`]s in the editor.
+//!
+//! Inlays are "not real" text that gets mixed into the "real" buffer's text.
+//! They are attached to a certain [`Anchor`], and display certain contents (usually, strings)
+//! between real text around that anchor.
+//!
+//! Inlay examples in Zed:
+//! * inlay hints, received from LSP
+//! * inline values, shown in the debugger
+//! * inline predictions, showing the Zeta/Copilot/etc. predictions
+//! * document color values, if configured to be displayed as inlays
+//! * ... anything else, potentially.
+//!
+//! Editor uses [`crate::DisplayMap`] and [`crate::display_map::InlayMap`] to manage what's rendered inside the editor, using
+//! [`InlaySplice`] to update this state.
+
+/// Logic, related to managing LSP inlay hint inlays.
+pub mod inlay_hints;
+
+use std::{any::TypeId, sync::OnceLock};
+
+use gpui::{Context, HighlightStyle, Hsla, Rgba, Task};
+use multi_buffer::Anchor;
+use project::{InlayHint, InlayId};
+use text::Rope;
+
+use crate::{Editor, hover_links::InlayHighlight};
+
+/// A splice to send into the `inlay_map` for updating the visible inlays on the screen.
+/// "Visible" inlays may not be displayed in the buffer right away, but those are ready to be displayed on further buffer scroll, pane item activations, etc. right away without additional LSP queries or settings changes.
+/// The data in the cache is never used directly for displaying inlays on the screen, to avoid races with updates from LSP queries and sync overhead.
+/// Splice is picked to help avoid extra hint flickering and "jumps" on the screen.
+#[derive(Debug, Default)]
+pub struct InlaySplice {
+    pub to_remove: Vec<InlayId>,
+    pub to_insert: Vec<Inlay>,
+}
+
+impl InlaySplice {
+    pub fn is_empty(&self) -> bool {
+        self.to_remove.is_empty() && self.to_insert.is_empty()
+    }
+}
+
+#[derive(Debug, Clone)]
+pub struct Inlay {
+    pub id: InlayId,
+    pub position: Anchor,
+    pub content: InlayContent,
+}
+
+#[derive(Debug, Clone)]
+pub enum InlayContent {
+    Text(text::Rope),
+    Color(Hsla),
+}
+
+impl Inlay {
+    pub fn hint(id: InlayId, position: Anchor, hint: &InlayHint) -> Self {
+        let mut text = hint.text();
+        if hint.padding_right && text.reversed_chars_at(text.len()).next() != Some(' ') {
+            text.push(" ");
+        }
+        if hint.padding_left && text.chars_at(0).next() != Some(' ') {
+            text.push_front(" ");
+        }
+        Self {
+            id,
+            position,
+            content: InlayContent::Text(text),
+        }
+    }
+
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn mock_hint(id: usize, position: Anchor, text: impl Into<Rope>) -> Self {
+        Self {
+            id: InlayId::Hint(id),
+            position,
+            content: InlayContent::Text(text.into()),
+        }
+    }
+
+    pub fn color(id: usize, position: Anchor, color: Rgba) -> Self {
+        Self {
+            id: InlayId::Color(id),
+            position,
+            content: InlayContent::Color(color.into()),
+        }
+    }
+
+    pub fn edit_prediction<T: Into<Rope>>(id: usize, position: Anchor, text: T) -> Self {
+        Self {
+            id: InlayId::EditPrediction(id),
+            position,
+            content: InlayContent::Text(text.into()),
+        }
+    }
+
+    pub fn debugger<T: Into<Rope>>(id: usize, position: Anchor, text: T) -> Self {
+        Self {
+            id: InlayId::DebuggerValue(id),
+            position,
+            content: InlayContent::Text(text.into()),
+        }
+    }
+
+    pub fn text(&self) -> &Rope {
+        static COLOR_TEXT: OnceLock<Rope> = OnceLock::new();
+        match &self.content {
+            InlayContent::Text(text) => text,
+            InlayContent::Color(_) => COLOR_TEXT.get_or_init(|| Rope::from("◼")),
+        }
+    }
+
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn get_color(&self) -> Option<Hsla> {
+        match self.content {
+            InlayContent::Color(color) => Some(color),
+            _ => None,
+        }
+    }
+}
+
+pub struct InlineValueCache {
+    pub enabled: bool,
+    pub inlays: Vec<InlayId>,
+    pub refresh_task: Task<Option<()>>,
+}
+
+impl InlineValueCache {
+    pub fn new(enabled: bool) -> Self {
+        Self {
+            enabled,
+            inlays: Vec::new(),
+            refresh_task: Task::ready(None),
+        }
+    }
+}
+
+impl Editor {
+    /// Modify which hints are displayed in the editor.
+    pub fn splice_inlays(
+        &mut self,
+        to_remove: &[InlayId],
+        to_insert: Vec<Inlay>,
+        cx: &mut Context<Self>,
+    ) {
+        if let Some(inlay_hints) = &mut self.inlay_hints {
+            for id_to_remove in to_remove {
+                inlay_hints.added_hints.remove(id_to_remove);
+            }
+        }
+        self.display_map.update(cx, |display_map, cx| {
+            display_map.splice_inlays(to_remove, to_insert, cx)
+        });
+        cx.notify();
+    }
+
+    pub(crate) fn highlight_inlays<T: 'static>(
+        &mut self,
+        highlights: Vec<InlayHighlight>,
+        style: HighlightStyle,
+        cx: &mut Context<Self>,
+    ) {
+        self.display_map.update(cx, |map, _| {
+            map.highlight_inlays(TypeId::of::<T>(), highlights, style)
+        });
+        cx.notify();
+    }
+
+    pub fn inline_values_enabled(&self) -> bool {
+        self.inline_value_cache.enabled
+    }
+
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn inline_value_inlays(&self, cx: &gpui::App) -> Vec<Inlay> {
+        self.display_map
+            .read(cx)
+            .current_inlays()
+            .filter(|inlay| matches!(inlay.id, InlayId::DebuggerValue(_)))
+            .cloned()
+            .collect()
+    }
+
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn all_inlays(&self, cx: &gpui::App) -> Vec<Inlay> {
+        self.display_map
+            .read(cx)
+            .current_inlays()
+            .cloned()
+            .collect()
+    }
+}

crates/editor/src/inlay_hint_cache.rs → crates/editor/src/inlays/inlay_hints.rs 🔗

@@ -1,295 +1,116 @@
-/// Stores and updates all data received from LSP <a href="https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_inlayHint">textDocument/inlayHint</a> requests.
-/// Has nothing to do with other inlays, e.g. copilot suggestions — those are stored elsewhere.
-/// On every update, cache may query for more inlay hints and update inlays on the screen.
-///
-/// Inlays stored on screen are in [`crate::display_map::inlay_map`] and this cache is the only way to update any inlay hint data in the visible hints in the inlay map.
-/// For determining the update to the `inlay_map`, the cache requires a list of visible inlay hints — all other hints are not relevant and their separate updates are not influencing the cache work.
-///
-/// Due to the way the data is stored for both visible inlays and the cache, every inlay (and inlay hint) collection is editor-specific, so a single buffer may have multiple sets of inlays of open on different panes.
 use std::{
-    cmp,
+    collections::hash_map,
     ops::{ControlFlow, Range},
     sync::Arc,
     time::Duration,
 };
 
-use crate::{
-    Anchor, Editor, ExcerptId, InlayId, MultiBuffer, MultiBufferSnapshot, display_map::Inlay,
-};
-use anyhow::Context as _;
 use clock::Global;
-use futures::future;
-use gpui::{AppContext as _, AsyncApp, Context, Entity, Task, Window};
+use collections::{HashMap, HashSet};
+use futures::future::join_all;
+use gpui::{App, Entity, Task};
 use language::{
-    Buffer, BufferSnapshot,
-    language_settings::{InlayHintKind, InlayHintSettings},
+    BufferRow,
+    language_settings::{InlayHintKind, InlayHintSettings, language_settings},
 };
-use parking_lot::RwLock;
-use project::{InlayHint, ResolveState};
+use lsp::LanguageServerId;
+use multi_buffer::{Anchor, ExcerptId, MultiBufferSnapshot};
+use parking_lot::Mutex;
+use project::{
+    HoverBlock, HoverBlockKind, InlayHintLabel, InlayHintLabelPartTooltip, InlayHintTooltip,
+    InvalidationStrategy, ResolveState,
+    lsp_store::{CacheInlayHints, ResolvedHint},
+};
+use text::{Bias, BufferId};
+use ui::{Context, Window};
+use util::debug_panic;
 
-use collections::{HashMap, HashSet, hash_map};
-use smol::lock::Semaphore;
-use sum_tree::Bias;
-use text::{BufferId, ToOffset, ToPoint};
-use util::{ResultExt, post_inc};
+use super::{Inlay, InlayId};
+use crate::{
+    Editor, EditorSnapshot, PointForPosition, ToggleInlayHints, ToggleInlineValues, debounce_value,
+    hover_links::{InlayHighlight, TriggerPoint, show_link_definition},
+    hover_popover::{self, InlayHover},
+    inlays::InlaySplice,
+};
 
-pub struct InlayHintCache {
-    hints: HashMap<ExcerptId, Arc<RwLock<CachedExcerptHints>>>,
-    allowed_hint_kinds: HashSet<Option<InlayHintKind>>,
-    version: usize,
-    pub(super) enabled: bool,
+pub fn inlay_hint_settings(
+    location: Anchor,
+    snapshot: &MultiBufferSnapshot,
+    cx: &mut Context<Editor>,
+) -> InlayHintSettings {
+    let file = snapshot.file_at(location);
+    let language = snapshot.language_at(location).map(|l| l.name());
+    language_settings(language, file, cx).inlay_hints
+}
+
+#[derive(Debug)]
+pub struct LspInlayHintData {
+    enabled: bool,
     modifiers_override: bool,
     enabled_in_settings: bool,
-    update_tasks: HashMap<ExcerptId, TasksForRanges>,
-    refresh_task: Task<()>,
+    allowed_hint_kinds: HashSet<Option<InlayHintKind>>,
     invalidate_debounce: Option<Duration>,
     append_debounce: Option<Duration>,
-    lsp_request_limiter: Arc<Semaphore>,
-}
-
-#[derive(Debug)]
-struct TasksForRanges {
-    tasks: Vec<Task<()>>,
-    sorted_ranges: Vec<Range<language::Anchor>>,
-}
-
-#[derive(Debug)]
-struct CachedExcerptHints {
-    version: usize,
-    buffer_version: Global,
-    buffer_id: BufferId,
-    ordered_hints: Vec<InlayId>,
-    hints_by_id: HashMap<InlayId, InlayHint>,
-}
-
-/// 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(super) enum InvalidationStrategy {
-    /// Hints reset is <a href="https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_inlayHint_refresh">requested</a> by the LSP server.
-    /// 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,
-    /// 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 settings change: in addition to LSP capabilities, Zed allows omitting certain hint kinds (defined by the corresponding LSP part: type/parameter/other).
-    /// 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,
-}
-
-/// A splice to send into the `inlay_map` for updating the visible inlays on the screen.
-/// "Visible" inlays may not be displayed in the buffer right away, but those are ready to be displayed on further buffer scroll, pane item activations, etc. right away without additional LSP queries or settings changes.
-/// The data in the cache is never used directly for displaying inlays on the screen, to avoid races with updates from LSP queries and sync overhead.
-/// Splice is picked to help avoid extra hint flickering and "jumps" on the screen.
-#[derive(Debug, Default)]
-pub(super) struct InlaySplice {
-    pub to_remove: Vec<InlayId>,
-    pub to_insert: Vec<Inlay>,
-}
-
-#[derive(Debug)]
-struct ExcerptHintsUpdate {
-    excerpt_id: ExcerptId,
-    remove_from_visible: HashSet<InlayId>,
-    remove_from_cache: HashSet<InlayId>,
-    add_to_cache: Vec<InlayHint>,
-}
-
-#[derive(Debug, Clone, Copy)]
-struct ExcerptQuery {
-    buffer_id: BufferId,
-    excerpt_id: ExcerptId,
-    cache_version: usize,
-    invalidate: InvalidationStrategy,
-    reason: &'static str,
-}
-
-impl InvalidationStrategy {
-    fn should_invalidate(&self) -> bool {
-        matches!(
-            self,
-            InvalidationStrategy::RefreshRequested | InvalidationStrategy::BufferEdited
-        )
-    }
+    hint_refresh_tasks: HashMap<BufferId, HashMap<Vec<Range<BufferRow>>, Vec<Task<()>>>>,
+    hint_chunk_fetched: HashMap<BufferId, (Global, HashSet<Range<BufferRow>>)>,
+    pub added_hints: HashMap<InlayId, Option<InlayHintKind>>,
 }
 
-impl TasksForRanges {
-    fn new(query_ranges: QueryRanges, task: Task<()>) -> Self {
+impl LspInlayHintData {
+    pub fn new(settings: InlayHintSettings) -> Self {
         Self {
-            tasks: vec![task],
-            sorted_ranges: query_ranges.into_sorted_query_ranges(),
+            modifiers_override: false,
+            enabled: settings.enabled,
+            enabled_in_settings: settings.enabled,
+            hint_refresh_tasks: HashMap::default(),
+            added_hints: HashMap::default(),
+            hint_chunk_fetched: HashMap::default(),
+            invalidate_debounce: debounce_value(settings.edit_debounce_ms),
+            append_debounce: debounce_value(settings.scroll_debounce_ms),
+            allowed_hint_kinds: settings.enabled_inlay_hint_kinds(),
         }
     }
 
-    fn update_cached_tasks(
-        &mut self,
-        buffer_snapshot: &BufferSnapshot,
-        query_ranges: QueryRanges,
-        invalidate: InvalidationStrategy,
-        spawn_task: impl FnOnce(QueryRanges) -> Task<()>,
-    ) {
-        let query_ranges = if invalidate.should_invalidate() {
-            self.tasks.clear();
-            self.sorted_ranges = query_ranges.clone().into_sorted_query_ranges();
-            query_ranges
+    pub fn modifiers_override(&mut self, new_override: bool) -> Option<bool> {
+        if self.modifiers_override == new_override {
+            return None;
+        }
+        self.modifiers_override = new_override;
+        if (self.enabled && self.modifiers_override) || (!self.enabled && !self.modifiers_override)
+        {
+            self.clear();
+            Some(false)
         } else {
-            let mut non_cached_query_ranges = query_ranges;
-            non_cached_query_ranges.before_visible = non_cached_query_ranges
-                .before_visible
-                .into_iter()
-                .flat_map(|query_range| {
-                    self.remove_cached_ranges_from_query(buffer_snapshot, query_range)
-                })
-                .collect();
-            non_cached_query_ranges.visible = non_cached_query_ranges
-                .visible
-                .into_iter()
-                .flat_map(|query_range| {
-                    self.remove_cached_ranges_from_query(buffer_snapshot, query_range)
-                })
-                .collect();
-            non_cached_query_ranges.after_visible = non_cached_query_ranges
-                .after_visible
-                .into_iter()
-                .flat_map(|query_range| {
-                    self.remove_cached_ranges_from_query(buffer_snapshot, query_range)
-                })
-                .collect();
-            non_cached_query_ranges
-        };
-
-        if !query_ranges.is_empty() {
-            self.tasks.push(spawn_task(query_ranges));
+            Some(true)
         }
     }
 
-    fn remove_cached_ranges_from_query(
-        &mut self,
-        buffer_snapshot: &BufferSnapshot,
-        query_range: Range<language::Anchor>,
-    ) -> Vec<Range<language::Anchor>> {
-        let mut ranges_to_query = Vec::new();
-        let mut latest_cached_range = None::<&mut Range<language::Anchor>>;
-        for cached_range in self
-            .sorted_ranges
-            .iter_mut()
-            .skip_while(|cached_range| {
-                cached_range
-                    .end
-                    .cmp(&query_range.start, buffer_snapshot)
-                    .is_lt()
-            })
-            .take_while(|cached_range| {
-                cached_range
-                    .start
-                    .cmp(&query_range.end, buffer_snapshot)
-                    .is_le()
-            })
-        {
-            match latest_cached_range {
-                Some(latest_cached_range) => {
-                    if latest_cached_range.end.offset.saturating_add(1) < cached_range.start.offset
-                    {
-                        ranges_to_query.push(latest_cached_range.end..cached_range.start);
-                        cached_range.start = latest_cached_range.end;
-                    }
-                }
-                None => {
-                    if query_range
-                        .start
-                        .cmp(&cached_range.start, buffer_snapshot)
-                        .is_lt()
-                    {
-                        ranges_to_query.push(query_range.start..cached_range.start);
-                        cached_range.start = query_range.start;
-                    }
-                }
-            }
-            latest_cached_range = Some(cached_range);
+    pub fn toggle(&mut self, enabled: bool) -> bool {
+        if self.enabled == enabled {
+            return false;
         }
-
-        match latest_cached_range {
-            Some(latest_cached_range) => {
-                if latest_cached_range.end.offset.saturating_add(1) < query_range.end.offset {
-                    ranges_to_query.push(latest_cached_range.end..query_range.end);
-                    latest_cached_range.end = query_range.end;
-                }
-            }
-            None => {
-                ranges_to_query.push(query_range.clone());
-                self.sorted_ranges.push(query_range);
-                self.sorted_ranges
-                    .sort_by(|range_a, range_b| range_a.start.cmp(&range_b.start, buffer_snapshot));
-            }
+        self.enabled = enabled;
+        self.modifiers_override = false;
+        if !enabled {
+            self.clear();
         }
-
-        ranges_to_query
-    }
-
-    fn invalidate_range(&mut self, buffer: &BufferSnapshot, range: &Range<language::Anchor>) {
-        self.sorted_ranges = self
-            .sorted_ranges
-            .drain(..)
-            .filter_map(|mut cached_range| {
-                if cached_range.start.cmp(&range.end, buffer).is_gt()
-                    || cached_range.end.cmp(&range.start, buffer).is_lt()
-                {
-                    Some(vec![cached_range])
-                } else if cached_range.start.cmp(&range.start, buffer).is_ge()
-                    && cached_range.end.cmp(&range.end, buffer).is_le()
-                {
-                    None
-                } else if range.start.cmp(&cached_range.start, buffer).is_ge()
-                    && range.end.cmp(&cached_range.end, buffer).is_le()
-                {
-                    Some(vec![
-                        cached_range.start..range.start,
-                        range.end..cached_range.end,
-                    ])
-                } else if cached_range.start.cmp(&range.start, buffer).is_ge() {
-                    cached_range.start = range.end;
-                    Some(vec![cached_range])
-                } else {
-                    cached_range.end = range.start;
-                    Some(vec![cached_range])
-                }
-            })
-            .flatten()
-            .collect();
+        true
     }
-}
 
-impl InlayHintCache {
-    pub(super) fn new(inlay_hint_settings: InlayHintSettings) -> Self {
-        Self {
-            allowed_hint_kinds: inlay_hint_settings.enabled_inlay_hint_kinds(),
-            enabled: inlay_hint_settings.enabled,
-            modifiers_override: false,
-            enabled_in_settings: inlay_hint_settings.enabled,
-            hints: HashMap::default(),
-            update_tasks: HashMap::default(),
-            refresh_task: Task::ready(()),
-            invalidate_debounce: debounce_value(inlay_hint_settings.edit_debounce_ms),
-            append_debounce: debounce_value(inlay_hint_settings.scroll_debounce_ms),
-            version: 0,
-            lsp_request_limiter: Arc::new(Semaphore::new(MAX_CONCURRENT_LSP_REQUESTS)),
-        }
+    pub fn clear(&mut self) {
+        self.hint_refresh_tasks.clear();
+        self.hint_chunk_fetched.clear();
+        self.added_hints.clear();
     }
 
     /// Checks inlay hint settings for enabled hint kinds and general enabled state.
     /// Generates corresponding inlay_map splice updates on settings changes.
     /// Does not update inlay hint cache state on disabling or inlay hint kinds change: only reenabling forces new LSP queries.
-    pub(super) fn update_settings(
+    fn update_settings(
         &mut self,
-        multi_buffer: &Entity<MultiBuffer>,
         new_hint_settings: InlayHintSettings,
         visible_hints: Vec<Inlay>,
-        cx: &mut Context<Editor>,
-    ) -> ControlFlow<Option<InlaySplice>> {
+    ) -> ControlFlow<Option<InlaySplice>, Option<InlaySplice>> {
         let old_enabled = self.enabled;
         // If the setting for inlay hints has changed, update `enabled`. This condition avoids inlay
         // hint visibility changes when other settings change (such as theme).
@@ -314,23 +135,30 @@ impl InlayHintCache {
                 if new_allowed_hint_kinds == self.allowed_hint_kinds {
                     ControlFlow::Break(None)
                 } else {
-                    let new_splice = self.new_allowed_hint_kinds_splice(
-                        multi_buffer,
-                        &visible_hints,
-                        &new_allowed_hint_kinds,
-                        cx,
-                    );
-                    if new_splice.is_some() {
-                        self.version += 1;
-                        self.allowed_hint_kinds = new_allowed_hint_kinds;
-                    }
-                    ControlFlow::Break(new_splice)
+                    self.allowed_hint_kinds = new_allowed_hint_kinds;
+                    ControlFlow::Continue(
+                        Some(InlaySplice {
+                            to_remove: visible_hints
+                                .iter()
+                                .filter_map(|inlay| {
+                                    let inlay_kind = self.added_hints.get(&inlay.id).copied()?;
+                                    if !self.allowed_hint_kinds.contains(&inlay_kind) {
+                                        Some(inlay.id)
+                                    } else {
+                                        None
+                                    }
+                                })
+                                .collect(),
+                            to_insert: Vec::new(),
+                        })
+                        .filter(|splice| !splice.is_empty()),
+                    )
                 }
             }
             (true, false) => {
                 self.modifiers_override = false;
                 self.allowed_hint_kinds = new_allowed_hint_kinds;
-                if self.hints.is_empty() {
+                if visible_hints.is_empty() {
                     ControlFlow::Break(None)
                 } else {
                     self.clear();
@@ -343,978 +171,774 @@ impl InlayHintCache {
             (false, true) => {
                 self.modifiers_override = false;
                 self.allowed_hint_kinds = new_allowed_hint_kinds;
-                ControlFlow::Continue(())
+                ControlFlow::Continue(
+                    Some(InlaySplice {
+                        to_remove: visible_hints
+                            .iter()
+                            .filter_map(|inlay| {
+                                let inlay_kind = self.added_hints.get(&inlay.id).copied()?;
+                                if !self.allowed_hint_kinds.contains(&inlay_kind) {
+                                    Some(inlay.id)
+                                } else {
+                                    None
+                                }
+                            })
+                            .collect(),
+                        to_insert: Vec::new(),
+                    })
+                    .filter(|splice| !splice.is_empty()),
+                )
             }
         }
     }
 
-    pub(super) fn modifiers_override(&mut self, new_override: bool) -> Option<bool> {
-        if self.modifiers_override == new_override {
-            return None;
-        }
-        self.modifiers_override = new_override;
-        if (self.enabled && self.modifiers_override) || (!self.enabled && !self.modifiers_override)
-        {
-            self.clear();
-            Some(false)
-        } else {
-            Some(true)
+    pub(crate) fn remove_inlay_chunk_data<'a>(
+        &'a mut self,
+        removed_buffer_ids: impl IntoIterator<Item = &'a BufferId> + 'a,
+    ) {
+        for buffer_id in removed_buffer_ids {
+            self.hint_refresh_tasks.remove(buffer_id);
+            self.hint_chunk_fetched.remove(buffer_id);
         }
     }
+}
 
-    pub(super) fn toggle(&mut self, enabled: bool) -> bool {
-        if self.enabled == enabled {
+#[derive(Debug, Clone)]
+pub enum InlayHintRefreshReason {
+    ModifiersChanged(bool),
+    Toggle(bool),
+    SettingsChange(InlayHintSettings),
+    NewLinesShown,
+    BufferEdited(BufferId),
+    RefreshRequested(LanguageServerId),
+    ExcerptsRemoved(Vec<ExcerptId>),
+}
+
+impl Editor {
+    pub fn supports_inlay_hints(&self, cx: &mut App) -> bool {
+        let Some(provider) = self.semantics_provider.as_ref() else {
             return false;
-        }
-        self.enabled = enabled;
-        self.modifiers_override = false;
-        if !enabled {
-            self.clear();
-        }
-        true
+        };
+
+        let mut supports = false;
+        self.buffer().update(cx, |this, cx| {
+            this.for_each_buffer(|buffer| {
+                supports |= provider.supports_inlay_hints(buffer, cx);
+            });
+        });
+
+        supports
     }
 
-    /// If needed, queries LSP for new inlay hints, using the invalidation strategy given.
-    /// To reduce inlay hint jumping, attempts to query a visible range of the editor(s) first,
-    /// followed by the delayed queries of the same range above and below the visible one.
-    /// This way, subsequent refresh invocations are less likely to trigger LSP queries for the invisible ranges.
-    pub(super) fn spawn_hint_refresh(
+    pub fn toggle_inline_values(
         &mut self,
-        reason_description: &'static str,
-        excerpts_to_query: HashMap<ExcerptId, (Entity<Buffer>, Global, Range<usize>)>,
-        invalidate: InvalidationStrategy,
-        ignore_debounce: bool,
-        cx: &mut Context<Editor>,
-    ) -> Option<InlaySplice> {
-        if (self.enabled && self.modifiers_override) || (!self.enabled && !self.modifiers_override)
-        {
-            return None;
-        }
-        let mut invalidated_hints = Vec::new();
-        if invalidate.should_invalidate() {
-            self.update_tasks
-                .retain(|task_excerpt_id, _| excerpts_to_query.contains_key(task_excerpt_id));
-            self.hints.retain(|cached_excerpt, cached_hints| {
-                let retain = excerpts_to_query.contains_key(cached_excerpt);
-                if !retain {
-                    invalidated_hints.extend(cached_hints.read().ordered_hints.iter().copied());
-                }
-                retain
-            });
-        }
-        if excerpts_to_query.is_empty() && invalidated_hints.is_empty() {
-            return None;
-        }
+        _: &ToggleInlineValues,
+        _: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        self.inline_value_cache.enabled = !self.inline_value_cache.enabled;
 
-        let cache_version = self.version + 1;
-        let debounce_duration = if ignore_debounce {
-            None
-        } else if invalidate.should_invalidate() {
-            self.invalidate_debounce
-        } else {
-            self.append_debounce
-        };
-        self.refresh_task = cx.spawn(async move |editor, cx| {
-            if let Some(debounce_duration) = debounce_duration {
-                cx.background_executor().timer(debounce_duration).await;
-            }
+        self.refresh_inline_values(cx);
+    }
 
-            editor
-                .update(cx, |editor, cx| {
-                    spawn_new_update_tasks(
-                        editor,
-                        reason_description,
-                        excerpts_to_query,
-                        invalidate,
-                        cache_version,
-                        cx,
-                    )
-                })
-                .ok();
-        });
+    pub fn toggle_inlay_hints(
+        &mut self,
+        _: &ToggleInlayHints,
+        _: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        self.refresh_inlay_hints(
+            InlayHintRefreshReason::Toggle(!self.inlay_hints_enabled()),
+            cx,
+        );
+    }
 
-        if invalidated_hints.is_empty() {
-            None
-        } else {
-            Some(InlaySplice {
-                to_remove: invalidated_hints,
-                to_insert: Vec::new(),
-            })
-        }
+    pub fn inlay_hints_enabled(&self) -> bool {
+        self.inlay_hints.as_ref().is_some_and(|cache| cache.enabled)
     }
 
-    fn new_allowed_hint_kinds_splice(
-        &self,
-        multi_buffer: &Entity<MultiBuffer>,
-        visible_hints: &[Inlay],
-        new_kinds: &HashSet<Option<InlayHintKind>>,
-        cx: &mut Context<Editor>,
-    ) -> Option<InlaySplice> {
-        let old_kinds = &self.allowed_hint_kinds;
-        if new_kinds == old_kinds {
-            return None;
+    /// Updates inlay hints for the visible ranges of the singleton buffer(s).
+    /// Based on its parameters, either invalidates the previous data, or appends to it.
+    pub(crate) fn refresh_inlay_hints(
+        &mut self,
+        reason: InlayHintRefreshReason,
+        cx: &mut Context<Self>,
+    ) {
+        if !self.mode.is_full() || self.inlay_hints.is_none() {
+            return;
         }
+        let Some(semantics_provider) = self.semantics_provider() else {
+            return;
+        };
+        let Some(invalidate_cache) = self.refresh_editor_data(&reason, cx) else {
+            return;
+        };
 
-        let mut to_remove = Vec::new();
-        let mut to_insert = Vec::new();
-        let mut shown_hints_to_remove = visible_hints.iter().fold(
-            HashMap::<ExcerptId, Vec<(Anchor, InlayId)>>::default(),
-            |mut current_hints, inlay| {
-                current_hints
-                    .entry(inlay.position.excerpt_id)
-                    .or_default()
-                    .push((inlay.position, inlay.id));
-                current_hints
-            },
-        );
+        let debounce = match &reason {
+            InlayHintRefreshReason::SettingsChange(_)
+            | InlayHintRefreshReason::Toggle(_)
+            | InlayHintRefreshReason::ExcerptsRemoved(_)
+            | InlayHintRefreshReason::ModifiersChanged(_) => None,
+            _may_need_lsp_call => self.inlay_hints.as_ref().and_then(|inlay_hints| {
+                if invalidate_cache.should_invalidate() {
+                    inlay_hints.invalidate_debounce
+                } else {
+                    inlay_hints.append_debounce
+                }
+            }),
+        };
 
-        let multi_buffer = multi_buffer.read(cx);
-        let multi_buffer_snapshot = multi_buffer.snapshot(cx);
-
-        for (excerpt_id, excerpt_cached_hints) in &self.hints {
-            let shown_excerpt_hints_to_remove =
-                shown_hints_to_remove.entry(*excerpt_id).or_default();
-            let excerpt_cached_hints = excerpt_cached_hints.read();
-            let mut excerpt_cache = excerpt_cached_hints.ordered_hints.iter().fuse().peekable();
-            shown_excerpt_hints_to_remove.retain(|(shown_anchor, shown_hint_id)| {
-                let Some(buffer) = multi_buffer.buffer_for_anchor(*shown_anchor, cx) else {
-                    return false;
+        let mut visible_excerpts = self.visible_excerpts(cx);
+        let mut all_affected_buffers = HashSet::default();
+        let ignore_previous_fetches = match reason {
+            InlayHintRefreshReason::ModifiersChanged(_)
+            | InlayHintRefreshReason::Toggle(_)
+            | InlayHintRefreshReason::SettingsChange(_) => true,
+            InlayHintRefreshReason::NewLinesShown
+            | InlayHintRefreshReason::RefreshRequested(_)
+            | InlayHintRefreshReason::ExcerptsRemoved(_) => false,
+            InlayHintRefreshReason::BufferEdited(buffer_id) => {
+                let Some(affected_language) = self
+                    .buffer()
+                    .read(cx)
+                    .buffer(buffer_id)
+                    .and_then(|buffer| buffer.read(cx).language().cloned())
+                else {
+                    return;
                 };
-                let buffer_snapshot = buffer.read(cx).snapshot();
-                loop {
-                    match excerpt_cache.peek() {
-                        Some(&cached_hint_id) => {
-                            let cached_hint = &excerpt_cached_hints.hints_by_id[cached_hint_id];
-                            if cached_hint_id == shown_hint_id {
-                                excerpt_cache.next();
-                                return !new_kinds.contains(&cached_hint.kind);
-                            }
 
-                            match cached_hint
-                                .position
-                                .cmp(&shown_anchor.text_anchor, &buffer_snapshot)
-                            {
-                                cmp::Ordering::Less | cmp::Ordering::Equal => {
-                                    if !old_kinds.contains(&cached_hint.kind)
-                                        && new_kinds.contains(&cached_hint.kind)
-                                        && let Some(anchor) = multi_buffer_snapshot
-                                            .anchor_in_excerpt(*excerpt_id, cached_hint.position)
-                                    {
-                                        to_insert.push(Inlay::hint(
-                                            cached_hint_id.id(),
-                                            anchor,
-                                            cached_hint,
-                                        ));
-                                    }
-                                    excerpt_cache.next();
-                                }
-                                cmp::Ordering::Greater => return true,
+                all_affected_buffers.extend(
+                    self.buffer()
+                        .read(cx)
+                        .all_buffers()
+                        .into_iter()
+                        .filter_map(|buffer| {
+                            let buffer = buffer.read(cx);
+                            if buffer.language() == Some(&affected_language) {
+                                Some(buffer.remote_id())
+                            } else {
+                                None
                             }
-                        }
-                        None => return true,
-                    }
-                }
-            });
+                        }),
+                );
 
-            for cached_hint_id in excerpt_cache {
-                let maybe_missed_cached_hint = &excerpt_cached_hints.hints_by_id[cached_hint_id];
-                let cached_hint_kind = maybe_missed_cached_hint.kind;
-                if !old_kinds.contains(&cached_hint_kind)
-                    && new_kinds.contains(&cached_hint_kind)
-                    && let Some(anchor) = multi_buffer_snapshot
-                        .anchor_in_excerpt(*excerpt_id, maybe_missed_cached_hint.position)
-                {
-                    to_insert.push(Inlay::hint(
-                        cached_hint_id.id(),
-                        anchor,
-                        maybe_missed_cached_hint,
-                    ));
-                }
+                semantics_provider.invalidate_inlay_hints(&all_affected_buffers, cx);
+                visible_excerpts.retain(|_, (visible_buffer, _, _)| {
+                    visible_buffer.read(cx).language() == Some(&affected_language)
+                });
+                false
             }
-        }
+        };
 
-        to_remove.extend(
-            shown_hints_to_remove
-                .into_values()
-                .flatten()
-                .map(|(_, hint_id)| hint_id),
-        );
-        if to_remove.is_empty() && to_insert.is_empty() {
-            None
-        } else {
-            Some(InlaySplice {
-                to_remove,
-                to_insert,
-            })
+        let multi_buffer = self.buffer().clone();
+        let Some(inlay_hints) = self.inlay_hints.as_mut() else {
+            return;
+        };
+
+        if invalidate_cache.should_invalidate() {
+            inlay_hints.clear();
         }
-    }
 
-    /// Completely forget of certain excerpts that were removed from the multibuffer.
-    pub(super) fn remove_excerpts(
-        &mut self,
-        excerpts_removed: &[ExcerptId],
-    ) -> Option<InlaySplice> {
-        let mut to_remove = Vec::new();
-        for excerpt_to_remove in excerpts_removed {
-            self.update_tasks.remove(excerpt_to_remove);
-            if let Some(cached_hints) = self.hints.remove(excerpt_to_remove) {
-                let cached_hints = cached_hints.read();
-                to_remove.extend(cached_hints.ordered_hints.iter().copied());
+        let mut buffers_to_query = HashMap::default();
+        for (excerpt_id, (buffer, buffer_version, visible_range)) in visible_excerpts {
+            let buffer_id = buffer.read(cx).remote_id();
+            if !self.registered_buffers.contains_key(&buffer_id) {
+                continue;
             }
-        }
-        if to_remove.is_empty() {
-            None
-        } else {
-            self.version += 1;
-            Some(InlaySplice {
-                to_remove,
-                to_insert: Vec::new(),
-            })
-        }
-    }
 
-    pub(super) fn clear(&mut self) {
-        if !self.update_tasks.is_empty() || !self.hints.is_empty() {
-            self.version += 1;
+            let buffer_snapshot = buffer.read(cx).snapshot();
+            let buffer_anchor_range = buffer_snapshot.anchor_before(visible_range.start)
+                ..buffer_snapshot.anchor_after(visible_range.end);
+
+            let visible_excerpts =
+                buffers_to_query
+                    .entry(buffer_id)
+                    .or_insert_with(|| VisibleExcerpts {
+                        excerpts: Vec::new(),
+                        ranges: Vec::new(),
+                        buffer_version: buffer_version.clone(),
+                        buffer: buffer.clone(),
+                    });
+            visible_excerpts.buffer_version = buffer_version;
+            visible_excerpts.excerpts.push(excerpt_id);
+            visible_excerpts.ranges.push(buffer_anchor_range);
         }
-        self.update_tasks.clear();
-        self.refresh_task = Task::ready(());
-        self.hints.clear();
-    }
 
-    pub(super) fn hint_by_id(&self, excerpt_id: ExcerptId, hint_id: InlayId) -> Option<InlayHint> {
-        self.hints
-            .get(&excerpt_id)?
-            .read()
-            .hints_by_id
-            .get(&hint_id)
-            .cloned()
-    }
+        let all_affected_buffers = Arc::new(Mutex::new(all_affected_buffers));
+        for (buffer_id, visible_excerpts) in buffers_to_query {
+            let Some(buffer) = multi_buffer.read(cx).buffer(buffer_id) else {
+                continue;
+            };
+            let fetched_tasks = inlay_hints.hint_chunk_fetched.entry(buffer_id).or_default();
+            if visible_excerpts
+                .buffer_version
+                .changed_since(&fetched_tasks.0)
+            {
+                fetched_tasks.1.clear();
+                fetched_tasks.0 = visible_excerpts.buffer_version.clone();
+                inlay_hints.hint_refresh_tasks.remove(&buffer_id);
+            }
 
-    pub fn hints(&self) -> Vec<InlayHint> {
-        let mut hints = Vec::new();
-        for excerpt_hints in self.hints.values() {
-            let excerpt_hints = excerpt_hints.read();
-            hints.extend(
-                excerpt_hints
-                    .ordered_hints
-                    .iter()
-                    .map(|id| &excerpt_hints.hints_by_id[id])
-                    .cloned(),
-            );
-        }
-        hints
-    }
+            let applicable_chunks =
+                semantics_provider.applicable_inlay_chunks(&buffer, &visible_excerpts.ranges, cx);
 
-    /// Queries a certain hint from the cache for extra data via the LSP <a href="https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#inlayHint_resolve">resolve</a> request.
-    pub(super) fn spawn_hint_resolve(
-        &self,
-        buffer_id: BufferId,
-        excerpt_id: ExcerptId,
-        id: InlayId,
-        window: &mut Window,
-        cx: &mut Context<Editor>,
-    ) {
-        if let Some(excerpt_hints) = self.hints.get(&excerpt_id) {
-            let mut guard = excerpt_hints.write();
-            if let Some(cached_hint) = guard.hints_by_id.get_mut(&id)
-                && let ResolveState::CanResolve(server_id, _) = &cached_hint.resolve_state
+            match inlay_hints
+                .hint_refresh_tasks
+                .entry(buffer_id)
+                .or_default()
+                .entry(applicable_chunks)
             {
-                let hint_to_resolve = cached_hint.clone();
-                let server_id = *server_id;
-                cached_hint.resolve_state = ResolveState::Resolving;
-                drop(guard);
-                cx.spawn_in(window, async move |editor, cx| {
-                    let resolved_hint_task = editor.update(cx, |editor, cx| {
-                        let buffer = editor.buffer().read(cx).buffer(buffer_id)?;
-                        editor.semantics_provider.as_ref()?.resolve_inlay_hint(
-                            hint_to_resolve,
-                            buffer,
-                            server_id,
+                hash_map::Entry::Occupied(mut o) => {
+                    if invalidate_cache.should_invalidate() || ignore_previous_fetches {
+                        o.get_mut().push(spawn_editor_hints_refresh(
+                            buffer_id,
+                            invalidate_cache,
+                            ignore_previous_fetches,
+                            debounce,
+                            visible_excerpts,
+                            all_affected_buffers.clone(),
                             cx,
-                        )
-                    })?;
-                    if let Some(resolved_hint_task) = resolved_hint_task {
-                        let mut resolved_hint =
-                            resolved_hint_task.await.context("hint resolve task")?;
-                        editor.read_with(cx, |editor, _| {
-                            if let Some(excerpt_hints) =
-                                editor.inlay_hint_cache.hints.get(&excerpt_id)
-                            {
-                                let mut guard = excerpt_hints.write();
-                                if let Some(cached_hint) = guard.hints_by_id.get_mut(&id)
-                                    && cached_hint.resolve_state == ResolveState::Resolving
-                                {
-                                    resolved_hint.resolve_state = ResolveState::Resolved;
-                                    *cached_hint = resolved_hint;
-                                }
-                            }
-                        })?;
+                        ));
                     }
-
-                    anyhow::Ok(())
-                })
-                .detach_and_log_err(cx);
+                }
+                hash_map::Entry::Vacant(v) => {
+                    v.insert(Vec::new()).push(spawn_editor_hints_refresh(
+                        buffer_id,
+                        invalidate_cache,
+                        ignore_previous_fetches,
+                        debounce,
+                        visible_excerpts,
+                        all_affected_buffers.clone(),
+                        cx,
+                    ));
+                }
             }
         }
     }
-}
 
-fn debounce_value(debounce_ms: u64) -> Option<Duration> {
-    if debounce_ms > 0 {
-        Some(Duration::from_millis(debounce_ms))
-    } else {
-        None
+    pub fn clear_inlay_hints(&mut self, cx: &mut Context<Self>) {
+        let to_remove = self
+            .visible_inlay_hints(cx)
+            .into_iter()
+            .map(|inlay| {
+                let inlay_id = inlay.id;
+                if let Some(inlay_hints) = &mut self.inlay_hints {
+                    inlay_hints.added_hints.remove(&inlay_id);
+                }
+                inlay_id
+            })
+            .collect::<Vec<_>>();
+        self.splice_inlays(&to_remove, Vec::new(), cx);
     }
-}
-
-fn spawn_new_update_tasks(
-    editor: &mut Editor,
-    reason: &'static str,
-    excerpts_to_query: HashMap<ExcerptId, (Entity<Buffer>, Global, Range<usize>)>,
-    invalidate: InvalidationStrategy,
-    update_cache_version: usize,
-    cx: &mut Context<Editor>,
-) {
-    for (excerpt_id, (excerpt_buffer, new_task_buffer_version, excerpt_visible_range)) in
-        excerpts_to_query
-    {
-        if excerpt_visible_range.is_empty() {
-            continue;
-        }
-        let buffer = excerpt_buffer.read(cx);
-        let buffer_id = buffer.remote_id();
-        let buffer_snapshot = buffer.snapshot();
-        if buffer_snapshot
-            .version()
-            .changed_since(&new_task_buffer_version)
-        {
-            continue;
-        }
-
-        if let Some(cached_excerpt_hints) = editor.inlay_hint_cache.hints.get(&excerpt_id) {
-            let cached_excerpt_hints = cached_excerpt_hints.read();
-            let cached_buffer_version = &cached_excerpt_hints.buffer_version;
-            if cached_excerpt_hints.version > update_cache_version
-                || cached_buffer_version.changed_since(&new_task_buffer_version)
-            {
-                continue;
-            }
-        };
 
-        let Some(query_ranges) = editor.buffer.update(cx, |multi_buffer, cx| {
-            determine_query_ranges(
-                multi_buffer,
-                excerpt_id,
-                &excerpt_buffer,
-                excerpt_visible_range,
-                cx,
-            )
-        }) else {
-            return;
-        };
-        let query = ExcerptQuery {
-            buffer_id,
-            excerpt_id,
-            cache_version: update_cache_version,
-            invalidate,
-            reason,
+    fn refresh_editor_data(
+        &mut self,
+        reason: &InlayHintRefreshReason,
+        cx: &mut Context<'_, Editor>,
+    ) -> Option<InvalidationStrategy> {
+        let visible_inlay_hints = self.visible_inlay_hints(cx);
+        let Some(inlay_hints) = self.inlay_hints.as_mut() else {
+            return None;
         };
 
-        let mut new_update_task =
-            |query_ranges| new_update_task(query, query_ranges, excerpt_buffer.clone(), cx);
-
-        match editor.inlay_hint_cache.update_tasks.entry(excerpt_id) {
-            hash_map::Entry::Occupied(mut o) => {
-                o.get_mut().update_cached_tasks(
-                    &buffer_snapshot,
-                    query_ranges,
-                    invalidate,
-                    new_update_task,
-                );
-            }
-            hash_map::Entry::Vacant(v) => {
-                v.insert(TasksForRanges::new(
-                    query_ranges.clone(),
-                    new_update_task(query_ranges),
-                ));
+        let invalidate_cache = match reason {
+            InlayHintRefreshReason::ModifiersChanged(enabled) => {
+                match inlay_hints.modifiers_override(*enabled) {
+                    Some(enabled) => {
+                        if enabled {
+                            InvalidationStrategy::None
+                        } else {
+                            self.clear_inlay_hints(cx);
+                            return None;
+                        }
+                    }
+                    None => return None,
+                }
             }
-        }
-    }
-}
-
-#[derive(Debug, Clone)]
-struct QueryRanges {
-    before_visible: Vec<Range<language::Anchor>>,
-    visible: Vec<Range<language::Anchor>>,
-    after_visible: Vec<Range<language::Anchor>>,
-}
-
-impl QueryRanges {
-    fn is_empty(&self) -> bool {
-        self.before_visible.is_empty() && self.visible.is_empty() && self.after_visible.is_empty()
-    }
-
-    fn into_sorted_query_ranges(self) -> Vec<Range<text::Anchor>> {
-        let mut sorted_ranges = Vec::with_capacity(
-            self.before_visible.len() + self.visible.len() + self.after_visible.len(),
-        );
-        sorted_ranges.extend(self.before_visible);
-        sorted_ranges.extend(self.visible);
-        sorted_ranges.extend(self.after_visible);
-        sorted_ranges
-    }
-}
-
-fn determine_query_ranges(
-    multi_buffer: &mut MultiBuffer,
-    excerpt_id: ExcerptId,
-    excerpt_buffer: &Entity<Buffer>,
-    excerpt_visible_range: Range<usize>,
-    cx: &mut Context<MultiBuffer>,
-) -> Option<QueryRanges> {
-    let buffer = excerpt_buffer.read(cx);
-    let full_excerpt_range = multi_buffer
-        .excerpts_for_buffer(buffer.remote_id(), cx)
-        .into_iter()
-        .find(|(id, _)| id == &excerpt_id)
-        .map(|(_, range)| range.context)?;
-    let snapshot = buffer.snapshot();
-    let excerpt_visible_len = excerpt_visible_range.end - excerpt_visible_range.start;
-
-    let visible_range = if excerpt_visible_range.start == excerpt_visible_range.end {
-        return None;
-    } else {
-        vec![
-            buffer.anchor_before(snapshot.clip_offset(excerpt_visible_range.start, Bias::Left))
-                ..buffer.anchor_after(snapshot.clip_offset(excerpt_visible_range.end, Bias::Right)),
-        ]
-    };
-
-    let full_excerpt_range_end_offset = full_excerpt_range.end.to_offset(&snapshot);
-    let after_visible_range_start = excerpt_visible_range
-        .end
-        .saturating_add(1)
-        .min(full_excerpt_range_end_offset)
-        .min(buffer.len());
-    let after_visible_range = if after_visible_range_start == full_excerpt_range_end_offset {
-        Vec::new()
-    } else {
-        let after_range_end_offset = after_visible_range_start
-            .saturating_add(excerpt_visible_len)
-            .min(full_excerpt_range_end_offset)
-            .min(buffer.len());
-        vec![
-            buffer.anchor_before(snapshot.clip_offset(after_visible_range_start, Bias::Left))
-                ..buffer.anchor_after(snapshot.clip_offset(after_range_end_offset, Bias::Right)),
-        ]
-    };
-
-    let full_excerpt_range_start_offset = full_excerpt_range.start.to_offset(&snapshot);
-    let before_visible_range_end = excerpt_visible_range
-        .start
-        .saturating_sub(1)
-        .max(full_excerpt_range_start_offset);
-    let before_visible_range = if before_visible_range_end == full_excerpt_range_start_offset {
-        Vec::new()
-    } else {
-        let before_range_start_offset = before_visible_range_end
-            .saturating_sub(excerpt_visible_len)
-            .max(full_excerpt_range_start_offset);
-        vec![
-            buffer.anchor_before(snapshot.clip_offset(before_range_start_offset, Bias::Left))
-                ..buffer.anchor_after(snapshot.clip_offset(before_visible_range_end, Bias::Right)),
-        ]
-    };
-
-    Some(QueryRanges {
-        before_visible: before_visible_range,
-        visible: visible_range,
-        after_visible: after_visible_range,
-    })
-}
-
-const MAX_CONCURRENT_LSP_REQUESTS: usize = 5;
-const INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS: u64 = 400;
-
-fn new_update_task(
-    query: ExcerptQuery,
-    query_ranges: QueryRanges,
-    excerpt_buffer: Entity<Buffer>,
-    cx: &mut Context<Editor>,
-) -> Task<()> {
-    cx.spawn(async move |editor, cx| {
-        let visible_range_update_results = future::join_all(
-            query_ranges
-                .visible
-                .into_iter()
-                .filter_map(|visible_range| {
-                    let fetch_task = editor
-                        .update(cx, |_, cx| {
-                            fetch_and_update_hints(
-                                excerpt_buffer.clone(),
-                                query,
-                                visible_range.clone(),
-                                query.invalidate.should_invalidate(),
-                                cx,
-                            )
-                        })
-                        .log_err()?;
-                    Some(async move { (visible_range, fetch_task.await) })
-                }),
-        )
-        .await;
-
-        let hint_delay = cx.background_executor().timer(Duration::from_millis(
-            INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS,
-        ));
-
-        let query_range_failed =
-            |range: &Range<language::Anchor>, e: anyhow::Error, cx: &mut AsyncApp| {
-                log::error!("inlay hint update task for range failed: {e:#?}");
-                editor
-                    .update(cx, |editor, cx| {
-                        if let Some(task_ranges) = editor
-                            .inlay_hint_cache
-                            .update_tasks
-                            .get_mut(&query.excerpt_id)
+            InlayHintRefreshReason::Toggle(enabled) => {
+                if inlay_hints.toggle(*enabled) {
+                    if *enabled {
+                        InvalidationStrategy::None
+                    } else {
+                        self.clear_inlay_hints(cx);
+                        return None;
+                    }
+                } else {
+                    return None;
+                }
+            }
+            InlayHintRefreshReason::SettingsChange(new_settings) => {
+                match inlay_hints.update_settings(*new_settings, visible_inlay_hints) {
+                    ControlFlow::Break(Some(InlaySplice {
+                        to_remove,
+                        to_insert,
+                    })) => {
+                        self.splice_inlays(&to_remove, to_insert, cx);
+                        return None;
+                    }
+                    ControlFlow::Break(None) => return None,
+                    ControlFlow::Continue(splice) => {
+                        if let Some(InlaySplice {
+                            to_remove,
+                            to_insert,
+                        }) = splice
                         {
-                            let buffer_snapshot = excerpt_buffer.read(cx).snapshot();
-                            task_ranges.invalidate_range(&buffer_snapshot, range);
+                            self.splice_inlays(&to_remove, to_insert, cx);
                         }
-                    })
-                    .ok()
-            };
-
-        for (range, result) in visible_range_update_results {
-            if let Err(e) = result {
-                query_range_failed(&range, e, cx);
+                        InvalidationStrategy::None
+                    }
+                }
             }
-        }
-
-        hint_delay.await;
-        let invisible_range_update_results = future::join_all(
-            query_ranges
-                .before_visible
-                .into_iter()
-                .chain(query_ranges.after_visible.into_iter())
-                .filter_map(|invisible_range| {
-                    let fetch_task = editor
-                        .update(cx, |_, cx| {
-                            fetch_and_update_hints(
-                                excerpt_buffer.clone(),
-                                query,
-                                invisible_range.clone(),
-                                false, // visible screen request already invalidated the entries
-                                cx,
-                            )
-                        })
-                        .log_err()?;
-                    Some(async move { (invisible_range, fetch_task.await) })
-                }),
-        )
-        .await;
-        for (range, result) in invisible_range_update_results {
-            if let Err(e) = result {
-                query_range_failed(&range, e, cx);
+            InlayHintRefreshReason::ExcerptsRemoved(excerpts_removed) => {
+                let to_remove = self
+                    .display_map
+                    .read(cx)
+                    .current_inlays()
+                    .filter_map(|inlay| {
+                        if excerpts_removed.contains(&inlay.position.excerpt_id) {
+                            Some(inlay.id)
+                        } else {
+                            None
+                        }
+                    })
+                    .collect::<Vec<_>>();
+                self.splice_inlays(&to_remove, Vec::new(), cx);
+                return None;
             }
-        }
-    })
-}
-
-fn fetch_and_update_hints(
-    excerpt_buffer: Entity<Buffer>,
-    query: ExcerptQuery,
-    fetch_range: Range<language::Anchor>,
-    invalidate: bool,
-    cx: &mut Context<Editor>,
-) -> Task<anyhow::Result<()>> {
-    cx.spawn(async move |editor, cx|{
-        let buffer_snapshot = excerpt_buffer.read_with(cx, |buffer, _| buffer.snapshot())?;
-        let (lsp_request_limiter, multi_buffer_snapshot) =
-            editor.update(cx, |editor, cx| {
-                let multi_buffer_snapshot =
-                    editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
-                let lsp_request_limiter = Arc::clone(&editor.inlay_hint_cache.lsp_request_limiter);
-                (lsp_request_limiter, multi_buffer_snapshot)
-            })?;
-
-        let (lsp_request_guard, got_throttled) = if query.invalidate.should_invalidate() {
-            (None, false)
-        } else {
-            match lsp_request_limiter.try_acquire() {
-                Some(guard) => (Some(guard), false),
-                None => (Some(lsp_request_limiter.acquire().await), true),
+            InlayHintRefreshReason::NewLinesShown => InvalidationStrategy::None,
+            InlayHintRefreshReason::BufferEdited(_) => InvalidationStrategy::BufferEdited,
+            InlayHintRefreshReason::RefreshRequested(server_id) => {
+                InvalidationStrategy::RefreshRequested(*server_id)
             }
         };
-        let fetch_range_to_log = fetch_range.start.to_point(&buffer_snapshot)
-            ..fetch_range.end.to_point(&buffer_snapshot);
-        let inlay_hints_fetch_task = editor
-            .update(cx, |editor, cx| {
-                if got_throttled {
-                    let query_not_around_visible_range = match editor
-                        .visible_excerpts(None, cx)
-                        .remove(&query.excerpt_id)
-                    {
-                        Some((_, _, current_visible_range)) => {
-                            let visible_offset_length = current_visible_range.len();
-                            let double_visible_range = current_visible_range
-                                .start
-                                .saturating_sub(visible_offset_length)
-                                ..current_visible_range
-                                    .end
-                                    .saturating_add(visible_offset_length)
-                                    .min(buffer_snapshot.len());
-                            !double_visible_range
-                                .contains(&fetch_range.start.to_offset(&buffer_snapshot))
-                                && !double_visible_range
-                                    .contains(&fetch_range.end.to_offset(&buffer_snapshot))
-                        }
-                        None => true,
-                    };
-                    if query_not_around_visible_range {
-                        log::trace!("Fetching inlay hints for range {fetch_range_to_log:?} got throttled and fell off the current visible range, skipping.");
-                        if let Some(task_ranges) = editor
-                            .inlay_hint_cache
-                            .update_tasks
-                            .get_mut(&query.excerpt_id)
-                        {
-                            task_ranges.invalidate_range(&buffer_snapshot, &fetch_range);
-                        }
-                        return None;
-                    }
-                }
 
-                let buffer = editor.buffer().read(cx).buffer(query.buffer_id)?;
+        match &mut self.inlay_hints {
+            Some(inlay_hints) => {
+                if !inlay_hints.enabled
+                    && !matches!(reason, InlayHintRefreshReason::ModifiersChanged(_))
+                {
+                    return None;
+                }
+            }
+            None => return None,
+        }
 
-                if !editor.registered_buffers.contains_key(&query.buffer_id)
-                    && let Some(project) = editor.project.as_ref() {
-                        project.update(cx, |project, cx| {
-                            editor.registered_buffers.insert(
-                                query.buffer_id,
-                                project.register_buffer_with_language_servers(&buffer, cx),
-                            );
-                        })
-                    }
+        Some(invalidate_cache)
+    }
 
-                editor
-                    .semantics_provider
-                    .as_ref()?
-                    .inlay_hints(buffer, fetch_range.clone(), cx)
-            })
-            .ok()
-            .flatten();
+    pub(crate) fn visible_inlay_hints(&self, cx: &Context<Editor>) -> Vec<Inlay> {
+        self.display_map
+            .read(cx)
+            .current_inlays()
+            .filter(move |inlay| matches!(inlay.id, InlayId::Hint(_)))
+            .cloned()
+            .collect()
+    }
 
-        let cached_excerpt_hints = editor.read_with(cx, |editor, _| {
-            editor
-                .inlay_hint_cache
-                .hints
-                .get(&query.excerpt_id)
-                .cloned()
-        })?;
-
-        let visible_hints = editor.update(cx, |editor, cx| editor.visible_inlay_hints(cx).cloned().collect::<Vec<_>>())?;
-        let new_hints = match inlay_hints_fetch_task {
-            Some(fetch_task) => {
-                log::debug!(
-                    "Fetching inlay hints for range {fetch_range_to_log:?}, reason: {query_reason}, invalidate: {invalidate}",
-                    query_reason = query.reason,
-                );
-                log::trace!(
-                    "Currently visible hints: {visible_hints:?}, cached hints present: {}",
-                    cached_excerpt_hints.is_some(),
-                );
-                fetch_task.await.context("inlay hint fetch task")?
-            }
-            None => return Ok(()),
+    pub fn update_inlay_link_and_hover_points(
+        &mut self,
+        snapshot: &EditorSnapshot,
+        point_for_position: PointForPosition,
+        secondary_held: bool,
+        shift_held: bool,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        let Some(lsp_store) = self.project().map(|project| project.read(cx).lsp_store()) else {
+            return;
         };
-        drop(lsp_request_guard);
-        log::debug!(
-            "Fetched {} hints for range {fetch_range_to_log:?}",
-            new_hints.len()
-        );
-        log::trace!("Fetched hints: {new_hints:?}");
-
-        let background_task_buffer_snapshot = buffer_snapshot.clone();
-        let background_fetch_range = fetch_range.clone();
-        let new_update = cx.background_spawn(async move {
-            calculate_hint_updates(
-                query.excerpt_id,
-                invalidate,
-                background_fetch_range,
-                new_hints,
-                &background_task_buffer_snapshot,
-                cached_excerpt_hints,
-                &visible_hints,
+        let hovered_offset = if point_for_position.column_overshoot_after_line_end == 0 {
+            Some(
+                snapshot
+                    .display_point_to_inlay_offset(point_for_position.exact_unclipped, Bias::Left),
             )
-        })
-            .await;
-        if let Some(new_update) = new_update {
-            log::debug!(
-                "Applying update for range {fetch_range_to_log:?}: remove from editor: {}, remove from cache: {}, add to cache: {}",
-                new_update.remove_from_visible.len(),
-                new_update.remove_from_cache.len(),
-                new_update.add_to_cache.len()
+        } else {
+            None
+        };
+        let mut go_to_definition_updated = false;
+        let mut hover_updated = false;
+        if let Some(hovered_offset) = hovered_offset {
+            let buffer_snapshot = self.buffer().read(cx).snapshot(cx);
+            let previous_valid_anchor = buffer_snapshot.anchor_at(
+                point_for_position.previous_valid.to_point(snapshot),
+                Bias::Left,
             );
-            log::trace!("New update: {new_update:?}");
-            editor
-                .update(cx, |editor,  cx| {
-                    apply_hint_update(
-                        editor,
-                        new_update,
-                        query,
-                        invalidate,
-                        buffer_snapshot,
-                        multi_buffer_snapshot,
-                        cx,
-                    );
+            let next_valid_anchor = buffer_snapshot.anchor_at(
+                point_for_position.next_valid.to_point(snapshot),
+                Bias::Right,
+            );
+            if let Some(hovered_hint) = self
+                .visible_inlay_hints(cx)
+                .into_iter()
+                .skip_while(|hint| {
+                    hint.position
+                        .cmp(&previous_valid_anchor, &buffer_snapshot)
+                        .is_lt()
                 })
-                .ok();
-        }
-        anyhow::Ok(())
-    })
-}
-
-fn calculate_hint_updates(
-    excerpt_id: ExcerptId,
-    invalidate: bool,
-    fetch_range: Range<language::Anchor>,
-    new_excerpt_hints: Vec<InlayHint>,
-    buffer_snapshot: &BufferSnapshot,
-    cached_excerpt_hints: Option<Arc<RwLock<CachedExcerptHints>>>,
-    visible_hints: &[Inlay],
-) -> Option<ExcerptHintsUpdate> {
-    let mut add_to_cache = Vec::<InlayHint>::new();
-    let mut excerpt_hints_to_persist = HashMap::default();
-    for new_hint in new_excerpt_hints {
-        if !contains_position(&fetch_range, new_hint.position, buffer_snapshot) {
-            continue;
-        }
-        let missing_from_cache = match &cached_excerpt_hints {
-            Some(cached_excerpt_hints) => {
-                let cached_excerpt_hints = cached_excerpt_hints.read();
-                match cached_excerpt_hints
-                    .ordered_hints
-                    .binary_search_by(|probe| {
-                        cached_excerpt_hints.hints_by_id[probe]
-                            .position
-                            .cmp(&new_hint.position, buffer_snapshot)
-                    }) {
-                    Ok(ix) => {
-                        let mut missing_from_cache = true;
-                        for id in &cached_excerpt_hints.ordered_hints[ix..] {
-                            let cached_hint = &cached_excerpt_hints.hints_by_id[id];
-                            if new_hint
-                                .position
-                                .cmp(&cached_hint.position, buffer_snapshot)
-                                .is_gt()
-                            {
-                                break;
+                .take_while(|hint| {
+                    hint.position
+                        .cmp(&next_valid_anchor, &buffer_snapshot)
+                        .is_le()
+                })
+                .max_by_key(|hint| hint.id)
+            {
+                if let Some(ResolvedHint::Resolved(cached_hint)) =
+                    hovered_hint.position.buffer_id.and_then(|buffer_id| {
+                        lsp_store.update(cx, |lsp_store, cx| {
+                            lsp_store.resolved_hint(buffer_id, hovered_hint.id, cx)
+                        })
+                    })
+                {
+                    match cached_hint.resolve_state {
+                        ResolveState::Resolved => {
+                            let mut extra_shift_left = 0;
+                            let mut extra_shift_right = 0;
+                            if cached_hint.padding_left {
+                                extra_shift_left += 1;
+                                extra_shift_right += 1;
                             }
-                            if cached_hint == &new_hint {
-                                excerpt_hints_to_persist.insert(*id, cached_hint.kind);
-                                missing_from_cache = false;
+                            if cached_hint.padding_right {
+                                extra_shift_right += 1;
                             }
+                            match cached_hint.label {
+                                InlayHintLabel::String(_) => {
+                                    if let Some(tooltip) = cached_hint.tooltip {
+                                        hover_popover::hover_at_inlay(
+                                            self,
+                                            InlayHover {
+                                                tooltip: match tooltip {
+                                                    InlayHintTooltip::String(text) => HoverBlock {
+                                                        text,
+                                                        kind: HoverBlockKind::PlainText,
+                                                    },
+                                                    InlayHintTooltip::MarkupContent(content) => {
+                                                        HoverBlock {
+                                                            text: content.value,
+                                                            kind: content.kind,
+                                                        }
+                                                    }
+                                                },
+                                                range: InlayHighlight {
+                                                    inlay: hovered_hint.id,
+                                                    inlay_position: hovered_hint.position,
+                                                    range: extra_shift_left
+                                                        ..hovered_hint.text().len()
+                                                            + extra_shift_right,
+                                                },
+                                            },
+                                            window,
+                                            cx,
+                                        );
+                                        hover_updated = true;
+                                    }
+                                }
+                                InlayHintLabel::LabelParts(label_parts) => {
+                                    let hint_start =
+                                        snapshot.anchor_to_inlay_offset(hovered_hint.position);
+                                    if let Some((hovered_hint_part, part_range)) =
+                                        hover_popover::find_hovered_hint_part(
+                                            label_parts,
+                                            hint_start,
+                                            hovered_offset,
+                                        )
+                                    {
+                                        let highlight_start =
+                                            (part_range.start - hint_start).0 + extra_shift_left;
+                                        let highlight_end =
+                                            (part_range.end - hint_start).0 + extra_shift_right;
+                                        let highlight = InlayHighlight {
+                                            inlay: hovered_hint.id,
+                                            inlay_position: hovered_hint.position,
+                                            range: highlight_start..highlight_end,
+                                        };
+                                        if let Some(tooltip) = hovered_hint_part.tooltip {
+                                            hover_popover::hover_at_inlay(
+                                                self,
+                                                InlayHover {
+                                                    tooltip: match tooltip {
+                                                        InlayHintLabelPartTooltip::String(text) => {
+                                                            HoverBlock {
+                                                                text,
+                                                                kind: HoverBlockKind::PlainText,
+                                                            }
+                                                        }
+                                                        InlayHintLabelPartTooltip::MarkupContent(
+                                                            content,
+                                                        ) => HoverBlock {
+                                                            text: content.value,
+                                                            kind: content.kind,
+                                                        },
+                                                    },
+                                                    range: highlight.clone(),
+                                                },
+                                                window,
+                                                cx,
+                                            );
+                                            hover_updated = true;
+                                        }
+                                        if let Some((language_server_id, location)) =
+                                            hovered_hint_part.location
+                                            && secondary_held
+                                            && !self.has_pending_nonempty_selection()
+                                        {
+                                            go_to_definition_updated = true;
+                                            show_link_definition(
+                                                shift_held,
+                                                self,
+                                                TriggerPoint::InlayHint(
+                                                    highlight,
+                                                    location,
+                                                    language_server_id,
+                                                ),
+                                                snapshot,
+                                                window,
+                                                cx,
+                                            );
+                                        }
+                                    }
+                                }
+                            };
                         }
-                        missing_from_cache
+                        ResolveState::CanResolve(_, _) => debug_panic!(
+                            "Expected resolved_hint retrieval to return a resolved hint"
+                        ),
+                        ResolveState::Resolving => {}
                     }
-                    Err(_) => true,
                 }
             }
-            None => true,
-        };
-        if missing_from_cache {
-            add_to_cache.push(new_hint);
+        }
+
+        if !go_to_definition_updated {
+            self.hide_hovered_link(cx)
+        }
+        if !hover_updated {
+            hover_popover::hover_at(self, None, window, cx);
         }
     }
 
-    let mut remove_from_visible = HashSet::default();
-    let mut remove_from_cache = HashSet::default();
-    if invalidate {
-        remove_from_visible.extend(
-            visible_hints
-                .iter()
-                .filter(|hint| hint.position.excerpt_id == excerpt_id)
-                .map(|inlay_hint| inlay_hint.id)
-                .filter(|hint_id| !excerpt_hints_to_persist.contains_key(hint_id)),
-        );
+    fn inlay_hints_for_buffer(
+        &mut self,
+        invalidate_cache: InvalidationStrategy,
+        ignore_previous_fetches: bool,
+        buffer_excerpts: VisibleExcerpts,
+        cx: &mut Context<Self>,
+    ) -> Option<Vec<Task<(Range<BufferRow>, anyhow::Result<CacheInlayHints>)>>> {
+        let semantics_provider = self.semantics_provider()?;
+        let inlay_hints = self.inlay_hints.as_mut()?;
+        let buffer_id = buffer_excerpts.buffer.read(cx).remote_id();
+
+        let new_hint_tasks = semantics_provider
+            .inlay_hints(
+                invalidate_cache,
+                buffer_excerpts.buffer,
+                buffer_excerpts.ranges,
+                inlay_hints
+                    .hint_chunk_fetched
+                    .get(&buffer_id)
+                    .filter(|_| !ignore_previous_fetches && !invalidate_cache.should_invalidate())
+                    .cloned(),
+                cx,
+            )
+            .unwrap_or_default();
+
+        let (known_version, known_chunks) =
+            inlay_hints.hint_chunk_fetched.entry(buffer_id).or_default();
+        if buffer_excerpts.buffer_version.changed_since(known_version) {
+            known_chunks.clear();
+            *known_version = buffer_excerpts.buffer_version;
+        }
+
+        let mut hint_tasks = Vec::new();
+        for (row_range, new_hints_task) in new_hint_tasks {
+            let inserted = known_chunks.insert(row_range.clone());
+            if inserted || ignore_previous_fetches || invalidate_cache.should_invalidate() {
+                hint_tasks.push(cx.spawn(async move |_, _| (row_range, new_hints_task.await)));
+            }
+        }
+
+        Some(hint_tasks)
+    }
+
+    fn apply_fetched_hints(
+        &mut self,
+        buffer_id: BufferId,
+        query_version: Global,
+        invalidate_cache: InvalidationStrategy,
+        new_hints: Vec<(Range<BufferRow>, anyhow::Result<CacheInlayHints>)>,
+        all_affected_buffers: Arc<Mutex<HashSet<BufferId>>>,
+        cx: &mut Context<Self>,
+    ) {
+        let visible_inlay_hint_ids = self
+            .visible_inlay_hints(cx)
+            .iter()
+            .filter(|inlay| inlay.position.buffer_id == Some(buffer_id))
+            .map(|inlay| inlay.id)
+            .collect::<Vec<_>>();
+        let Some(inlay_hints) = &mut self.inlay_hints else {
+            return;
+        };
+
+        let mut hints_to_remove = Vec::new();
+        let multi_buffer_snapshot = self.buffer.read(cx).snapshot(cx);
+
+        // If we've received hints from the cache, it means `invalidate_cache` had invalidated whatever possible there,
+        // and most probably there are no more hints with IDs from `visible_inlay_hint_ids` in the cache.
+        // So, if we hover such hints, no resolve will happen.
+        //
+        // Another issue is in the fact that changing one buffer may lead to other buffers' hints changing, so more cache entries may be removed.
+        // Hence, clear all excerpts' hints in the multi buffer: later, the invalidated ones will re-trigger the LSP query, the rest will be restored
+        // from the cache.
+        if invalidate_cache.should_invalidate() {
+            hints_to_remove.extend(visible_inlay_hint_ids);
+        }
 
-        if let Some(cached_excerpt_hints) = &cached_excerpt_hints {
-            let cached_excerpt_hints = cached_excerpt_hints.read();
-            remove_from_cache.extend(
-                cached_excerpt_hints
-                    .ordered_hints
+        let excerpts = self.buffer.read(cx).excerpt_ids();
+        let hints_to_insert = new_hints
+            .into_iter()
+            .filter_map(|(chunk_range, hints_result)| match hints_result {
+                Ok(new_hints) => Some(new_hints),
+                Err(e) => {
+                    log::error!(
+                        "Failed to query inlays for buffer row range {chunk_range:?}, {e:#}"
+                    );
+                    if let Some((for_version, chunks_fetched)) =
+                        inlay_hints.hint_chunk_fetched.get_mut(&buffer_id)
+                    {
+                        if for_version == &query_version {
+                            chunks_fetched.remove(&chunk_range);
+                        }
+                    }
+                    None
+                }
+            })
+            .flat_map(|hints| hints.into_values())
+            .flatten()
+            .filter_map(|(hint_id, lsp_hint)| {
+                if inlay_hints.allowed_hint_kinds.contains(&lsp_hint.kind)
+                    && inlay_hints
+                        .added_hints
+                        .insert(hint_id, lsp_hint.kind)
+                        .is_none()
+                {
+                    let position = excerpts.iter().find_map(|excerpt_id| {
+                        multi_buffer_snapshot.anchor_in_excerpt(*excerpt_id, lsp_hint.position)
+                    })?;
+                    return Some(Inlay::hint(hint_id, position, &lsp_hint));
+                }
+                None
+            })
+            .collect::<Vec<_>>();
+
+        // We need to invalidate excerpts all buffers with the same language, do that once only, after first new data chunk is inserted.
+        let all_other_affected_buffers = all_affected_buffers
+            .lock()
+            .drain()
+            .filter(|id| buffer_id != *id)
+            .collect::<HashSet<_>>();
+        if !all_other_affected_buffers.is_empty() {
+            hints_to_remove.extend(
+                self.visible_inlay_hints(cx)
                     .iter()
-                    .filter(|cached_inlay_id| {
-                        !excerpt_hints_to_persist.contains_key(cached_inlay_id)
+                    .filter(|inlay| {
+                        inlay
+                            .position
+                            .buffer_id
+                            .is_none_or(|buffer_id| all_other_affected_buffers.contains(&buffer_id))
                     })
-                    .copied(),
+                    .map(|inlay| inlay.id),
             );
-            remove_from_visible.extend(remove_from_cache.iter().cloned());
         }
-    }
 
-    if remove_from_visible.is_empty() && remove_from_cache.is_empty() && add_to_cache.is_empty() {
-        None
-    } else {
-        Some(ExcerptHintsUpdate {
-            excerpt_id,
-            remove_from_visible,
-            remove_from_cache,
-            add_to_cache,
-        })
+        self.splice_inlays(&hints_to_remove, hints_to_insert, cx);
     }
 }
 
-fn contains_position(
-    range: &Range<language::Anchor>,
-    position: language::Anchor,
-    buffer_snapshot: &BufferSnapshot,
-) -> bool {
-    range.start.cmp(&position, buffer_snapshot).is_le()
-        && range.end.cmp(&position, buffer_snapshot).is_ge()
+#[derive(Debug)]
+struct VisibleExcerpts {
+    excerpts: Vec<ExcerptId>,
+    ranges: Vec<Range<text::Anchor>>,
+    buffer_version: Global,
+    buffer: Entity<language::Buffer>,
 }
 
-fn apply_hint_update(
-    editor: &mut Editor,
-    new_update: ExcerptHintsUpdate,
-    query: ExcerptQuery,
-    invalidate: bool,
-    buffer_snapshot: BufferSnapshot,
-    multi_buffer_snapshot: MultiBufferSnapshot,
-    cx: &mut Context<Editor>,
-) {
-    let cached_excerpt_hints = editor
-        .inlay_hint_cache
-        .hints
-        .entry(new_update.excerpt_id)
-        .or_insert_with(|| {
-            Arc::new(RwLock::new(CachedExcerptHints {
-                version: query.cache_version,
-                buffer_version: buffer_snapshot.version().clone(),
-                buffer_id: query.buffer_id,
-                ordered_hints: Vec::new(),
-                hints_by_id: HashMap::default(),
-            }))
-        });
-    let mut cached_excerpt_hints = cached_excerpt_hints.write();
-    match query.cache_version.cmp(&cached_excerpt_hints.version) {
-        cmp::Ordering::Less => return,
-        cmp::Ordering::Greater | cmp::Ordering::Equal => {
-            cached_excerpt_hints.version = query.cache_version;
+fn spawn_editor_hints_refresh(
+    buffer_id: BufferId,
+    invalidate_cache: InvalidationStrategy,
+    ignore_previous_fetches: bool,
+    debounce: Option<Duration>,
+    buffer_excerpts: VisibleExcerpts,
+    all_affected_buffers: Arc<Mutex<HashSet<BufferId>>>,
+    cx: &mut Context<'_, Editor>,
+) -> Task<()> {
+    cx.spawn(async move |editor, cx| {
+        if let Some(debounce) = debounce {
+            cx.background_executor().timer(debounce).await;
         }
-    }
 
-    let mut cached_inlays_changed = !new_update.remove_from_cache.is_empty();
-    cached_excerpt_hints
-        .ordered_hints
-        .retain(|hint_id| !new_update.remove_from_cache.contains(hint_id));
-    cached_excerpt_hints
-        .hints_by_id
-        .retain(|hint_id, _| !new_update.remove_from_cache.contains(hint_id));
-    let mut splice = InlaySplice::default();
-    splice.to_remove.extend(new_update.remove_from_visible);
-    for new_hint in new_update.add_to_cache {
-        let insert_position = match cached_excerpt_hints
-            .ordered_hints
-            .binary_search_by(|probe| {
-                cached_excerpt_hints.hints_by_id[probe]
-                    .position
-                    .cmp(&new_hint.position, &buffer_snapshot)
-            }) {
-            Ok(i) => {
-                // When a hint is added to the same position where existing ones are present,
-                // do not deduplicate it: we split hint queries into non-overlapping ranges
-                // and each hint batch returned by the server should already contain unique hints.
-                i + cached_excerpt_hints.ordered_hints[i..].len() + 1
-            }
-            Err(i) => i,
+        let query_version = buffer_excerpts.buffer_version.clone();
+        let Some(hint_tasks) = editor
+            .update(cx, |editor, cx| {
+                editor.inlay_hints_for_buffer(
+                    invalidate_cache,
+                    ignore_previous_fetches,
+                    buffer_excerpts,
+                    cx,
+                )
+            })
+            .ok()
+        else {
+            return;
         };
-
-        let new_inlay_id = post_inc(&mut editor.next_inlay_id);
-        if editor
-            .inlay_hint_cache
-            .allowed_hint_kinds
-            .contains(&new_hint.kind)
-            && let Some(new_hint_position) =
-                multi_buffer_snapshot.anchor_in_excerpt(query.excerpt_id, new_hint.position)
-        {
-            splice
-                .to_insert
-                .push(Inlay::hint(new_inlay_id, new_hint_position, &new_hint));
-        }
-        let new_id = InlayId::Hint(new_inlay_id);
-        cached_excerpt_hints.hints_by_id.insert(new_id, new_hint);
-        if cached_excerpt_hints.ordered_hints.len() <= insert_position {
-            cached_excerpt_hints.ordered_hints.push(new_id);
-        } else {
-            cached_excerpt_hints
-                .ordered_hints
-                .insert(insert_position, new_id);
-        }
-
-        cached_inlays_changed = true;
-    }
-    cached_excerpt_hints.buffer_version = buffer_snapshot.version().clone();
-    drop(cached_excerpt_hints);
-
-    if invalidate {
-        let mut outdated_excerpt_caches = HashSet::default();
-        for (excerpt_id, excerpt_hints) in &editor.inlay_hint_cache().hints {
-            let excerpt_hints = excerpt_hints.read();
-            if excerpt_hints.buffer_id == query.buffer_id
-                && excerpt_id != &query.excerpt_id
-                && buffer_snapshot
-                    .version()
-                    .changed_since(&excerpt_hints.buffer_version)
-            {
-                outdated_excerpt_caches.insert(*excerpt_id);
-                splice
-                    .to_remove
-                    .extend(excerpt_hints.ordered_hints.iter().copied());
-            }
+        let hint_tasks = hint_tasks.unwrap_or_default();
+        if hint_tasks.is_empty() {
+            return;
         }
-        cached_inlays_changed |= !outdated_excerpt_caches.is_empty();
+        let new_hints = join_all(hint_tasks).await;
         editor
-            .inlay_hint_cache
-            .hints
-            .retain(|excerpt_id, _| !outdated_excerpt_caches.contains(excerpt_id));
-    }
-
-    let InlaySplice {
-        to_remove,
-        to_insert,
-    } = splice;
-    let displayed_inlays_changed = !to_remove.is_empty() || !to_insert.is_empty();
-    if cached_inlays_changed || displayed_inlays_changed {
-        editor.inlay_hint_cache.version += 1;
-    }
-    if displayed_inlays_changed {
-        editor.splice_inlays(&to_remove, to_insert, cx)
-    }
+            .update(cx, |editor, cx| {
+                editor.apply_fetched_hints(
+                    buffer_id,
+                    query_version,
+                    invalidate_cache,
+                    new_hints,
+                    all_affected_buffers,
+                    cx,
+                );
+            })
+            .ok();
+    })
 }
 
 #[cfg(test)]
 pub mod tests {
-    use crate::SelectionEffects;
     use crate::editor_tests::update_test_language_settings;
+    use crate::inlays::inlay_hints::InlayHintRefreshReason;
     use crate::scroll::ScrollAmount;
-    use crate::{ExcerptRange, scroll::Autoscroll, test::editor_lsp_test_context::rust_lang};
-    use futures::StreamExt;
+    use crate::{Editor, SelectionEffects};
+    use crate::{ExcerptRange, scroll::Autoscroll};
+    use collections::HashSet;
+    use futures::{StreamExt, future};
     use gpui::{AppContext as _, Context, SemanticVersion, TestAppContext, WindowHandle};
     use itertools::Itertools as _;
+    use language::language_settings::InlayHintKind;
     use language::{Capability, FakeLspAdapter};
     use language::{Language, LanguageConfig, LanguageMatcher};
+    use languages::rust_lang;
     use lsp::FakeLanguageServer;
+    use multi_buffer::MultiBuffer;
     use parking_lot::Mutex;
+    use pretty_assertions::assert_eq;
     use project::{FakeFs, Project};
     use serde_json::json;
     use settings::{AllLanguageSettingsContent, InlayHintSettingsContent, SettingsStore};
+    use std::ops::Range;
+    use std::sync::Arc;
     use std::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering};
-    use text::Point;
+    use std::time::Duration;
+    use text::{OffsetRangeExt, Point};
+    use ui::App;
     use util::path;
-
-    use super::*;
+    use util::paths::natural_sort;
 
     #[gpui::test]
     async fn test_basic_cache_update_with_duplicate_hints(cx: &mut gpui::TestAppContext) {

crates/editor/src/items.rs 🔗

@@ -42,7 +42,7 @@ use ui::{IconDecorationKind, prelude::*};
 use util::{ResultExt, TryFutureExt, paths::PathExt};
 use workspace::{
     CollaboratorId, ItemId, ItemNavHistory, ToolbarItemLocation, ViewId, Workspace, WorkspaceId,
-    invalid_buffer_view::InvalidBufferView,
+    invalid_item_view::InvalidItemView,
     item::{FollowableItem, Item, ItemBufferKind, ItemEvent, ProjectItem, SaveOptions},
     searchable::{
         Direction, FilteredSearchRange, SearchEvent, SearchableItem, SearchableItemHandle,
@@ -226,7 +226,7 @@ impl FollowableItem for Editor {
 
         Some(proto::view::Variant::Editor(proto::view::Editor {
             singleton: buffer.is_singleton(),
-            title: (!buffer.is_singleton()).then(|| buffer.title(cx).into()),
+            title: buffer.explicit_title().map(ToOwned::to_owned),
             excerpts,
             scroll_top_anchor: Some(serialize_anchor(&scroll_anchor.anchor, &snapshot)),
             scroll_x: scroll_anchor.offset.x,
@@ -594,7 +594,7 @@ impl Item for Editor {
         cx: &mut Context<Self>,
     ) -> bool {
         if let Ok(data) = data.downcast::<NavigationData>() {
-            let newest_selection = self.selections.newest::<Point>(cx);
+            let newest_selection = self.selections.newest::<Point>(&self.display_snapshot(cx));
             let buffer = self.buffer.read(cx).read(cx);
             let offset = if buffer.can_resolve(&data.cursor_anchor) {
                 data.cursor_anchor.to_point(&buffer)
@@ -762,11 +762,11 @@ impl Item for Editor {
         _workspace_id: Option<WorkspaceId>,
         window: &mut Window,
         cx: &mut Context<Self>,
-    ) -> Option<Entity<Editor>>
+    ) -> Task<Option<Entity<Editor>>>
     where
         Self: Sized,
     {
-        Some(cx.new(|cx| self.clone(window, cx)))
+        Task::ready(Some(cx.new(|cx| self.clone(window, cx))))
     }
 
     fn set_nav_history(
@@ -938,8 +938,9 @@ impl Item for Editor {
     fn breadcrumbs(&self, variant: &Theme, cx: &App) -> Option<Vec<BreadcrumbText>> {
         let cursor = self.selections.newest_anchor().head();
         let multibuffer = &self.buffer().read(cx);
-        let (buffer_id, symbols) =
-            multibuffer.symbols_containing(cursor, Some(variant.syntax()), cx)?;
+        let (buffer_id, symbols) = multibuffer
+            .read(cx)
+            .symbols_containing(cursor, Some(variant.syntax()))?;
         let buffer = multibuffer.buffer(buffer_id)?;
 
         let buffer = buffer.read(cx);
@@ -1079,12 +1080,17 @@ impl SerializableItem for Editor {
                 }
             }
             Ok(None) => {
-                return Task::ready(Err(anyhow!("No path or contents found for buffer")));
+                return Task::ready(Err(anyhow!(
+                    "Unable to deserialize editor: No entry in database for item_id: {item_id} and workspace_id {workspace_id:?}"
+                )));
             }
             Err(error) => {
                 return Task::ready(Err(error));
             }
         };
+        log::debug!(
+            "Deserialized editor {item_id:?} in workspace {workspace_id:?}, {serialized_editor:?}"
+        );
 
         match serialized_editor {
             SerializedEditor {
@@ -1112,7 +1118,8 @@ impl SerializableItem for Editor {
                     // First create the empty buffer
                     let buffer = project
                         .update(cx, |project, cx| project.create_buffer(true, cx))?
-                        .await?;
+                        .await
+                        .context("Failed to create buffer while deserializing editor")?;
 
                     // Then set the text so that the dirty bit is set correctly
                     buffer.update(cx, |buffer, cx| {
@@ -1154,7 +1161,9 @@ impl SerializableItem for Editor {
                 match opened_buffer {
                     Some(opened_buffer) => {
                         window.spawn(cx, async move |cx| {
-                            let (_, buffer) = opened_buffer.await?;
+                            let (_, buffer) = opened_buffer
+                                .await
+                                .context("Failed to open path in project")?;
 
                             // This is a bit wasteful: we're loading the whole buffer from
                             // disk and then overwrite the content.
@@ -1220,7 +1229,8 @@ impl SerializableItem for Editor {
             } => window.spawn(cx, async move |cx| {
                 let buffer = project
                     .update(cx, |project, cx| project.create_buffer(true, cx))?
-                    .await?;
+                    .await
+                    .context("Failed to create buffer")?;
 
                 cx.update(|window, cx| {
                     cx.new(|cx| {
@@ -1383,8 +1393,8 @@ impl ProjectItem for Editor {
         e: &anyhow::Error,
         window: &mut Window,
         cx: &mut App,
-    ) -> Option<InvalidBufferView> {
-        Some(InvalidBufferView::new(abs_path, is_local, e, window, cx))
+    ) -> Option<InvalidItemView> {
+        Some(InvalidItemView::new(abs_path, is_local, e, window, cx))
     }
 }
 
@@ -1539,13 +1549,13 @@ impl SearchableItem for Editor {
     fn query_suggestion(&mut self, window: &mut Window, cx: &mut Context<Self>) -> String {
         let setting = EditorSettings::get_global(cx).seed_search_query_from_cursor;
         let snapshot = self.snapshot(window, cx);
-        let snapshot = snapshot.buffer_snapshot();
-        let selection = self.selections.newest_adjusted(cx);
+        let selection = self.selections.newest_adjusted(&snapshot.display_snapshot);
+        let buffer_snapshot = snapshot.buffer_snapshot();
 
         match setting {
             SeedQuerySetting::Never => String::new(),
             SeedQuerySetting::Selection | SeedQuerySetting::Always if !selection.is_empty() => {
-                let text: String = snapshot
+                let text: String = buffer_snapshot
                     .text_for_range(selection.start..selection.end)
                     .collect();
                 if text.contains('\n') {
@@ -1556,10 +1566,10 @@ impl SearchableItem for Editor {
             }
             SeedQuerySetting::Selection => String::new(),
             SeedQuerySetting::Always => {
-                let (range, kind) =
-                    snapshot.surrounding_word(selection.start, Some(CharScopeContext::Completion));
+                let (range, kind) = buffer_snapshot
+                    .surrounding_word(selection.start, Some(CharScopeContext::Completion));
                 if kind == Some(CharKind::Word) {
-                    let text: String = snapshot.text_for_range(range).collect();
+                    let text: String = buffer_snapshot.text_for_range(range).collect();
                     if !text.trim().is_empty() {
                         return text;
                     }

crates/editor/src/linked_editing_ranges.rs 🔗

@@ -59,7 +59,7 @@ pub(super) fn refresh_linked_ranges(
         let mut applicable_selections = Vec::new();
         editor
             .update(cx, |editor, cx| {
-                let selections = editor.selections.all::<usize>(cx);
+                let selections = editor.selections.all::<usize>(&editor.display_snapshot(cx));
                 let snapshot = editor.buffer.read(cx).snapshot(cx);
                 let buffer = editor.buffer.read(cx);
                 for selection in selections {

crates/editor/src/lsp_colors.rs 🔗

@@ -6,15 +6,15 @@ use gpui::{Hsla, Rgba, Task};
 use itertools::Itertools;
 use language::point_from_lsp;
 use multi_buffer::Anchor;
-use project::DocumentColor;
+use project::{DocumentColor, InlayId};
 use settings::Settings as _;
 use text::{Bias, BufferId, OffsetRangeExt as _};
 use ui::{App, Context, Window};
 use util::post_inc;
 
 use crate::{
-    DisplayPoint, Editor, EditorSettings, EditorSnapshot, FETCH_COLORS_DEBOUNCE_TIMEOUT, InlayId,
-    InlaySplice, RangeToAnchorExt, display_map::Inlay, editor_settings::DocumentColorsRenderMode,
+    DisplayPoint, Editor, EditorSettings, EditorSnapshot, FETCH_COLORS_DEBOUNCE_TIMEOUT,
+    InlaySplice, RangeToAnchorExt, editor_settings::DocumentColorsRenderMode, inlays::Inlay,
 };
 
 #[derive(Debug)]
@@ -164,7 +164,7 @@ impl Editor {
         }
 
         let visible_buffers = self
-            .visible_excerpts(None, cx)
+            .visible_excerpts(cx)
             .into_values()
             .map(|(buffer, ..)| buffer)
             .filter(|editor_buffer| {
@@ -400,8 +400,7 @@ impl Editor {
                     }
 
                     if colors.render_mode == DocumentColorsRenderMode::Inlay
-                        && (!colors_splice.to_insert.is_empty()
-                            || !colors_splice.to_remove.is_empty())
+                        && !colors_splice.is_empty()
                     {
                         editor.splice_inlays(&colors_splice.to_remove, colors_splice.to_insert, cx);
                         updated = true;

crates/editor/src/mouse_context_menu.rs 🔗

@@ -11,6 +11,7 @@ use gpui::{Context, DismissEvent, Entity, Focusable as _, Pixels, Point, Subscri
 use std::ops::Range;
 use text::PointUtf16;
 use workspace::OpenInTerminal;
+use zed_actions::agent::AddSelectionToThread;
 
 #[derive(Debug)]
 pub enum MenuPosition {
@@ -154,7 +155,7 @@ pub fn deploy_context_menu(
         return;
     }
 
-    let display_map = editor.selections.display_map(cx);
+    let display_map = editor.display_snapshot(cx);
     let source_anchor = display_map.display_point_to_anchor(point, text::Bias::Right);
     let context_menu = if let Some(custom) = editor.custom_context_menu.take() {
         let menu = custom(editor, point, window, cx);
@@ -169,8 +170,8 @@ pub fn deploy_context_menu(
             return;
         };
 
-        let display_map = editor.selections.display_map(cx);
         let snapshot = editor.snapshot(window, cx);
+        let display_map = editor.display_snapshot(cx);
         let buffer = snapshot.buffer_snapshot();
         let anchor = buffer.anchor_before(point.to_point(&display_map));
         if !display_ranges(&display_map, &editor.selections).any(|r| r.contains(&point)) {
@@ -185,7 +186,7 @@ pub fn deploy_context_menu(
         let has_reveal_target = editor.target_file(cx).is_some();
         let has_selections = editor
             .selections
-            .all::<PointUtf16>(cx)
+            .all::<PointUtf16>(&display_map)
             .into_iter()
             .any(|s| !s.is_empty());
         let has_git_repo = buffer
@@ -233,6 +234,7 @@ pub fn deploy_context_menu(
                         quick_launch: false,
                     }),
                 )
+                .action("Add to Agent Thread", Box::new(AddSelectionToThread))
                 .separator()
                 .action("Cut", Box::new(Cut))
                 .action("Copy", Box::new(Copy))

crates/editor/src/movement.rs 🔗

@@ -872,7 +872,7 @@ mod tests {
     use super::*;
     use crate::{
         Buffer, DisplayMap, DisplayRow, ExcerptRange, FoldPlaceholder, MultiBuffer,
-        display_map::Inlay,
+        inlays::Inlay,
         test::{editor_test_context::EditorTestContext, marked_display_snapshot},
     };
     use gpui::{AppContext as _, font, px};

crates/editor/src/proposed_changes_editor.rs 🔗

@@ -1,14 +1,14 @@
 use crate::{ApplyAllDiffHunks, Editor, EditorEvent, SelectionEffects, SemanticsProvider};
 use buffer_diff::BufferDiff;
-use collections::HashSet;
+use collections::{HashMap, HashSet};
 use futures::{channel::mpsc, future::join_all};
 use gpui::{App, Entity, EventEmitter, Focusable, Render, Subscription, Task};
-use language::{Buffer, BufferEvent, Capability};
+use language::{Buffer, BufferEvent, BufferRow, Capability};
 use multi_buffer::{ExcerptRange, MultiBuffer};
-use project::Project;
+use project::{InvalidationStrategy, Project, lsp_store::CacheInlayHints};
 use smol::stream::StreamExt;
 use std::{any::TypeId, ops::Range, rc::Rc, time::Duration};
-use text::ToOffset;
+use text::{BufferId, ToOffset};
 use ui::{ButtonLike, KeyBinding, prelude::*};
 use workspace::{
     Item, ItemHandle as _, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
@@ -370,17 +370,15 @@ impl ProposedChangesEditorToolbar {
 }
 
 impl Render for ProposedChangesEditorToolbar {
-    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
         let button_like = ButtonLike::new("apply-changes").child(Label::new("Apply All"));
 
         match &self.current_editor {
             Some(editor) => {
                 let focus_handle = editor.focus_handle(cx);
-                let keybinding =
-                    KeyBinding::for_action_in(&ApplyAllDiffHunks, &focus_handle, window, cx)
-                        .map(|binding| binding.into_any_element());
+                let keybinding = KeyBinding::for_action_in(&ApplyAllDiffHunks, &focus_handle, cx);
 
-                button_like.children(keybinding).on_click({
+                button_like.child(keybinding).on_click({
                     move |_event, window, cx| {
                         focus_handle.dispatch_action(&ApplyAllDiffHunks, window, cx)
                     }
@@ -436,14 +434,34 @@ impl SemanticsProvider for BranchBufferSemanticsProvider {
         self.0.hover(&buffer, position, cx)
     }
 
+    fn applicable_inlay_chunks(
+        &self,
+        buffer: &Entity<Buffer>,
+        ranges: &[Range<text::Anchor>],
+        cx: &mut App,
+    ) -> Vec<Range<BufferRow>> {
+        self.0.applicable_inlay_chunks(buffer, ranges, cx)
+    }
+
+    fn invalidate_inlay_hints(&self, for_buffers: &HashSet<BufferId>, cx: &mut App) {
+        self.0.invalidate_inlay_hints(for_buffers, cx);
+    }
+
     fn inlay_hints(
         &self,
+        invalidate: InvalidationStrategy,
         buffer: Entity<Buffer>,
-        range: Range<text::Anchor>,
+        ranges: Vec<Range<text::Anchor>>,
+        known_chunks: Option<(clock::Global, HashSet<Range<BufferRow>>)>,
         cx: &mut App,
-    ) -> Option<Task<anyhow::Result<Vec<project::InlayHint>>>> {
-        let buffer = self.to_base(&buffer, &[range.start, range.end], cx)?;
-        self.0.inlay_hints(buffer, range, cx)
+    ) -> Option<HashMap<Range<BufferRow>, Task<anyhow::Result<CacheInlayHints>>>> {
+        let positions = ranges
+            .iter()
+            .flat_map(|range| [range.start, range.end])
+            .collect::<Vec<_>>();
+        let buffer = self.to_base(&buffer, &positions, cx)?;
+        self.0
+            .inlay_hints(invalidate, buffer, ranges, known_chunks, cx)
     }
 
     fn inline_values(
@@ -455,17 +473,6 @@ impl SemanticsProvider for BranchBufferSemanticsProvider {
         None
     }
 
-    fn resolve_inlay_hint(
-        &self,
-        hint: project::InlayHint,
-        buffer: Entity<Buffer>,
-        server_id: lsp::LanguageServerId,
-        cx: &mut App,
-    ) -> Option<Task<anyhow::Result<project::InlayHint>>> {
-        let buffer = self.to_base(&buffer, &[], cx)?;
-        self.0.resolve_inlay_hint(hint, buffer, server_id, cx)
-    }
-
     fn supports_inlay_hints(&self, buffer: &Entity<Buffer>, cx: &mut App) -> bool {
         if let Some(buffer) = self.to_base(buffer, &[], cx) {
             self.0.supports_inlay_hints(&buffer, cx)

crates/editor/src/scroll/actions.rs 🔗

@@ -72,7 +72,12 @@ impl Editor {
         cx: &mut Context<Editor>,
     ) {
         let scroll_margin_rows = self.vertical_scroll_margin() as u32;
-        let new_screen_top = self.selections.newest_display(cx).head().row().0;
+        let new_screen_top = self
+            .selections
+            .newest_display(&self.display_snapshot(cx))
+            .head()
+            .row()
+            .0;
         let new_screen_top = new_screen_top.saturating_sub(scroll_margin_rows);
         self.set_scroll_top_row(DisplayRow(new_screen_top), window, cx);
     }
@@ -86,7 +91,12 @@ impl Editor {
         let Some(visible_rows) = self.visible_line_count().map(|count| count as u32) else {
             return;
         };
-        let new_screen_top = self.selections.newest_display(cx).head().row().0;
+        let new_screen_top = self
+            .selections
+            .newest_display(&self.display_snapshot(cx))
+            .head()
+            .row()
+            .0;
         let new_screen_top = new_screen_top.saturating_sub(visible_rows / 2);
         self.set_scroll_top_row(DisplayRow(new_screen_top), window, cx);
     }
@@ -101,7 +111,12 @@ impl Editor {
         let Some(visible_rows) = self.visible_line_count().map(|count| count as u32) else {
             return;
         };
-        let new_screen_top = self.selections.newest_display(cx).head().row().0;
+        let new_screen_top = self
+            .selections
+            .newest_display(&self.display_snapshot(cx))
+            .head()
+            .row()
+            .0;
         let new_screen_top =
             new_screen_top.saturating_sub(visible_rows.saturating_sub(scroll_margin_rows));
         self.set_scroll_top_row(DisplayRow(new_screen_top), window, cx);

crates/editor/src/scroll/autoscroll.rs 🔗

@@ -148,7 +148,7 @@ impl Editor {
             target_top = first_highlighted_row.as_f64();
             target_bottom = target_top + 1.;
         } else {
-            let selections = self.selections.all::<Point>(cx);
+            let selections = self.selections.all::<Point>(&display_map);
 
             target_top = selections
                 .first()
@@ -293,7 +293,7 @@ impl Editor {
         let scroll_width = ScrollOffset::from(scroll_width);
 
         let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
-        let selections = self.selections.all::<Point>(cx);
+        let selections = self.selections.all::<Point>(&display_map);
         let mut scroll_position = self.scroll_manager.scroll_position(&display_map);
 
         let mut target_left;

crates/editor/src/selections_collection.rs 🔗

@@ -110,7 +110,7 @@ impl SelectionsCollection {
         if self.pending.is_none() {
             self.disjoint_anchors_arc()
         } else {
-            let all_offset_selections = self.all::<usize>(cx);
+            let all_offset_selections = self.all::<usize>(&self.display_map(cx));
             let buffer = self.buffer(cx);
             all_offset_selections
                 .into_iter()
@@ -129,25 +129,23 @@ impl SelectionsCollection {
 
     pub fn pending<D: TextDimension + Ord + Sub<D, Output = D>>(
         &self,
-        cx: &mut App,
+        snapshot: &DisplaySnapshot,
     ) -> Option<Selection<D>> {
-        let map = self.display_map(cx);
-
-        resolve_selections(self.pending_anchor(), &map).next()
+        resolve_selections(self.pending_anchor(), &snapshot).next()
     }
 
     pub(crate) fn pending_mode(&self) -> Option<SelectMode> {
         self.pending.as_ref().map(|pending| pending.mode.clone())
     }
 
-    pub fn all<'a, D>(&self, cx: &mut App) -> Vec<Selection<D>>
+    pub fn all<'a, D>(&self, snapshot: &DisplaySnapshot) -> Vec<Selection<D>>
     where
         D: 'a + TextDimension + Ord + Sub<D, Output = D>,
     {
-        let map = self.display_map(cx);
         let disjoint_anchors = &self.disjoint;
-        let mut disjoint = resolve_selections::<D, _>(disjoint_anchors.iter(), &map).peekable();
-        let mut pending_opt = self.pending::<D>(cx);
+        let mut disjoint =
+            resolve_selections::<D, _>(disjoint_anchors.iter(), &snapshot).peekable();
+        let mut pending_opt = self.pending::<D>(&snapshot);
         iter::from_fn(move || {
             if let Some(pending) = pending_opt.as_mut() {
                 while let Some(next_selection) = disjoint.peek() {
@@ -175,12 +173,11 @@ impl SelectionsCollection {
     }
 
     /// Returns all of the selections, adjusted to take into account the selection line_mode
-    pub fn all_adjusted(&self, cx: &mut App) -> Vec<Selection<Point>> {
-        let mut selections = self.all::<Point>(cx);
+    pub fn all_adjusted(&self, snapshot: &DisplaySnapshot) -> Vec<Selection<Point>> {
+        let mut selections = self.all::<Point>(&snapshot);
         if self.line_mode {
-            let map = self.display_map(cx);
             for selection in &mut selections {
-                let new_range = map.expand_to_line(selection.range());
+                let new_range = snapshot.expand_to_line(selection.range());
                 selection.start = new_range.start;
                 selection.end = new_range.end;
             }
@@ -210,11 +207,10 @@ impl SelectionsCollection {
     }
 
     /// Returns the newest selection, adjusted to take into account the selection line_mode
-    pub fn newest_adjusted(&self, cx: &mut App) -> Selection<Point> {
-        let mut selection = self.newest::<Point>(cx);
+    pub fn newest_adjusted(&self, snapshot: &DisplaySnapshot) -> Selection<Point> {
+        let mut selection = self.newest::<Point>(&snapshot);
         if self.line_mode {
-            let map = self.display_map(cx);
-            let new_range = map.expand_to_line(selection.range());
+            let new_range = snapshot.expand_to_line(selection.range());
             selection.start = new_range.start;
             selection.end = new_range.end;
         }
@@ -223,53 +219,55 @@ impl SelectionsCollection {
 
     pub fn all_adjusted_display(
         &self,
-        cx: &mut App,
-    ) -> (DisplaySnapshot, Vec<Selection<DisplayPoint>>) {
+        display_map: &DisplaySnapshot,
+    ) -> Vec<Selection<DisplayPoint>> {
         if self.line_mode {
-            let selections = self.all::<Point>(cx);
-            let map = self.display_map(cx);
+            let selections = self.all::<Point>(&display_map);
             let result = selections
                 .into_iter()
                 .map(|mut selection| {
-                    let new_range = map.expand_to_line(selection.range());
+                    let new_range = display_map.expand_to_line(selection.range());
                     selection.start = new_range.start;
                     selection.end = new_range.end;
-                    selection.map(|point| point.to_display_point(&map))
+                    selection.map(|point| point.to_display_point(&display_map))
                 })
                 .collect();
-            (map, result)
+            result
         } else {
-            self.all_display(cx)
+            self.all_display(display_map)
         }
     }
 
-    pub fn disjoint_in_range<'a, D>(&self, range: Range<Anchor>, cx: &mut App) -> Vec<Selection<D>>
+    pub fn disjoint_in_range<'a, D>(
+        &self,
+        range: Range<Anchor>,
+        snapshot: &DisplaySnapshot,
+    ) -> Vec<Selection<D>>
     where
         D: 'a + TextDimension + Ord + Sub<D, Output = D> + std::fmt::Debug,
     {
-        let map = self.display_map(cx);
         let start_ix = match self
             .disjoint
-            .binary_search_by(|probe| probe.end.cmp(&range.start, map.buffer_snapshot()))
+            .binary_search_by(|probe| probe.end.cmp(&range.start, snapshot.buffer_snapshot()))
         {
             Ok(ix) | Err(ix) => ix,
         };
         let end_ix = match self
             .disjoint
-            .binary_search_by(|probe| probe.start.cmp(&range.end, map.buffer_snapshot()))
+            .binary_search_by(|probe| probe.start.cmp(&range.end, snapshot.buffer_snapshot()))
         {
             Ok(ix) => ix + 1,
             Err(ix) => ix,
         };
-        resolve_selections(&self.disjoint[start_ix..end_ix], &map).collect()
+        resolve_selections(&self.disjoint[start_ix..end_ix], snapshot).collect()
     }
 
-    pub fn all_display(&self, cx: &mut App) -> (DisplaySnapshot, Vec<Selection<DisplayPoint>>) {
-        let map = self.display_map(cx);
+    pub fn all_display(&self, snapshot: &DisplaySnapshot) -> Vec<Selection<DisplayPoint>> {
         let disjoint_anchors = &self.disjoint;
-        let mut disjoint = resolve_selections_display(disjoint_anchors.iter(), &map).peekable();
-        let mut pending_opt = resolve_selections_display(self.pending_anchor(), &map).next();
-        let selections = iter::from_fn(move || {
+        let mut disjoint =
+            resolve_selections_display(disjoint_anchors.iter(), &snapshot).peekable();
+        let mut pending_opt = resolve_selections_display(self.pending_anchor(), &snapshot).next();
+        iter::from_fn(move || {
             if let Some(pending) = pending_opt.as_mut() {
                 while let Some(next_selection) = disjoint.peek() {
                     if pending.start <= next_selection.end && pending.end >= next_selection.start {
@@ -292,8 +290,7 @@ impl SelectionsCollection {
                 disjoint.next()
             }
         })
-        .collect();
-        (map, selections)
+        .collect()
     }
 
     pub fn newest_anchor(&self) -> &Selection<Anchor> {
@@ -306,19 +303,15 @@ impl SelectionsCollection {
 
     pub fn newest<D: TextDimension + Ord + Sub<D, Output = D>>(
         &self,
-        cx: &mut App,
+        snapshot: &DisplaySnapshot,
     ) -> Selection<D> {
-        let map = self.display_map(cx);
-
-        resolve_selections([self.newest_anchor()], &map)
+        resolve_selections([self.newest_anchor()], &snapshot)
             .next()
             .unwrap()
     }
 
-    pub fn newest_display(&self, cx: &mut App) -> Selection<DisplayPoint> {
-        let map = self.display_map(cx);
-
-        resolve_selections_display([self.newest_anchor()], &map)
+    pub fn newest_display(&self, snapshot: &DisplaySnapshot) -> Selection<DisplayPoint> {
+        resolve_selections_display([self.newest_anchor()], &snapshot)
             .next()
             .unwrap()
     }
@@ -333,11 +326,9 @@ impl SelectionsCollection {
 
     pub fn oldest<D: TextDimension + Ord + Sub<D, Output = D>>(
         &self,
-        cx: &mut App,
+        snapshot: &DisplaySnapshot,
     ) -> Selection<D> {
-        let map = self.display_map(cx);
-
-        resolve_selections([self.oldest_anchor()], &map)
+        resolve_selections([self.oldest_anchor()], &snapshot)
             .next()
             .unwrap()
     }
@@ -349,12 +340,18 @@ impl SelectionsCollection {
             .unwrap_or_else(|| self.disjoint.first().cloned().unwrap())
     }
 
-    pub fn first<D: TextDimension + Ord + Sub<D, Output = D>>(&self, cx: &mut App) -> Selection<D> {
-        self.all(cx).first().unwrap().clone()
+    pub fn first<D: TextDimension + Ord + Sub<D, Output = D>>(
+        &self,
+        snapshot: &DisplaySnapshot,
+    ) -> Selection<D> {
+        self.all(snapshot).first().unwrap().clone()
     }
 
-    pub fn last<D: TextDimension + Ord + Sub<D, Output = D>>(&self, cx: &mut App) -> Selection<D> {
-        self.all(cx).last().unwrap().clone()
+    pub fn last<D: TextDimension + Ord + Sub<D, Output = D>>(
+        &self,
+        snapshot: &DisplaySnapshot,
+    ) -> Selection<D> {
+        self.all(snapshot).last().unwrap().clone()
     }
 
     /// Returns a list of (potentially backwards!) ranges representing the selections.
@@ -362,9 +359,9 @@ impl SelectionsCollection {
     #[cfg(any(test, feature = "test-support"))]
     pub fn ranges<D: TextDimension + Ord + Sub<D, Output = D>>(
         &self,
-        cx: &mut App,
+        snapshot: &DisplaySnapshot,
     ) -> Vec<Range<D>> {
-        self.all::<D>(cx)
+        self.all::<D>(snapshot)
             .iter()
             .map(|s| {
                 if s.reversed {
@@ -596,7 +593,8 @@ impl<'a> MutableSelectionsCollection<'a> {
     where
         T: 'a + ToOffset + ToPoint + TextDimension + Ord + Sub<T, Output = T> + std::marker::Copy,
     {
-        let mut selections = self.collection.all(self.cx);
+        let display_map = self.display_map();
+        let mut selections = self.collection.all(&display_map);
         let mut start = range.start.to_offset(&self.buffer());
         let mut end = range.end.to_offset(&self.buffer());
         let reversed = if start > end {
@@ -790,7 +788,7 @@ impl<'a> MutableSelectionsCollection<'a> {
     ) {
         let mut changed = false;
         let display_map = self.display_map();
-        let (_, selections) = self.collection.all_display(self.cx);
+        let selections = self.collection.all_display(&display_map);
         let selections = selections
             .into_iter()
             .map(|selection| {
@@ -814,9 +812,10 @@ impl<'a> MutableSelectionsCollection<'a> {
     ) {
         let mut changed = false;
         let snapshot = self.buffer().clone();
+        let display_map = self.display_map();
         let selections = self
             .collection
-            .all::<usize>(self.cx)
+            .all::<usize>(&display_map)
             .into_iter()
             .map(|selection| {
                 let mut moved_selection = selection.clone();

crates/editor/src/signature_help.rs 🔗

@@ -82,7 +82,7 @@ impl Editor {
         if !(self.signature_help_state.is_shown() || self.auto_signature_help_enabled(cx)) {
             return false;
         }
-        let newest_selection = self.selections.newest::<usize>(cx);
+        let newest_selection = self.selections.newest::<usize>(&self.display_snapshot(cx));
         let head = newest_selection.head();
 
         if !newest_selection.is_empty() && head != newest_selection.tail() {
@@ -396,13 +396,8 @@ impl SignatureHelpPopover {
                 .shape(IconButtonShape::Square)
                 .style(ButtonStyle::Subtle)
                 .icon_size(IconSize::Small)
-                .tooltip(move |window, cx| {
-                    ui::Tooltip::for_action(
-                        "Previous Signature",
-                        &crate::SignatureHelpPrevious,
-                        window,
-                        cx,
-                    )
+                .tooltip(move |_window, cx| {
+                    ui::Tooltip::for_action("Previous Signature", &crate::SignatureHelpPrevious, cx)
                 })
                 .on_click(cx.listener(|editor, _, window, cx| {
                     editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
@@ -412,8 +407,8 @@ impl SignatureHelpPopover {
                 .shape(IconButtonShape::Square)
                 .style(ButtonStyle::Subtle)
                 .icon_size(IconSize::Small)
-                .tooltip(move |window, cx| {
-                    ui::Tooltip::for_action("Next Signature", &crate::SignatureHelpNext, window, cx)
+                .tooltip(move |_window, cx| {
+                    ui::Tooltip::for_action("Next Signature", &crate::SignatureHelpNext, cx)
                 })
                 .on_click(cx.listener(|editor, _, window, cx| {
                     editor.signature_help_next(&crate::SignatureHelpNext, window, cx);

crates/editor/src/tasks.rs 🔗

@@ -14,7 +14,7 @@ impl Editor {
             return Task::ready(None);
         };
         let (selection, buffer, editor_snapshot) = {
-            let selection = self.selections.newest_adjusted(cx);
+            let selection = self.selections.newest_adjusted(&self.display_snapshot(cx));
             let Some((buffer, _)) = self
                 .buffer()
                 .read(cx)

crates/editor/src/test.rs 🔗

@@ -108,7 +108,7 @@ pub fn assert_text_with_selections(
     assert_eq!(editor.text(cx), unmarked_text, "text doesn't match");
     let actual = generate_marked_text(
         &editor.text(cx),
-        &editor.selections.ranges(cx),
+        &editor.selections.ranges(&editor.display_snapshot(cx)),
         marked_text.contains("«"),
     );
     assert_eq!(actual, marked_text, "Selections don't match");

crates/editor/src/test/editor_lsp_test_context.rs 🔗

@@ -6,6 +6,7 @@ use std::{
 };
 
 use anyhow::Result;
+use language::rust_lang;
 use serde_json::json;
 
 use crate::{Editor, ToPoint};
@@ -18,7 +19,6 @@ use language::{
     point_to_lsp,
 };
 use lsp::{notification, request};
-use multi_buffer::ToPointUtf16;
 use project::Project;
 use smol::stream::StreamExt;
 use workspace::{AppState, Workspace, WorkspaceHandle};
@@ -32,55 +32,6 @@ pub struct EditorLspTestContext {
     pub buffer_lsp_url: lsp::Uri,
 }
 
-pub(crate) fn rust_lang() -> Arc<Language> {
-    let language = Language::new(
-        LanguageConfig {
-            name: "Rust".into(),
-            matcher: LanguageMatcher {
-                path_suffixes: vec!["rs".to_string()],
-                ..Default::default()
-            },
-            line_comments: vec!["// ".into(), "/// ".into(), "//! ".into()],
-            ..Default::default()
-        },
-        Some(tree_sitter_rust::LANGUAGE.into()),
-    )
-    .with_queries(LanguageQueries {
-        indents: Some(Cow::from(indoc! {r#"
-            [
-                ((where_clause) _ @end)
-                (field_expression)
-                (call_expression)
-                (assignment_expression)
-                (let_declaration)
-                (let_chain)
-                (await_expression)
-            ] @indent
-
-            (_ "[" "]" @end) @indent
-            (_ "<" ">" @end) @indent
-            (_ "{" "}" @end) @indent
-            (_ "(" ")" @end) @indent"#})),
-        brackets: Some(Cow::from(indoc! {r#"
-            ("(" @open ")" @close)
-            ("[" @open "]" @close)
-            ("{" @open "}" @close)
-            ("<" @open ">" @close)
-            ("\"" @open "\"" @close)
-            (closure_parameters "|" @open "|" @close)"#})),
-        text_objects: Some(Cow::from(indoc! {r#"
-            (function_item
-                body: (_
-                    "{"
-                    (_)* @function.inside
-                    "}" )) @function.around
-        "#})),
-        ..Default::default()
-    })
-    .expect("Could not parse queries");
-    Arc::new(language)
-}
-
 #[cfg(test)]
 pub(crate) fn git_commit_lang() -> Arc<Language> {
     Arc::new(Language::new(

crates/editor/src/test/editor_test_context.rs 🔗

@@ -276,7 +276,10 @@ impl EditorTestContext {
 
     pub fn pixel_position_for(&mut self, display_point: DisplayPoint) -> Point<Pixels> {
         self.update_editor(|editor, window, cx| {
-            let newest_point = editor.selections.newest_display(cx).head();
+            let newest_point = editor
+                .selections
+                .newest_display(&editor.display_snapshot(cx))
+                .head();
             let pixel_position = editor.pixel_position_of_newest_cursor.unwrap();
             let line_height = editor
                 .style()
@@ -601,7 +604,7 @@ impl EditorTestContext {
     fn editor_selections(&mut self) -> Vec<Range<usize>> {
         self.editor
             .update(&mut self.cx, |editor, cx| {
-                editor.selections.all::<usize>(cx)
+                editor.selections.all::<usize>(&editor.display_snapshot(cx))
             })
             .into_iter()
             .map(|s| {
@@ -699,9 +702,12 @@ pub fn assert_state_with_diff(
     expected_diff_text: &str,
 ) {
     let (snapshot, selections) = editor.update_in(cx, |editor, window, cx| {
+        let snapshot = editor.snapshot(window, cx);
         (
-            editor.snapshot(window, cx).buffer_snapshot().clone(),
-            editor.selections.ranges::<usize>(cx),
+            snapshot.buffer_snapshot().clone(),
+            editor
+                .selections
+                .ranges::<usize>(&snapshot.display_snapshot),
         )
     });
 

crates/eval/Cargo.toml 🔗

@@ -18,18 +18,17 @@ name = "explorer"
 path = "src/explorer.rs"
 
 [dependencies]
-agent.workspace = true
+acp_thread.workspace = true
+agent = { workspace = true, features = ["eval"] }
+agent-client-protocol.workspace = true
 agent_settings.workspace = true
 agent_ui.workspace = true
 anyhow.workspace = true
-assistant_tool.workspace = true
-assistant_tools.workspace = true
 async-trait.workspace = true
 buffer_diff.workspace = true
 chrono.workspace = true
 clap.workspace = true
 client.workspace = true
-cloud_llm_client.workspace = true
 collections.workspace = true
 debug_adapter_extension.workspace = true
 dirs.workspace = true
@@ -54,13 +53,13 @@ pretty_assertions.workspace = true
 project.workspace = true
 prompt_store.workspace = true
 regex.workspace = true
+rand.workspace = true
 release_channel.workspace = true
 reqwest_client.workspace = true
 serde.workspace = true
 serde_json.workspace = true
 settings.workspace = true
 shellexpand.workspace = true
-smol.workspace = true
 telemetry.workspace = true
 terminal_view.workspace = true
 toml.workspace = true
@@ -68,4 +67,3 @@ unindent.workspace = true
 util.workspace = true
 uuid.workspace = true
 watch.workspace = true
-workspace-hack.workspace = true

crates/eval/runner_settings.json 🔗

@@ -1,7 +1,5 @@
 {
-  "assistant": {
-    "always_allow_tool_actions": true,
-    "stream_edits": true,
-    "version": "2"
+  "agent": {
+    "always_allow_tool_actions": true
   }
 }

crates/eval/src/eval.rs 🔗

@@ -61,9 +61,22 @@ struct Args {
     /// Maximum number of examples to run concurrently.
     #[arg(long, default_value = "4")]
     concurrency: usize,
+    /// Output current environment variables as JSON to stdout
+    #[arg(long, hide = true)]
+    printenv: bool,
 }
 
 fn main() {
+    let args = Args::parse();
+
+    // This prevents errors showing up in the logs, because
+    // project::environment::load_shell_environment() calls
+    // std::env::current_exe().unwrap() --printenv
+    if args.printenv {
+        util::shell_env::print_env();
+        return;
+    }
+
     dotenvy::from_filename(CARGO_MANIFEST_DIR.join(".env")).ok();
 
     env_logger::init();
@@ -99,7 +112,6 @@ fn main() {
 
     let zed_commit_sha = commit_sha_for_path(&root_dir);
     let zed_branch_name = git_branch_for_path(&root_dir);
-    let args = Args::parse();
     let languages: HashSet<String> = args.languages.into_iter().collect();
 
     let http_client = Arc::new(ReqwestClient::new());
@@ -126,19 +138,20 @@ fn main() {
 
         let mut cumulative_tool_metrics = ToolMetrics::default();
 
-        let agent_model = load_model(&args.model, cx).unwrap();
-        let judge_model = load_model(&args.judge_model, cx).unwrap();
-
-        LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
-            registry.set_default_model(Some(agent_model.clone()), cx);
+        let tasks = LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
+            registry.providers().iter().map(|p| p.authenticate(cx)).collect::<Vec<_>>()
         });
 
-        let auth1 = agent_model.provider.authenticate(cx);
-        let auth2 = judge_model.provider.authenticate(cx);
-
         cx.spawn(async move |cx| {
-            auth1.await?;
-            auth2.await?;
+            future::join_all(tasks).await;
+            let judge_model = cx.update(|cx| {
+                let agent_model = load_model(&args.model, cx).unwrap();
+                let judge_model = load_model(&args.judge_model, cx).unwrap();
+                LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
+                    registry.set_default_model(Some(agent_model.clone()), cx);
+                });
+                judge_model
+            })?;
 
             let mut examples = Vec::new();
 
@@ -268,7 +281,6 @@ fn main() {
 
             future::join_all((0..args.concurrency).map(|_| {
                 let app_state = app_state.clone();
-                let model = agent_model.model.clone();
                 let judge_model = judge_model.model.clone();
                 let zed_commit_sha = zed_commit_sha.clone();
                 let zed_branch_name = zed_branch_name.clone();
@@ -283,7 +295,7 @@ fn main() {
                         let result = async {
                             example.setup().await?;
                             let run_output = cx
-                                .update(|cx| example.run(model.clone(), app_state.clone(), cx))?
+                                .update(|cx| example.run(app_state.clone(), cx))?
                                 .await?;
                             let judge_output = judge_example(
                                 example.clone(),
@@ -429,7 +441,6 @@ pub fn init(cx: &mut App) -> Arc<AgentAppState> {
         true,
         cx,
     );
-    assistant_tools::init(client.http_client(), cx);
 
     SettingsStore::update_global(cx, |store, cx| {
         store.set_user_settings(include_str!("../runner_settings.json"), cx)
@@ -525,7 +536,6 @@ async fn judge_example(
             diff_evaluation = judge_output.diff.clone(),
             thread_evaluation = judge_output.thread,
             tool_metrics = run_output.tool_metrics,
-            response_count = run_output.response_count,
             token_usage = run_output.token_usage,
             model = model.telemetry_id(),
             model_provider = model.provider_id().to_string(),

crates/eval/src/example.rs 🔗

@@ -3,22 +3,24 @@ use std::{
     fmt::{self, Debug},
     sync::{Arc, Mutex},
     time::Duration,
+    u32,
 };
 
 use crate::{
     ToolMetrics,
     assertions::{AssertionsReport, RanAssertion, RanAssertionResult},
 };
-use agent::{ContextLoadResult, Thread, ThreadEvent};
+use acp_thread::UserMessageId;
+use agent::{Thread, ThreadEvent, UserMessageContent};
+use agent_client_protocol as acp;
 use agent_settings::AgentProfileId;
 use anyhow::{Result, anyhow};
 use async_trait::async_trait;
 use buffer_diff::DiffHunkStatus;
-use cloud_llm_client::CompletionIntent;
 use collections::HashMap;
-use futures::{FutureExt as _, StreamExt, channel::mpsc, select_biased};
+use futures::{FutureExt as _, StreamExt, select_biased};
 use gpui::{App, AppContext, AsyncApp, Entity};
-use language_model::{LanguageModel, Role, StopReason};
+use language_model::Role;
 use util::rel_path::RelPath;
 
 pub const THREAD_EVENT_TIMEOUT: Duration = Duration::from_secs(60 * 2);
@@ -91,7 +93,6 @@ pub struct ExampleContext {
     log_prefix: String,
     agent_thread: Entity<agent::Thread>,
     app: AsyncApp,
-    model: Arc<dyn LanguageModel>,
     pub assertions: AssertionsReport,
     pub tool_metrics: Arc<Mutex<ToolMetrics>>,
 }
@@ -101,7 +102,6 @@ impl ExampleContext {
         meta: ExampleMetadata,
         log_prefix: String,
         agent_thread: Entity<Thread>,
-        model: Arc<dyn LanguageModel>,
         app: AsyncApp,
     ) -> Self {
         let assertions = AssertionsReport::new(meta.max_assertions);
@@ -111,26 +111,11 @@ impl ExampleContext {
             log_prefix,
             agent_thread,
             assertions,
-            model,
             app,
             tool_metrics: Arc::new(Mutex::new(ToolMetrics::default())),
         }
     }
 
-    pub fn push_user_message(&mut self, text: impl ToString) {
-        self.app
-            .update_entity(&self.agent_thread, |thread, cx| {
-                thread.insert_user_message(
-                    text.to_string(),
-                    ContextLoadResult::default(),
-                    None,
-                    Vec::new(),
-                    cx,
-                );
-            })
-            .unwrap();
-    }
-
     pub fn assert(&mut self, expected: bool, message: impl ToString) -> Result<()> {
         let message = message.to_string();
         self.log_assertion(
@@ -202,156 +187,174 @@ impl ExampleContext {
         result
     }
 
-    pub async fn run_to_end(&mut self) -> Result<Response> {
-        self.run_turns(u32::MAX).await
+    pub async fn prompt(&mut self, prompt: impl Into<String>) -> Result<Response> {
+        self.prompt_with_max_turns(prompt, u32::MAX).await
     }
 
-    pub async fn run_turn(&mut self) -> Result<Response> {
-        self.run_turns(1).await
+    pub async fn prompt_with_max_turns(
+        &mut self,
+        prompt: impl Into<String>,
+        max_turns: u32,
+    ) -> Result<Response> {
+        let content = vec![UserMessageContent::Text(prompt.into())];
+        self.run_turns(Some(content), max_turns).await
     }
 
-    pub async fn run_turns(&mut self, iterations: u32) -> Result<Response> {
-        let (mut tx, mut rx) = mpsc::channel(1);
+    pub async fn proceed_with_max_turns(&mut self, max_turns: u32) -> Result<Response> {
+        self.run_turns(None, max_turns).await
+    }
 
+    async fn run_turns(
+        &mut self,
+        prompt: Option<Vec<UserMessageContent>>,
+        max_turns: u32,
+    ) -> Result<Response> {
         let tool_metrics = self.tool_metrics.clone();
         let log_prefix = self.log_prefix.clone();
-        let _subscription = self.app.subscribe(
-            &self.agent_thread,
-            move |thread, event: &ThreadEvent, cx| match event {
-                ThreadEvent::ShowError(thread_error) => {
-                    tx.try_send(Err(anyhow!(thread_error.clone()))).ok();
-                }
-                ThreadEvent::Stopped(reason) => match reason {
-                    Ok(StopReason::EndTurn) => {
-                        tx.close_channel();
+
+        let mut remaining_turns = max_turns;
+
+        let mut event_stream = self.agent_thread.update(&mut self.app, |thread, cx| {
+            if let Some(prompt) = prompt {
+                let id = UserMessageId::new();
+                thread.send(id, prompt, cx)
+            } else {
+                thread.proceed(cx)
+            }
+        })??;
+
+        let task = self.app.background_spawn(async move {
+            let mut messages = Vec::new();
+            let mut tool_uses_by_id = HashMap::default();
+            while let Some(event) = event_stream.next().await {
+                match event? {
+                    ThreadEvent::UserMessage(user_message) => {
+                        messages.push(Message {
+                            role: Role::User,
+                            text: user_message.to_markdown(),
+                            tool_use: Vec::new(),
+                        });
                     }
-                    Ok(StopReason::ToolUse) => {
-                        if thread.read(cx).remaining_turns() == 0 {
-                            tx.close_channel();
+                    ThreadEvent::AgentThinking(text) | ThreadEvent::AgentText(text) => {
+                        if matches!(
+                            messages.last(),
+                            Some(Message {
+                                role: Role::Assistant,
+                                ..
+                            })
+                        ) {
+                            messages.last_mut().unwrap().text.push_str(&text);
+                        } else {
+                            messages.push(Message {
+                                role: Role::Assistant,
+                                text,
+                                tool_use: Vec::new(),
+                            });
                         }
                     }
-                    Ok(StopReason::MaxTokens) => {
-                        tx.try_send(Err(anyhow!("Exceeded maximum tokens"))).ok();
-                    }
-                    Ok(StopReason::Refusal) => {
-                        tx.try_send(Err(anyhow!("Model refused to generate content")))
-                            .ok();
-                    }
-                    Err(err) => {
-                        tx.try_send(Err(anyhow!(err.clone()))).ok();
+                    ThreadEvent::ToolCall(tool_call) => {
+                        let meta = tool_call.meta.expect("Missing meta field in tool_call");
+                        let tool_name = meta
+                            .get("tool_name")
+                            .expect("Missing tool_name field in meta")
+                            .as_str()
+                            .expect("Unknown tool_name content in meta");
+
+                        tool_uses_by_id.insert(
+                            tool_call.id,
+                            ToolUse {
+                                name: tool_name.to_string(),
+                                value: tool_call.raw_input.unwrap_or_default(),
+                            },
+                        );
+                        if matches!(
+                            tool_call.status,
+                            acp::ToolCallStatus::Completed | acp::ToolCallStatus::Failed
+                        ) {
+                            panic!("Tool call completed without update");
+                        }
                     }
-                },
-                ThreadEvent::NewRequest
-                | ThreadEvent::StreamedAssistantText(_, _)
-                | ThreadEvent::StreamedAssistantThinking(_, _)
-                | ThreadEvent::UsePendingTools { .. }
-                | ThreadEvent::CompletionCanceled => {}
-                ThreadEvent::ToolUseLimitReached => {}
-                ThreadEvent::ToolFinished {
-                    tool_use_id,
-                    pending_tool_use,
-                    ..
-                } => {
-                    thread.update(cx, |thread, _cx| {
-                        if let Some(tool_use) = pending_tool_use {
-                            let mut tool_metrics = tool_metrics.lock().unwrap();
-                            if let Some(tool_result) = thread.tool_result(tool_use_id) {
-                                let message = if tool_result.is_error {
-                                    format!("✖︎ {}", tool_use.name)
-                                } else {
+                    ThreadEvent::ToolCallUpdate(tool_call_update) => {
+                        if let acp_thread::ToolCallUpdate::UpdateFields(update) = tool_call_update {
+                            if let Some(raw_input) = update.fields.raw_input {
+                                if let Some(tool_use) = tool_uses_by_id.get_mut(&update.id) {
+                                    tool_use.value = raw_input;
+                                }
+                            }
+
+                            if matches!(
+                                update.fields.status,
+                                Some(acp::ToolCallStatus::Completed | acp::ToolCallStatus::Failed)
+                            ) {
+                                let succeeded =
+                                    update.fields.status == Some(acp::ToolCallStatus::Completed);
+
+                                let tool_use = tool_uses_by_id
+                                    .remove(&update.id)
+                                    .expect("Unrecognized tool call completed");
+
+                                let log_message = if succeeded {
                                     format!("✔︎ {}", tool_use.name)
+                                } else {
+                                    format!("✖︎ {}", tool_use.name)
                                 };
-                                println!("{log_prefix}{message}");
+                                println!("{log_prefix}{log_message}");
+
                                 tool_metrics
-                                    .insert(tool_result.tool_name.clone(), !tool_result.is_error);
-                            } else {
-                                let message =
-                                    format!("TOOL FINISHED WITHOUT RESULT: {}", tool_use.name);
-                                println!("{log_prefix}{message}");
-                                tool_metrics.insert(tool_use.name.clone(), true);
+                                    .lock()
+                                    .unwrap()
+                                    .insert(tool_use.name.clone().into(), succeeded);
+
+                                if let Some(message) = messages.last_mut() {
+                                    message.tool_use.push(tool_use);
+                                } else {
+                                    messages.push(Message {
+                                        role: Role::Assistant,
+                                        text: "".to_string(),
+                                        tool_use: vec![tool_use],
+                                    });
+                                }
+
+                                remaining_turns -= 1;
+                                if remaining_turns == 0 {
+                                    return Ok(messages);
+                                }
                             }
                         }
-                    });
-                }
-                ThreadEvent::InvalidToolInput { .. } => {
-                    println!("{log_prefix} invalid tool input");
-                }
-                ThreadEvent::MissingToolUse {
-                    tool_use_id: _,
-                    ui_text,
-                } => {
-                    println!("{log_prefix} {ui_text}");
-                }
-                ThreadEvent::ToolConfirmationNeeded => {
-                    panic!(
+                    }
+                    ThreadEvent::ToolCallAuthorization(_) => panic!(
                         "{}Bug: Tool confirmation should not be required in eval",
                         log_prefix
-                    );
-                }
-                ThreadEvent::StreamedCompletion
-                | ThreadEvent::MessageAdded(_)
-                | ThreadEvent::MessageEdited(_)
-                | ThreadEvent::MessageDeleted(_)
-                | ThreadEvent::SummaryChanged
-                | ThreadEvent::SummaryGenerated
-                | ThreadEvent::ProfileChanged
-                | ThreadEvent::ReceivedTextChunk
-                | ThreadEvent::StreamedToolUse { .. }
-                | ThreadEvent::CheckpointChanged
-                | ThreadEvent::CancelEditing => {
-                    tx.try_send(Ok(())).ok();
-                    if std::env::var("ZED_EVAL_DEBUG").is_ok() {
-                        println!("{}Event: {:#?}", log_prefix, event);
-                    }
-                }
-            },
-        );
-
-        let model = self.model.clone();
-
-        let message_count_before = self.app.update_entity(&self.agent_thread, |thread, cx| {
-            thread.set_remaining_turns(iterations);
-            thread.send_to_model(model, CompletionIntent::UserPrompt, None, cx);
-            thread.messages().len()
-        })?;
-
-        loop {
-            select_biased! {
-                result = rx.next() => {
-                    if let Some(result) = result {
-                        result?;
-                    } else {
-                        break;
+                    ),
+                    ThreadEvent::Retry(status) => {
+                        println!("{log_prefix} Got retry: {status:?}");
                     }
-                }
-                _ = self.app.background_executor().timer(THREAD_EVENT_TIMEOUT).fuse() => {
-                    anyhow::bail!("Agentic loop stalled - waited {THREAD_EVENT_TIMEOUT:?} without any events");
+                    ThreadEvent::Stop(stop_reason) => match stop_reason {
+                        acp::StopReason::EndTurn => {}
+                        acp::StopReason::MaxTokens => {
+                            return Err(anyhow!("Exceeded maximum tokens"));
+                        }
+                        acp::StopReason::MaxTurnRequests => {
+                            return Err(anyhow!("Exceeded maximum turn requests"));
+                        }
+                        acp::StopReason::Refusal => {
+                            return Err(anyhow!("Refusal"));
+                        }
+                        acp::StopReason::Cancelled => return Err(anyhow!("Cancelled")),
+                    },
                 }
             }
-        }
+            Ok(messages)
+        });
 
-        let messages = self.app.read_entity(&self.agent_thread, |thread, cx| {
-            let mut messages = Vec::new();
-            for message in thread.messages().skip(message_count_before) {
-                messages.push(Message {
-                    _role: message.role,
-                    text: message.to_message_content(),
-                    tool_use: thread
-                        .tool_uses_for_message(message.id, cx)
-                        .into_iter()
-                        .map(|tool_use| ToolUse {
-                            name: tool_use.name.to_string(),
-                            value: tool_use.input,
-                        })
-                        .collect(),
-                });
+        select_biased! {
+            result = task.fuse() => {
+                Ok(Response::new(result?))
             }
-            messages
-        })?;
-
-        let response = Response::new(messages);
-
-        Ok(response)
+            _ = self.app.background_executor().timer(THREAD_EVENT_TIMEOUT).fuse() => {
+                anyhow::bail!("Agentic loop stalled - waited {THREAD_EVENT_TIMEOUT:?} without any events");
+            }
+        }
     }
 
     pub fn edits(&self) -> HashMap<Arc<RelPath>, FileEdits> {
@@ -486,7 +489,7 @@ impl Response {
         Self { messages }
     }
 
-    pub fn expect_tool(
+    pub fn expect_tool_call(
         &self,
         tool_name: &'static str,
         cx: &mut ExampleContext,
@@ -503,8 +506,7 @@ impl Response {
         })
     }
 
-    #[allow(dead_code)]
-    pub fn tool_uses(&self) -> impl Iterator<Item = &ToolUse> {
+    pub fn tool_calls(&self) -> impl Iterator<Item = &ToolUse> {
         self.messages.iter().flat_map(|msg| &msg.tool_use)
     }
 
@@ -515,7 +517,7 @@ impl Response {
 
 #[derive(Debug)]
 pub struct Message {
-    _role: Role,
+    role: Role,
     text: String,
     tool_use: Vec<ToolUse>,
 }

crates/eval/src/examples/add_arg_to_trait_method.rs 🔗

@@ -27,14 +27,12 @@ impl Example for AddArgToTraitMethod {
 
     async fn conversation(&self, cx: &mut ExampleContext) -> Result<()> {
         const FILENAME: &str = "assistant_tool.rs";
-        cx.push_user_message(format!(
+        let _ = cx.prompt(format!(
             r#"
             Add a `window: Option<gpui::AnyWindowHandle>` argument to the `Tool::run` trait method in {FILENAME},
             and update all the implementations of the trait and call sites accordingly.
             "#
-        ));
-
-        let _ = cx.run_to_end().await?;
+        )).await?;
 
         // Adds ignored argument to all but `batch_tool`
 

crates/eval/src/examples/code_block_citations.rs 🔗

@@ -29,16 +29,19 @@ impl Example for CodeBlockCitations {
 
     async fn conversation(&self, cx: &mut ExampleContext) -> Result<()> {
         const FILENAME: &str = "assistant_tool.rs";
-        cx.push_user_message(format!(
-            r#"
-            Show me the method bodies of all the methods of the `Tool` trait in {FILENAME}.
-
-            Please show each method in a separate code snippet.
-            "#
-        ));
 
         // Verify that the messages all have the correct formatting.
-        let texts: Vec<String> = cx.run_to_end().await?.texts().collect();
+        let texts: Vec<String> = cx
+            .prompt(format!(
+                r#"
+                Show me the method bodies of all the methods of the `Tool` trait in {FILENAME}.
+
+                Please show each method in a separate code snippet.
+                "#
+            ))
+            .await?
+            .texts()
+            .collect();
         let closing_fence = format!("\n{FENCE}");
 
         for text in texts.iter() {

crates/eval/src/examples/comment_translation.rs 🔗

@@ -1,7 +1,7 @@
 use crate::example::{Example, ExampleContext, ExampleMetadata, JudgeAssertion};
+use agent::{EditFileMode, EditFileToolInput};
 use agent_settings::AgentProfileId;
 use anyhow::Result;
-use assistant_tools::{EditFileMode, EditFileToolInput};
 use async_trait::async_trait;
 
 pub struct CommentTranslation;
@@ -22,30 +22,26 @@ impl Example for CommentTranslation {
     }
 
     async fn conversation(&self, cx: &mut ExampleContext) -> Result<()> {
-        cx.push_user_message(r#"
-            Edit the following files and translate all their comments to italian, in this exact order:
+        let response = cx.prompt(
+            r#"
+                Edit the following files and translate all their comments to italian, in this exact order:
 
-            - font-kit/src/family.rs
-            - font-kit/src/canvas.rs
-            - font-kit/src/error.rs
-        "#);
-        cx.run_to_end().await?;
+                - font-kit/src/family.rs
+                - font-kit/src/canvas.rs
+                - font-kit/src/error.rs
+            "#
+        ).await?;
 
         let mut create_or_overwrite_count = 0;
-        cx.agent_thread().read_with(cx, |thread, cx| {
-            for message in thread.messages() {
-                for tool_use in thread.tool_uses_for_message(message.id, cx) {
-                    if tool_use.name == "edit_file" {
-                        let input: EditFileToolInput = serde_json::from_value(tool_use.input)?;
-                        if !matches!(input.mode, EditFileMode::Edit) {
-                            create_or_overwrite_count += 1;
-                        }
-                    }
+        for tool_call in response.tool_calls() {
+            if tool_call.name == "edit_file" {
+                let input = tool_call.parse_input::<EditFileToolInput>()?;
+                if !matches!(input.mode, EditFileMode::Edit) {
+                    create_or_overwrite_count += 1;
                 }
             }
+        }
 
-            anyhow::Ok(())
-        })??;
         cx.assert_eq(create_or_overwrite_count, 0, "no_creation_or_overwrite")?;
 
         Ok(())

crates/eval/src/examples/file_change_notification.rs 🔗

@@ -48,8 +48,8 @@ impl Example for FileChangeNotificationExample {
         })?;
 
         // Start conversation (specific message is not important)
-        cx.push_user_message("Find all files in this repo");
-        cx.run_turn().await?;
+        cx.prompt_with_max_turns("Find all files in this repo", 1)
+            .await?;
 
         // Edit the README buffer - the model should get a notification on next turn
         buffer.update(cx, |buffer, cx| {
@@ -58,7 +58,7 @@ impl Example for FileChangeNotificationExample {
 
         // Run for some more turns.
         // The model shouldn't thank us for letting it know about the file change.
-        cx.run_turns(3).await?;
+        cx.proceed_with_max_turns(3).await?;
 
         Ok(())
     }

crates/eval/src/examples/file_search.rs 🔗

@@ -1,6 +1,6 @@
+use agent::FindPathToolInput;
 use agent_settings::AgentProfileId;
 use anyhow::Result;
-use assistant_tools::FindPathToolInput;
 use async_trait::async_trait;
 use regex::Regex;
 
@@ -25,18 +25,19 @@ impl Example for FileSearchExample {
 
     async fn conversation(&self, cx: &mut ExampleContext) -> Result<()> {
         const FILENAME: &str = "find_replace_file_tool.rs";
-        cx.push_user_message(format!(
-                r#"
+
+        let prompt = format!(
+            r#"
         Look at the `{FILENAME}`. I want to implement a card for it. The card should implement the `Render` trait.
 
         The card should show a diff. It should be a beautifully presented diff. The card "box" should look like what we show for
         markdown codeblocks (look at `MarkdownElement`). I want to see a red background for lines that were deleted and a green
         background for lines that were added. We should have a div per diff line.
         "#
-        ));
+        );
 
-        let response = cx.run_turn().await?;
-        let tool_use = response.expect_tool("find_path", cx)?;
+        let response = cx.prompt_with_max_turns(prompt, 1).await?;
+        let tool_use = response.expect_tool_call("find_path", cx)?;
         let input = tool_use.parse_input::<FindPathToolInput>()?;
 
         let glob = input.glob;

crates/eval/src/examples/grep_params_escapement.rs 🔗

@@ -1,6 +1,6 @@
+use agent::GrepToolInput;
 use agent_settings::AgentProfileId;
 use anyhow::Result;
-use assistant_tools::GrepToolInput;
 use async_trait::async_trait;
 
 use crate::example::{Example, ExampleContext, ExampleMetadata};
@@ -36,9 +36,9 @@ impl Example for GrepParamsEscapementExample {
     }
 
     async fn conversation(&self, cx: &mut ExampleContext) -> Result<()> {
-        // cx.push_user_message("How does the precedence/specificity work with Keymap contexts? I am seeing that `MessageEditor > Editor` is lower precendence than `Editor` which is surprising to me, but might be how it works");
-        cx.push_user_message("Search for files containing the characters `>` or `<`");
-        let response = cx.run_turns(2).await?;
+        let response = cx
+            .prompt_with_max_turns("Search for files containing the characters `>` or `<`", 2)
+            .await?;
         let grep_input = response
             .find_tool_call("grep")
             .and_then(|tool_use| tool_use.parse_input::<GrepToolInput>().ok());

crates/eval/src/examples/mod.rs 🔗

@@ -144,9 +144,8 @@ impl Example for DeclarativeExample {
     }
 
     async fn conversation(&self, cx: &mut ExampleContext) -> Result<()> {
-        cx.push_user_message(&self.prompt);
         let max_turns = self.metadata.max_turns.unwrap_or(1000);
-        let _ = cx.run_turns(max_turns).await;
+        let _ = cx.prompt_with_max_turns(&self.prompt, max_turns).await;
         Ok(())
     }
 

crates/eval/src/examples/overwrite_file.rs 🔗

@@ -1,6 +1,6 @@
+use agent::{EditFileMode, EditFileToolInput};
 use agent_settings::AgentProfileId;
 use anyhow::Result;
-use assistant_tools::{EditFileMode, EditFileToolInput};
 use async_trait::async_trait;
 
 use crate::example::{Example, ExampleContext, ExampleMetadata};
@@ -36,17 +36,14 @@ impl Example for FileOverwriteExample {
     }
 
     async fn conversation(&self, cx: &mut ExampleContext) -> Result<()> {
-        let response = cx.run_turns(1).await?;
-        let file_overwritten = if let Some(tool_use) = response.find_tool_call("edit_file") {
-            let input = tool_use.parse_input::<EditFileToolInput>()?;
-            match input.mode {
-                EditFileMode::Edit => false,
-                EditFileMode::Create | EditFileMode::Overwrite => {
-                    input.path.ends_with("src/language_model_selector.rs")
-                }
+        let response = cx.proceed_with_max_turns(1).await?;
+        let tool_use = response.expect_tool_call("edit_file", cx)?;
+        let input = tool_use.parse_input::<EditFileToolInput>()?;
+        let file_overwritten = match input.mode {
+            EditFileMode::Edit => false,
+            EditFileMode::Create | EditFileMode::Overwrite => {
+                input.path.ends_with("src/language_model_selector.rs")
             }
-        } else {
-            false
         };
 
         cx.assert(!file_overwritten, "File should be edited, not overwritten")

crates/eval/src/examples/planets.rs 🔗

@@ -1,7 +1,6 @@
+use agent::{AgentTool, OpenTool, TerminalTool};
 use agent_settings::AgentProfileId;
 use anyhow::Result;
-use assistant_tool::Tool;
-use assistant_tools::{OpenTool, TerminalTool};
 use async_trait::async_trait;
 
 use crate::example::{Example, ExampleContext, ExampleMetadata, JudgeAssertion};
@@ -24,23 +23,22 @@ impl Example for Planets {
     }
 
     async fn conversation(&self, cx: &mut ExampleContext) -> Result<()> {
-        cx.push_user_message(
-            r#"
+        let response = cx
+            .prompt(
+                r#"
             Make a plain JavaScript web page which renders an animated 3D solar system.
             Let me drag to rotate the camera around.
             Do not use npm.
-            "#
-            .to_string(),
-        );
-
-        let response = cx.run_to_end().await?;
+            "#,
+            )
+            .await?;
         let mut open_tool_uses = 0;
         let mut terminal_tool_uses = 0;
 
-        for tool_use in response.tool_uses() {
-            if tool_use.name == OpenTool.name() {
+        for tool_use in response.tool_calls() {
+            if tool_use.name == OpenTool::name() {
                 open_tool_uses += 1;
-            } else if tool_use.name == TerminalTool::NAME {
+            } else if tool_use.name == TerminalTool::name() {
                 terminal_tool_uses += 1;
             }
         }

crates/eval/src/examples/threads/overwrite-file.json 🔗

@@ -116,7 +116,7 @@
       ],
       "tool_results": [
         {
-          "content": "[package]\nname = \"language_model_selector\"\nversion = \"0.1.0\"\nedition.workspace = true\npublish.workspace = true\nlicense = \"GPL-3.0-or-later\"\n\n[lints]\nworkspace = true\n\n[lib]\npath = \"src/language_model_selector.rs\"\n\n[dependencies]\ncollections.workspace = true\nfeature_flags.workspace = true\nfuzzy.workspace = true\ngpui.workspace = true\nlanguage_model.workspace = true\nlog.workspace = true\npicker.workspace = true\nproto.workspace = true\nui.workspace = true\nworkspace-hack.workspace = true\nzed_actions.workspace = true\n",
+          "content": "[package]\nname = \"language_model_selector\"\nversion = \"0.1.0\"\nedition.workspace = true\npublish.workspace = true\nlicense = \"GPL-3.0-or-later\"\n\n[lints]\nworkspace = true\n\n[lib]\npath = \"src/language_model_selector.rs\"\n\n[dependencies]\ncollections.workspace = true\nfeature_flags.workspace = true\nfuzzy.workspace = true\ngpui.workspace = true\nlanguage_model.workspace = true\nlog.workspace = true\npicker.workspace = true\nproto.workspace = true\nui.workspace = true\n\nzed_actions.workspace = true\n",
           "is_error": false,
           "output": null,
           "tool_use_id": "toolu_019Je2MLfJhpJr93g5igoRAH"

crates/eval/src/instance.rs 🔗

@@ -1,37 +1,38 @@
-use agent::{Message, MessageSegment, SerializedThread, ThreadStore};
+use agent::ContextServerRegistry;
+use agent_client_protocol as acp;
 use anyhow::{Context as _, Result, anyhow, bail};
-use assistant_tool::ToolWorkingSet;
 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, LanguageModelRequest, LanguageModelRequestMessage,
-    LanguageModelToolResultContent, MessageContent, Role, TokenUsage,
+    LanguageModel, LanguageModelCompletionEvent, LanguageModelRegistry, LanguageModelRequest,
+    LanguageModelRequestMessage, LanguageModelToolResultContent, MessageContent, Role, TokenUsage,
 };
-use project::lsp_store::OpenLspBufferHandle;
-use project::{DiagnosticSummary, Project, ProjectPath};
+use project::{DiagnosticSummary, Project, ProjectPath, lsp_store::OpenLspBufferHandle};
+use prompt_store::{ProjectContext, WorktreeContext};
+use rand::{distr, prelude::*};
 use serde::{Deserialize, Serialize};
-use std::cell::RefCell;
-use std::fmt::Write as _;
-use std::fs;
-use std::fs::File;
-use std::io::Write as _;
-use std::path::Path;
-use std::path::PathBuf;
-use std::rc::Rc;
-use std::sync::Arc;
-use std::time::Duration;
+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 _;
-use util::command::new_smol_command;
-use util::markdown::MarkdownCodeBlock;
+use util::{ResultExt as _, command::new_smol_command, markdown::MarkdownCodeBlock};
 
-use crate::assertions::{AssertionsReport, RanAssertion, RanAssertionResult};
-use crate::example::{Example, ExampleContext, FailedAssertion, JudgeAssertion};
-use crate::{AgentAppState, ToolMetrics};
+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";
 
@@ -57,10 +58,9 @@ pub struct RunOutput {
     pub diagnostic_summary_after: DiagnosticSummary,
     pub diagnostics_before: Option<String>,
     pub diagnostics_after: Option<String>,
-    pub response_count: usize,
     pub token_usage: TokenUsage,
     pub tool_metrics: ToolMetrics,
-    pub all_messages: String,
+    pub thread_markdown: String,
     pub programmatic_assertions: AssertionsReport,
 }
 
@@ -194,12 +194,7 @@ impl ExampleInstance {
             .join(self.thread.meta().repo_name())
     }
 
-    pub fn run(
-        &self,
-        model: Arc<dyn LanguageModel>,
-        app_state: Arc<AgentAppState>,
-        cx: &mut App,
-    ) -> Task<Result<RunOutput>> {
+    pub fn run(&self, app_state: Arc<AgentAppState>, cx: &mut App) -> Task<Result<RunOutput>> {
         let project = Project::local(
             app_state.client.clone(),
             app_state.node_runtime.clone(),
@@ -214,15 +209,6 @@ impl ExampleInstance {
             project.create_worktree(self.worktree_path(), true, cx)
         });
 
-        let tools = cx.new(|_| ToolWorkingSet::default());
-        let prompt_store = None;
-        let thread_store = ThreadStore::load(
-            project.clone(),
-            tools,
-            prompt_store,
-            app_state.prompt_builder.clone(),
-            cx,
-        );
         let meta = self.thread.meta();
         let this = self.clone();
 
@@ -301,74 +287,62 @@ impl ExampleInstance {
             // history using undo/redo.
             std::fs::write(&last_diff_file_path, "")?;
 
-            let thread_store = thread_store.await?;
-
+            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();
 
-            let thread =
-                thread_store.update(cx, |thread_store, cx| {
-                    let thread = if let Some(json) = &meta.existing_thread_json {
-                        let serialized = SerializedThread::from_json(json.as_bytes()).expect("Can't read serialized thread");
-                        thread_store.create_thread_from_serialized(serialized, cx)
-                    } else {
-                        thread_store.create_thread(cx)
-                    };
-                    thread.update(cx, |thread, cx| {
-                        thread.set_profile(meta.profile_id.clone(), cx);
-                    });
-                    thread
-                })?;
-
-
-            thread.update(cx, |thread, _cx| {
-                let mut request_count = 0;
-                let previous_diff = Rc::new(RefCell::new("".to_string()));
-                let example_output_dir = this.run_directory.clone();
-                let last_diff_file_path = last_diff_file_path.clone();
-                let messages_json_file_path = example_output_dir.join("last.messages.json");
-                let this = this.clone();
-                thread.set_request_callback(move |request, response_events| {
-                    request_count += 1;
-                    let messages_file_path = example_output_dir.join(format!("{request_count}.messages.md"));
-                    let diff_file_path = example_output_dir.join(format!("{request_count}.diff"));
-                    let last_messages_file_path = example_output_dir.join("last.messages.md");
-                    let request_markdown = RequestMarkdown::new(request);
-                    let response_events_markdown = response_events_to_markdown(response_events);
-                    let dialog = ThreadDialog::new(request, response_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");
-
-                    let diff_result = smol::block_on(this.repository_diff());
-                    match diff_result {
-                        Ok(diff) => {
-                            if diff != previous_diff.borrow().clone() {
-                                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.borrow_mut() = 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");
-                        }
+                    WorktreeContext {
+                        root_name,
+                        abs_path,
+                        rules_file: None,
                     }
+                }).collect::<Vec<_>>();
+                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(
+                        rand::rng()
+                            .sample_iter(&distr::Alphanumeric)
+                            .take(7)
+                            .map(char::from)
+                            .collect::<String>()
+                            .into(),
+                    );
+
+                    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))
+                };
 
-                    if 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");
-                    }
+                thread.update(cx, |thread, cx| {
+                    thread.add_default_tools(Rc::new(EvalThreadEnvironment {
+                        project: project.clone(),
+                    }), cx);
+                    thread.set_profile(meta.profile_id.clone());
+                    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(),
-                model.clone(),
                 cx.clone(),
             );
             let result = this.thread.conversation(&mut example_cx).await;
@@ -381,7 +355,7 @@ impl ExampleInstance {
             println!("{}Stopped", this.log_prefix);
 
             println!("{}Getting repository diff", this.log_prefix);
-            let repository_diff = this.repository_diff().await?;
+            let repository_diff = Self::repository_diff(this.worktree_path(), &this.repo_url()).await?;
 
             std::fs::write(last_diff_file_path, &repository_diff)?;
 
@@ -416,34 +390,28 @@ impl ExampleInstance {
             }
 
             thread.update(cx, |thread, _cx| {
-                let response_count = thread
-                    .messages()
-                    .filter(|message| message.role == language_model::Role::Assistant)
-                    .count();
                 RunOutput {
                     repository_diff,
                     diagnostic_summary_before,
                     diagnostic_summary_after,
                     diagnostics_before,
                     diagnostics_after,
-                    response_count,
-                    token_usage: thread.cumulative_token_usage(),
+                    token_usage: thread.latest_request_token_usage().unwrap(),
                     tool_metrics: example_cx.tool_metrics.lock().unwrap().clone(),
-                    all_messages: messages_to_markdown(thread.messages()),
+                    thread_markdown: thread.to_markdown(),
                     programmatic_assertions: example_cx.assertions,
                 }
             })
         })
     }
 
-    async fn repository_diff(&self) -> Result<String> {
-        let worktree_path = self.worktree_path();
-        run_git(&worktree_path, &["add", "."]).await?;
+    async fn repository_diff(repository_path: PathBuf, repository_url: &str) -> Result<String> {
+        run_git(&repository_path, &["add", "."]).await?;
         let mut diff_args = vec!["diff", "--staged"];
-        if self.thread.meta().url == ZED_REPO_URL {
+        if repository_url == ZED_REPO_URL {
             diff_args.push(":(exclude).rules");
         }
-        run_git(&worktree_path, &diff_args).await
+        run_git(&repository_path, &diff_args).await
     }
 
     pub async fn judge(
@@ -543,7 +511,7 @@ impl ExampleInstance {
         hbs.register_template_string(judge_thread_prompt_name, judge_thread_prompt)
             .unwrap();
 
-        let complete_messages = &run_output.all_messages;
+        let complete_messages = &run_output.thread_markdown;
         let to_prompt = |assertion: String| {
             hbs.render(
                 judge_thread_prompt_name,
@@ -635,6 +603,273 @@ impl ExampleInstance {
     }
 }
 
+struct EvalThreadEnvironment {
+    project: Entity<Project>,
+}
+
+struct EvalTerminalHandle {
+    terminal: Entity<acp_thread::Terminal>,
+}
+
+impl agent::TerminalHandle for EvalTerminalHandle {
+    fn id(&self, cx: &AsyncApp) -> Result<acp::TerminalId> {
+        self.terminal.read_with(cx, |term, _cx| term.id().clone())
+    }
+
+    fn wait_for_exit(&self, cx: &AsyncApp) -> Result<Shared<Task<acp::TerminalExitStatus>>> {
+        self.terminal
+            .read_with(cx, |term, _cx| term.wait_for_exit())
+    }
+
+    fn current_output(&self, cx: &AsyncApp) -> Result<acp::TerminalOutputResponse> {
+        self.terminal
+            .read_with(cx, |term, cx| term.current_output(cx))
+    }
+}
+
+impl agent::ThreadEnvironment for EvalThreadEnvironment {
+    fn create_terminal(
+        &self,
+        command: String,
+        cwd: Option<PathBuf>,
+        output_byte_limit: Option<u64>,
+        cx: &mut AsyncApp,
+    ) -> Task<Result<Rc<dyn agent::TerminalHandle>>> {
+        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(uuid::Uuid::new_v4().to_string().into());
+            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<dyn agent::TerminalHandle>)
+        })
+    }
+}
+
+struct LanguageModelInterceptor {
+    model: Arc<dyn LanguageModel>,
+    request_count: Arc<Mutex<usize>>,
+    previous_diff: Arc<Mutex<String>>,
+    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<dyn LanguageModel>,
+        example_output_dir: PathBuf,
+        last_diff_file_path: PathBuf,
+        messages_json_file_path: PathBuf,
+        repository_path: PathBuf,
+        repository_url: String,
+    ) -> Arc<Self> {
+        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<u64>> {
+        self.model.count_tokens(request, cx)
+    }
+
+    fn stream_completion(
+        &self,
+        request: LanguageModelRequest,
+        cx: &AsyncApp,
+    ) -> future::BoxFuture<
+        'static,
+        Result<
+            futures::stream::BoxStream<
+                'static,
+                Result<LanguageModelCompletionEvent, language_model::LanguageModelCompletionError>,
+            >,
+            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<Project>,
     buffer: &Entity<Buffer>,
@@ -826,40 +1061,6 @@ pub async fn run_git(repo_path: &Path, args: &[&str]) -> Result<String> {
     Ok(String::from_utf8(output.stdout)?.trim().to_string())
 }
 
-fn messages_to_markdown<'a>(message_iter: impl IntoIterator<Item = &'a Message>) -> String {
-    let mut messages = String::new();
-    let mut assistant_message_number: u32 = 1;
-
-    for message in message_iter {
-        push_role(&message.role, &mut messages, &mut assistant_message_number);
-
-        for segment in &message.segments {
-            match segment {
-                MessageSegment::Text(text) => {
-                    messages.push_str(text);
-                    messages.push_str("\n\n");
-                }
-                MessageSegment::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");
-                }
-                MessageSegment::RedactedThinking(items) => {
-                    messages.push_str(&format!(
-                        "**Redacted Thinking**: {} item(s)\n\n",
-                        items.len()
-                    ));
-                }
-            }
-        }
-    }
-
-    messages
-}
-
 fn push_role(role: &Role, buf: &mut String, assistant_message_number: &mut u32) {
     match role {
         Role::System => buf.push_str("# ⚙️ SYSTEM\n\n"),

crates/extension/Cargo.toml 🔗

@@ -36,7 +36,6 @@ url.workspace = true
 util.workspace = true
 wasm-encoder.workspace = true
 wasmparser.workspace = true
-workspace-hack.workspace = true
 
 [dev-dependencies]
 pretty_assertions.workspace = true

crates/extension_cli/Cargo.toml 🔗

@@ -30,4 +30,3 @@ tokio = { workspace = true, features = ["full"] }
 toml.workspace = true
 tree-sitter.workspace = true
 wasmtime.workspace = true
-workspace-hack.workspace = true

crates/extension_host/Cargo.toml 🔗

@@ -27,6 +27,7 @@ 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
@@ -51,7 +52,6 @@ util.workspace = true
 wasmparser.workspace = true
 wasmtime-wasi.workspace = true
 wasmtime.workspace = true
-workspace-hack.workspace = true
 
 [dev-dependencies]
 criterion.workspace = true

crates/extension_host/benches/extension_compilation_benchmark.rs 🔗

@@ -19,6 +19,7 @@ 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");
 
@@ -37,7 +38,7 @@ fn extension_benchmarks(c: &mut Criterion) {
             |wasm_bytes| {
                 let _extension = cx
                     .executor()
-                    .block(wasm_host.load_extension(wasm_bytes, &manifest, cx.executor()))
+                    .block(wasm_host.load_extension(wasm_bytes, &manifest, &cx.to_async()))
                     .unwrap();
             },
             BatchSize::SmallInput,

crates/extension_host/src/wasm_host.rs 🔗

@@ -591,11 +591,12 @@ impl WasmHost {
         self: &Arc<Self>,
         wasm_bytes: Vec<u8>,
         manifest: &Arc<ExtensionManifest>,
-        executor: BackgroundExecutor,
+        cx: &AsyncApp,
     ) -> Task<Result<WasmExtension>> {
         let this = self.clone();
         let manifest = manifest.clone();
-        executor.clone().spawn(async move {
+        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)
@@ -632,20 +633,29 @@ impl WasmHost {
                 .context("failed to initialize wasm extension")?;
 
             let (tx, mut rx) = mpsc::unbounded::<ExtensionCall>();
-            executor
-                .spawn(async move {
-                    while let Some(call) = rx.next().await {
-                        (call)(&mut extension, &mut store).await;
-                    }
-                })
-                .detach();
+            let extension_task = async move {
+                while let Some(call) = rx.next().await {
+                    (call)(&mut extension, &mut store).await;
+                }
+            };
 
-            Ok(WasmExtension {
-                manifest: manifest.clone(),
-                work_dir: this.work_dir.join(manifest.id.as_ref()).into(),
-                tx,
-                zed_api_version,
-            })
+            anyhow::Ok((
+                extension_task,
+                WasmExtension {
+                    manifest: manifest.clone(),
+                    work_dir: this.work_dir.join(manifest.id.as_ref()).into(),
+                    tx,
+                    zed_api_version,
+                },
+            ))
+        };
+        cx.spawn(async move |cx| {
+            let (extension_task, extension) = load_extension_task.await?;
+            // we need to run run the task in an extension context as wasmtime_wasi may
+            // call into tokio, accessing its runtime handle
+            gpui_tokio::Tokio::spawn(cx, extension_task)?.detach();
+
+            Ok(extension)
         })
     }
 
@@ -747,7 +757,7 @@ impl WasmExtension {
             .context("failed to read wasm")?;
 
         wasm_host
-            .load_extension(wasm_bytes, manifest, cx.background_executor().clone())
+            .load_extension(wasm_bytes, manifest, cx)
             .await
             .with_context(|| format!("failed to load wasm extension {}", manifest.id))
     }

crates/extension_host/src/wasm_host/wit/since_v0_1_0.rs 🔗

@@ -520,7 +520,7 @@ impl ExtensionImports for WasmState {
             anyhow::ensure!(
                 response.status().is_success(),
                 "download failed with status {}",
-                response.status().to_string()
+                response.status()
             );
             let body = BufReader::new(response.body_mut());
 

crates/extension_host/src/wasm_host/wit/since_v0_6_0.rs 🔗

@@ -1051,7 +1051,7 @@ impl ExtensionImports for WasmState {
             anyhow::ensure!(
                 response.status().is_success(),
                 "download failed with status {}",
-                response.status().to_string()
+                response.status()
             );
             let body = BufReader::new(response.body_mut());
 

crates/extensions_ui/Cargo.toml 🔗

@@ -38,7 +38,6 @@ theme.workspace = true
 ui.workspace = true
 util.workspace = true
 vim_mode_setting.workspace = true
-workspace-hack.workspace = true
 workspace.workspace = true
 zed_actions.workspace = true
 

crates/extensions_ui/src/components/extension_card.rs 🔗

@@ -32,14 +32,14 @@ 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(7.))
+                .h(rems_from_px(110.))
                 .p_3()
-                .mt_4()
                 .gap_2()
-                .bg(cx.theme().colors().elevated_surface_background)
+                .bg(cx.theme().colors().elevated_surface_background.opacity(0.5))
                 .border_1()
-                .border_color(cx.theme().colors().border)
+                .border_color(cx.theme().colors().border_variant)
                 .rounded_md()
                 .children(self.children)
                 .when(self.overridden_by_dev_extension, |card| {
@@ -51,7 +51,6 @@ impl RenderOnce for ExtensionCard {
                             .block_mouse_except_scroll()
                             .cursor_default()
                             .size_full()
-                            .items_center()
                             .justify_center()
                             .bg(cx.theme().colors().elevated_surface_background.alpha(0.8))
                             .child(Label::new("Overridden by dev extension.")),

crates/extensions_ui/src/extensions_ui.rs 🔗

@@ -13,8 +13,8 @@ use editor::{Editor, EditorElement, EditorStyle};
 use extension_host::{ExtensionManifest, ExtensionOperation, ExtensionStore};
 use fuzzy::{StringMatchCandidate, match_strings};
 use gpui::{
-    Action, App, ClipboardItem, Context, Entity, EventEmitter, Flatten, Focusable,
-    InteractiveElement, KeyContext, ParentElement, Render, Styled, Task, TextStyle,
+    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};
@@ -727,7 +727,7 @@ impl ExtensionsPage {
                             .gap_2()
                             .child(
                                 Headline::new(extension.manifest.name.clone())
-                                    .size(HeadlineSize::Medium),
+                                    .size(HeadlineSize::Small),
                             )
                             .child(Headline::new(format!("v{version}")).size(HeadlineSize::XSmall))
                             .children(
@@ -777,20 +777,12 @@ impl ExtensionsPage {
                 h_flex()
                     .gap_2()
                     .justify_between()
-                    .child(
-                        Label::new(format!(
-                            "{}: {}",
-                            if extension.manifest.authors.len() > 1 {
-                                "Authors"
-                            } else {
-                                "Author"
-                            },
-                            extension.manifest.authors.join(", ")
-                        ))
-                        .size(LabelSize::Small)
-                        .color(Color::Muted)
-                        .truncate(),
-                    )
+                    .children(extension.manifest.description.as_ref().map(|description| {
+                        Label::new(description.clone())
+                            .size(LabelSize::Small)
+                            .color(Color::Default)
+                            .truncate()
+                    }))
                     .child(
                         Label::new(format!(
                             "Downloads: {}",
@@ -803,21 +795,29 @@ impl ExtensionsPage {
                 h_flex()
                     .gap_2()
                     .justify_between()
-                    .children(extension.manifest.description.as_ref().map(|description| {
-                        Label::new(description.clone())
-                            .size(LabelSize::Small)
-                            .color(Color::Default)
-                            .truncate()
-                    }))
                     .child(
                         h_flex()
-                            .gap_2()
+                            .gap_1()
+                            .child(
+                                Icon::new(IconName::Person)
+                                    .size(IconSize::XSmall)
+                                    .color(Color::Muted),
+                            )
+                            .child(
+                                Label::new(extension.manifest.authors.join(", "))
+                                    .size(LabelSize::Small)
+                                    .color(Color::Muted)
+                                    .truncate(),
+                            ),
+                    )
+                    .child(
+                        h_flex()
+                            .gap_1()
                             .child(
                                 IconButton::new(
                                     SharedString::from(format!("repository-{}", extension.id)),
                                     IconName::Github,
                                 )
-                                .icon_color(Color::Accent)
                                 .icon_size(IconSize::Small)
                                 .on_click(cx.listener({
                                     let repository_url = repository_url.clone();
@@ -837,9 +837,13 @@ impl ExtensionsPage {
                                         SharedString::from(format!("more-{}", extension.id)),
                                         IconName::Ellipsis,
                                     )
-                                    .icon_color(Color::Accent)
                                     .icon_size(IconSize::Small),
                                 )
+                                .anchor(Corner::TopRight)
+                                .offset(Point {
+                                    x: px(0.0),
+                                    y: px(2.0),
+                                })
                                 .menu(move |window, cx| {
                                     Some(Self::render_remote_extension_context_menu(
                                         &this,
@@ -961,6 +965,11 @@ impl ExtensionsPage {
                     SharedString::from(extension.id.clone()),
                     "Install",
                 )
+                .style(ButtonStyle::Tinted(ui::TintColor::Accent))
+                .icon(IconName::Download)
+                .icon_size(IconSize::Small)
+                .icon_color(Color::Muted)
+                .icon_position(IconPosition::Start)
                 .on_click({
                     let extension_id = extension.id.clone();
                     move |_, _, cx| {
@@ -978,6 +987,11 @@ impl ExtensionsPage {
                     SharedString::from(extension.id.clone()),
                     "Install",
                 )
+                .style(ButtonStyle::Tinted(ui::TintColor::Accent))
+                .icon(IconName::Download)
+                .icon_size(IconSize::Small)
+                .icon_color(Color::Muted)
+                .icon_position(IconPosition::Start)
                 .disabled(true),
                 configure: None,
                 upgrade: None,
@@ -987,6 +1001,7 @@ impl ExtensionsPage {
                     SharedString::from(extension.id.clone()),
                     "Uninstall",
                 )
+                .style(ButtonStyle::OutlinedGhost)
                 .disabled(true),
                 configure: is_configurable.then(|| {
                     Button::new(
@@ -1004,6 +1019,7 @@ impl ExtensionsPage {
                     SharedString::from(extension.id.clone()),
                     "Uninstall",
                 )
+                .style(ButtonStyle::OutlinedGhost)
                 .on_click({
                     let extension_id = extension.id.clone();
                     move |_, _, cx| {
@@ -1020,6 +1036,7 @@ impl ExtensionsPage {
                         SharedString::from(format!("configure-{}", extension.id)),
                         "Configure",
                     )
+                    .style(ButtonStyle::OutlinedGhost)
                     .on_click({
                         let extension_id = extension.id.clone();
                         move |_, _, cx| {
@@ -1044,6 +1061,7 @@ impl ExtensionsPage {
                 } else {
                     Some(
                         Button::new(SharedString::from(extension.id.clone()), "Upgrade")
+                          .style(ButtonStyle::Tinted(ui::TintColor::Accent))
                             .when(!is_compatible, |upgrade_button| {
                                 upgrade_button.disabled(true).tooltip({
                                     let version = extension.manifest.version.clone();
@@ -1082,6 +1100,7 @@ impl ExtensionsPage {
                     SharedString::from(extension.id.clone()),
                     "Uninstall",
                 )
+                .style(ButtonStyle::OutlinedGhost)
                 .disabled(true),
                 configure: is_configurable.then(|| {
                     Button::new(

crates/feature_flags/Cargo.toml 🔗

@@ -15,4 +15,3 @@ path = "src/feature_flags.rs"
 futures.workspace = true
 gpui.workspace = true
 smol.workspace = true
-workspace-hack.workspace = true

crates/feedback/Cargo.toml 🔗

@@ -19,7 +19,6 @@ gpui.workspace = true
 system_specs.workspace = true
 urlencoding.workspace = true
 util.workspace = true
-workspace-hack.workspace = true
 workspace.workspace = true
 zed_actions.workspace = true
 

crates/file_finder/Cargo.toml 🔗

@@ -32,7 +32,6 @@ theme.workspace = true
 ui.workspace = true
 util.workspace = true
 workspace.workspace = true
-workspace-hack.workspace = true
 
 [dev-dependencies]
 ctor.workspace = true

crates/file_finder/src/file_finder.rs 🔗

@@ -21,7 +21,9 @@ use gpui::{
 };
 use open_path_prompt::OpenPathPrompt;
 use picker::{Picker, PickerDelegate};
-use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId};
+use project::{
+    PathMatchCandidateSet, Project, ProjectPath, WorktreeId, worktree_store::WorktreeStore,
+};
 use search::ToggleIncludeIgnored;
 use settings::Settings;
 use std::{
@@ -538,11 +540,14 @@ impl Matches {
 
     fn push_new_matches<'a>(
         &'a mut self,
+        worktree_store: Entity<WorktreeStore>,
+        cx: &'a App,
         history_items: impl IntoIterator<Item = &'a FoundPath> + Clone,
         currently_opened: Option<&'a FoundPath>,
         query: Option<&FileSearchQuery>,
         new_search_matches: impl Iterator<Item = ProjectPanelOrdMatch>,
         extend_old_matches: bool,
+        path_style: PathStyle,
     ) {
         let Some(query) = query else {
             // assuming that if there's no query, then there's no search matches.
@@ -556,8 +561,25 @@ impl Matches {
                 .extend(history_items.into_iter().map(path_to_entry));
             return;
         };
-
-        let new_history_matches = matching_history_items(history_items, currently_opened, query);
+        // If several worktress are open we have to set the worktree root names in path prefix
+        let several_worktrees = worktree_store.read(cx).worktrees().count() > 1;
+        let worktree_name_by_id = several_worktrees.then(|| {
+            worktree_store
+                .read(cx)
+                .worktrees()
+                .map(|worktree| {
+                    let snapshot = worktree.read(cx).snapshot();
+                    (snapshot.id(), snapshot.root_name().into())
+                })
+                .collect()
+        });
+        let new_history_matches = matching_history_items(
+            history_items,
+            currently_opened,
+            worktree_name_by_id,
+            query,
+            path_style,
+        );
         let new_search_matches: Vec<Match> = new_search_matches
             .filter(|path_match| {
                 !new_history_matches.contains_key(&ProjectPath {
@@ -694,7 +716,9 @@ impl Matches {
 fn matching_history_items<'a>(
     history_items: impl IntoIterator<Item = &'a FoundPath>,
     currently_opened: Option<&'a FoundPath>,
+    worktree_name_by_id: Option<HashMap<WorktreeId, Arc<RelPath>>>,
     query: &FileSearchQuery,
+    path_style: PathStyle,
 ) -> HashMap<ProjectPath, Match> {
     let mut candidates_paths = HashMap::default();
 
@@ -734,13 +758,18 @@ fn matching_history_items<'a>(
     let mut matching_history_paths = HashMap::default();
     for (worktree, candidates) in history_items_by_worktrees {
         let max_results = candidates.len() + 1;
+        let worktree_root_name = worktree_name_by_id
+            .as_ref()
+            .and_then(|w| w.get(&worktree).cloned());
         matching_history_paths.extend(
             fuzzy::match_fixed_path_set(
                 candidates,
                 worktree.to_usize(),
+                worktree_root_name,
                 query.path_query(),
                 false,
                 max_results,
+                path_style,
             )
             .into_iter()
             .filter_map(|path_match| {
@@ -937,15 +966,18 @@ impl FileFinderDelegate {
                 self.matches.get(self.selected_index).cloned()
             };
 
+            let path_style = self.project.read(cx).path_style(cx);
             self.matches.push_new_matches(
+                self.project.read(cx).worktree_store(),
+                cx,
                 &self.history_items,
                 self.currently_opened_path.as_ref(),
                 Some(&query),
                 matches.into_iter(),
                 extend_old_matches,
+                path_style,
             );
 
-            let path_style = self.project.read(cx).path_style(cx);
             let query_path = query.raw_query.as_str();
             if let Ok(mut query_path) = RelPath::new(Path::new(query_path), path_style) {
                 let available_worktree = self
@@ -1365,7 +1397,11 @@ impl PickerDelegate for FileFinderDelegate {
                     separate_history: self.separate_history,
                     ..Matches::default()
                 };
+                let path_style = self.project.read(cx).path_style(cx);
+
                 self.matches.push_new_matches(
+                    project.worktree_store(),
+                    cx,
                     self.history_items.iter().filter(|history_item| {
                         project
                             .worktree_for_id(history_item.project.worktree_id, cx)
@@ -1377,6 +1413,7 @@ impl PickerDelegate for FileFinderDelegate {
                     None,
                     None.into_iter(),
                     false,
+                    path_style,
                 );
 
                 self.first_update = false;
@@ -1626,11 +1663,7 @@ impl PickerDelegate for FileFinderDelegate {
         )
     }
 
-    fn render_footer(
-        &self,
-        window: &mut Window,
-        cx: &mut Context<Picker<Self>>,
-    ) -> Option<AnyElement> {
+    fn render_footer(&self, _: &mut Window, cx: &mut Context<Picker<Self>>) -> Option<AnyElement> {
         let focus_handle = self.focus_handle.clone();
 
         Some(
@@ -1659,12 +1692,11 @@ impl PickerDelegate for FileFinderDelegate {
                                 }),
                             {
                                 let focus_handle = focus_handle.clone();
-                                move |window, cx| {
+                                move |_window, cx| {
                                     Tooltip::for_action_in(
                                         "Filter Options",
                                         &ToggleFilterMenu,
                                         &focus_handle,
-                                        window,
                                         cx,
                                     )
                                 }
@@ -1714,14 +1746,13 @@ impl PickerDelegate for FileFinderDelegate {
                                     ButtonLike::new("split-trigger")
                                         .child(Label::new("Split…"))
                                         .selected_style(ButtonStyle::Tinted(TintColor::Accent))
-                                        .children(
+                                        .child(
                                             KeyBinding::for_action_in(
                                                 &ToggleSplitMenu,
                                                 &focus_handle,
-                                                window,
                                                 cx,
                                             )
-                                            .map(|kb| kb.size(rems_from_px(12.))),
+                                            .size(rems_from_px(12.)),
                                         ),
                                 )
                                 .menu({
@@ -1753,13 +1784,8 @@ impl PickerDelegate for FileFinderDelegate {
                         .child(
                             Button::new("open-selection", "Open")
                                 .key_binding(
-                                    KeyBinding::for_action_in(
-                                        &menu::Confirm,
-                                        &focus_handle,
-                                        window,
-                                        cx,
-                                    )
-                                    .map(|kb| kb.size(rems_from_px(12.))),
+                                    KeyBinding::for_action_in(&menu::Confirm, &focus_handle, cx)
+                                        .map(|kb| kb.size(rems_from_px(12.))),
                                 )
                                 .on_click(|_, window, cx| {
                                     window.dispatch_action(menu::Confirm.boxed_clone(), cx)

crates/file_finder/src/file_finder_settings.rs 🔗

@@ -18,7 +18,11 @@ impl Settings for FileFinderSettings {
             file_icons: file_finder.file_icons.unwrap(),
             modal_max_width: file_finder.modal_max_width.unwrap().into(),
             skip_focus_for_active_in_search: file_finder.skip_focus_for_active_in_search.unwrap(),
-            include_ignored: file_finder.include_ignored,
+            include_ignored: match file_finder.include_ignored.unwrap() {
+                settings::IncludeIgnoredContent::All => Some(true),
+                settings::IncludeIgnoredContent::Indexed => Some(false),
+                settings::IncludeIgnoredContent::Smart => None,
+            },
         }
     }
 }

crates/file_finder/src/file_finder_tests.rs 🔗

@@ -490,7 +490,7 @@ async fn test_row_column_numbers_query_inside_file(cx: &mut TestAppContext) {
     cx.executor().advance_clock(Duration::from_secs(2));
 
     editor.update(cx, |editor, cx| {
-            let all_selections = editor.selections.all_adjusted(cx);
+            let all_selections = editor.selections.all_adjusted(&editor.display_snapshot(cx));
             assert_eq!(
                 all_selections.len(),
                 1,
@@ -565,7 +565,7 @@ async fn test_row_column_numbers_query_outside_file(cx: &mut TestAppContext) {
     cx.executor().advance_clock(Duration::from_secs(2));
 
     editor.update(cx, |editor, cx| {
-            let all_selections = editor.selections.all_adjusted(cx);
+            let all_selections = editor.selections.all_adjusted(&editor.display_snapshot(cx));
             assert_eq!(
                 all_selections.len(),
                 1,
@@ -2503,6 +2503,147 @@ async fn test_search_results_refreshed_on_adding_and_removing_worktrees(
     });
 }
 
+#[gpui::test]
+async fn test_history_items_uniqueness_for_multiple_worktree_open_all_files(
+    cx: &mut TestAppContext,
+) {
+    let app_state = init_test(cx);
+    app_state
+        .fs
+        .as_fake()
+        .insert_tree(
+            path!("/repo1"),
+            json!({
+                "package.json": r#"{"name": "repo1"}"#,
+                "src": {
+                    "index.js": "// Repo 1 index",
+                }
+            }),
+        )
+        .await;
+
+    app_state
+        .fs
+        .as_fake()
+        .insert_tree(
+            path!("/repo2"),
+            json!({
+                "package.json": r#"{"name": "repo2"}"#,
+                "src": {
+                    "index.js": "// Repo 2 index",
+                }
+            }),
+        )
+        .await;
+
+    let project = Project::test(
+        app_state.fs.clone(),
+        [path!("/repo1").as_ref(), path!("/repo2").as_ref()],
+        cx,
+    )
+    .await;
+
+    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
+    let (worktree_id1, worktree_id2) = cx.read(|cx| {
+        let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
+        (worktrees[0].read(cx).id(), worktrees[1].read(cx).id())
+    });
+
+    workspace
+        .update_in(cx, |workspace, window, cx| {
+            workspace.open_path(
+                ProjectPath {
+                    worktree_id: worktree_id1,
+                    path: rel_path("package.json").into(),
+                },
+                None,
+                true,
+                window,
+                cx,
+            )
+        })
+        .await
+        .unwrap();
+
+    cx.dispatch_action(workspace::CloseActiveItem {
+        save_intent: None,
+        close_pinned: false,
+    });
+    workspace
+        .update_in(cx, |workspace, window, cx| {
+            workspace.open_path(
+                ProjectPath {
+                    worktree_id: worktree_id2,
+                    path: rel_path("package.json").into(),
+                },
+                None,
+                true,
+                window,
+                cx,
+            )
+        })
+        .await
+        .unwrap();
+
+    cx.dispatch_action(workspace::CloseActiveItem {
+        save_intent: None,
+        close_pinned: false,
+    });
+
+    let picker = open_file_picker(&workspace, cx);
+    cx.simulate_input("package.json");
+
+    picker.update(cx, |finder, _| {
+        let matches = &finder.delegate.matches.matches;
+
+        assert_eq!(
+            matches.len(),
+            2,
+            "Expected 1 history match + 1 search matches, but got {} matches: {:?}",
+            matches.len(),
+            matches
+        );
+
+        assert_matches!(matches[0], Match::History { .. });
+
+        let search_matches = collect_search_matches(finder);
+        assert_eq!(
+            search_matches.history.len(),
+            2,
+            "Should have exactly 2 history match"
+        );
+        assert_eq!(
+            search_matches.search.len(),
+            0,
+            "Should have exactly 0 search match (because we already opened the 2 package.json)"
+        );
+
+        if let Match::History { path, panel_match } = &matches[0] {
+            assert_eq!(path.project.worktree_id, worktree_id2);
+            assert_eq!(path.project.path.as_ref(), rel_path("package.json"));
+            let panel_match = panel_match.as_ref().unwrap();
+            assert_eq!(panel_match.0.path_prefix, rel_path("repo2").into());
+            assert_eq!(panel_match.0.path, rel_path("package.json").into());
+            assert_eq!(
+                panel_match.0.positions,
+                vec![6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]
+            );
+        }
+
+        if let Match::History { path, panel_match } = &matches[1] {
+            assert_eq!(path.project.worktree_id, worktree_id1);
+            assert_eq!(path.project.path.as_ref(), rel_path("package.json"));
+            let panel_match = panel_match.as_ref().unwrap();
+            assert_eq!(panel_match.0.path_prefix, rel_path("repo1").into());
+            assert_eq!(panel_match.0.path, rel_path("package.json").into());
+            assert_eq!(
+                panel_match.0.positions,
+                vec![6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]
+            );
+        }
+    });
+}
+
 #[gpui::test]
 async fn test_selected_match_stays_selected_after_matches_refreshed(cx: &mut gpui::TestAppContext) {
     let app_state = init_test(cx);

crates/file_icons/Cargo.toml 🔗

@@ -17,4 +17,3 @@ gpui.workspace = true
 serde.workspace = true
 theme.workspace = true
 util.workspace = true
-workspace-hack.workspace = true

crates/fs/Cargo.toml 🔗

@@ -33,7 +33,6 @@ tempfile.workspace = true
 text.workspace = true
 time.workspace = true
 util.workspace = true
-workspace-hack.workspace = true
 
 [target.'cfg(target_os = "macos")'.dependencies]
 fsevent.workspace = true

crates/fs/src/fake_git_repo.rs 🔗

@@ -11,14 +11,20 @@ use git::{
     },
     status::{FileStatus, GitStatus, StatusCode, TrackedStatus, UnmergedStatus},
 };
-use gpui::{AsyncApp, BackgroundExecutor, SharedString, Task};
+use gpui::{AsyncApp, BackgroundExecutor, SharedString, Task, TaskLabel};
 use ignore::gitignore::GitignoreBuilder;
 use parking_lot::Mutex;
 use rope::Rope;
 use smol::future::FutureExt as _;
-use std::{path::PathBuf, sync::Arc};
+use std::{
+    path::PathBuf,
+    sync::{Arc, LazyLock},
+};
 use util::{paths::PathStyle, rel_path::RelPath};
 
+pub static LOAD_INDEX_TEXT_TASK: LazyLock<TaskLabel> = LazyLock::new(TaskLabel::new);
+pub static LOAD_HEAD_TEXT_TASK: LazyLock<TaskLabel> = LazyLock::new(TaskLabel::new);
+
 #[derive(Clone)]
 pub struct FakeGitRepository {
     pub(crate) fs: Arc<FakeFs>,
@@ -79,33 +85,29 @@ impl GitRepository for FakeGitRepository {
     fn reload_index(&self) {}
 
     fn load_index_text(&self, path: RepoPath) -> BoxFuture<'_, Option<String>> {
-        async {
-            self.with_state_async(false, move |state| {
-                state
-                    .index_contents
-                    .get(&path)
-                    .context("not present in index")
-                    .cloned()
-            })
-            .await
-            .ok()
-        }
-        .boxed()
+        let fut = self.with_state_async(false, move |state| {
+            state
+                .index_contents
+                .get(&path)
+                .context("not present in index")
+                .cloned()
+        });
+        self.executor
+            .spawn_labeled(*LOAD_INDEX_TEXT_TASK, async move { fut.await.ok() })
+            .boxed()
     }
 
     fn load_committed_text(&self, path: RepoPath) -> BoxFuture<'_, Option<String>> {
-        async {
-            self.with_state_async(false, move |state| {
-                state
-                    .head_contents
-                    .get(&path)
-                    .context("not present in HEAD")
-                    .cloned()
-            })
-            .await
-            .ok()
-        }
-        .boxed()
+        let fut = self.with_state_async(false, move |state| {
+            state
+                .head_contents
+                .get(&path)
+                .context("not present in HEAD")
+                .cloned()
+        });
+        self.executor
+            .spawn_labeled(*LOAD_HEAD_TEXT_TASK, async move { fut.await.ok() })
+            .boxed()
     }
 
     fn load_commit(

crates/fs/src/fs.rs 🔗

@@ -58,6 +58,9 @@ use smol::io::AsyncReadExt;
 #[cfg(any(test, feature = "test-support"))]
 use std::ffi::OsStr;
 
+#[cfg(any(test, feature = "test-support"))]
+pub use fake_git_repo::{LOAD_HEAD_TEXT_TASK, LOAD_INDEX_TEXT_TASK};
+
 pub trait Watcher: Send + Sync {
     fn add(&self, path: &Path) -> Result<()>;
     fn remove(&self, path: &Path) -> Result<()>;
@@ -321,7 +324,33 @@ impl FileHandle for std::fs::File {
 
     #[cfg(target_os = "windows")]
     fn current_path(&self, _: &Arc<dyn Fs>) -> Result<PathBuf> {
-        anyhow::bail!("unimplemented")
+        use std::ffi::OsString;
+        use std::os::windows::ffi::OsStringExt;
+        use std::os::windows::io::AsRawHandle;
+
+        use windows::Win32::Foundation::HANDLE;
+        use windows::Win32::Storage::FileSystem::{
+            FILE_NAME_NORMALIZED, GetFinalPathNameByHandleW,
+        };
+
+        let handle = HANDLE(self.as_raw_handle() as _);
+
+        // Query required buffer size (in wide chars)
+        let required_len =
+            unsafe { GetFinalPathNameByHandleW(handle, &mut [], FILE_NAME_NORMALIZED) };
+        if required_len == 0 {
+            anyhow::bail!("GetFinalPathNameByHandleW returned 0 length");
+        }
+
+        // Allocate buffer and retrieve the path
+        let mut buf: Vec<u16> = vec![0u16; required_len as usize + 1];
+        let written = unsafe { GetFinalPathNameByHandleW(handle, &mut buf, FILE_NAME_NORMALIZED) };
+        if written == 0 {
+            anyhow::bail!("GetFinalPathNameByHandleW failed to write path");
+        }
+
+        let os_str: OsString = OsString::from_wide(&buf[..written as usize]);
+        Ok(PathBuf::from(os_str))
     }
 }
 
@@ -558,7 +587,14 @@ impl Fs for RealFs {
     }
 
     async fn open_handle(&self, path: &Path) -> Result<Arc<dyn FileHandle>> {
-        Ok(Arc::new(std::fs::File::open(path)?))
+        let mut options = std::fs::OpenOptions::new();
+        options.read(true);
+        #[cfg(windows)]
+        {
+            use std::os::windows::fs::OpenOptionsExt;
+            options.custom_flags(windows::Win32::Storage::FileSystem::FILE_FLAG_BACKUP_SEMANTICS.0);
+        }
+        Ok(Arc::new(options.open(path)?))
     }
 
     async fn load(&self, path: &Path) -> Result<String> {

crates/fs_benchmarks/Cargo.toml 🔗

@@ -7,7 +7,6 @@ edition.workspace = true
 [dependencies]
 fs.workspace = true
 gpui = {workspace = true, features = ["windows-manifest"]}
-workspace-hack.workspace = true
 
 [lints]
 workspace = true

crates/fsevent/Cargo.toml 🔗

@@ -16,7 +16,6 @@ doctest = false
 bitflags.workspace = true
 parking_lot.workspace = true
 log.workspace = true
-workspace-hack.workspace = true
 
 [target.'cfg(target_os = "macos")'.dependencies]
 core-foundation.workspace = true

crates/fuzzy/Cargo.toml 🔗

@@ -16,7 +16,6 @@ doctest = false
 gpui.workspace = true
 util.workspace = true
 log.workspace = true
-workspace-hack.workspace = true
 
 [dev-dependencies]
 util = {workspace = true, features = ["test-support"]}

crates/fuzzy/src/paths.rs 🔗

@@ -88,9 +88,11 @@ impl Ord for PathMatch {
 pub fn match_fixed_path_set(
     candidates: Vec<PathMatchCandidate>,
     worktree_id: usize,
+    worktree_root_name: Option<Arc<RelPath>>,
     query: &str,
     smart_case: bool,
     max_results: usize,
+    path_style: PathStyle,
 ) -> Vec<PathMatch> {
     let lowercase_query = query.to_lowercase().chars().collect::<Vec<_>>();
     let query = query.chars().collect::<Vec<_>>();
@@ -98,10 +100,31 @@ pub fn match_fixed_path_set(
 
     let mut matcher = Matcher::new(&query, &lowercase_query, query_char_bag, smart_case, true);
 
-    let mut results = Vec::new();
+    let mut results = Vec::with_capacity(candidates.len());
+    let (path_prefix, path_prefix_chars, lowercase_prefix) = match worktree_root_name {
+        Some(worktree_root_name) => {
+            let mut path_prefix_chars = worktree_root_name
+                .display(path_style)
+                .chars()
+                .collect::<Vec<_>>();
+            path_prefix_chars.extend(path_style.separator().chars());
+            let lowercase_pfx = path_prefix_chars
+                .iter()
+                .map(|c| c.to_ascii_lowercase())
+                .collect::<Vec<_>>();
+
+            (worktree_root_name, path_prefix_chars, lowercase_pfx)
+        }
+        None => (
+            RelPath::empty().into(),
+            Default::default(),
+            Default::default(),
+        ),
+    };
+
     matcher.match_candidates(
-        &[],
-        &[],
+        &path_prefix_chars,
+        &lowercase_prefix,
         candidates.into_iter(),
         &mut results,
         &AtomicBool::new(false),
@@ -111,7 +134,7 @@ pub fn match_fixed_path_set(
             positions: positions.clone(),
             is_dir: candidate.is_dir,
             path: candidate.path.into(),
-            path_prefix: RelPath::empty().into(),
+            path_prefix: path_prefix.clone(),
             distance_to_relative_ancestor: usize::MAX,
         },
     );

crates/git/Cargo.toml 🔗

@@ -41,7 +41,6 @@ urlencoding.workspace = true
 util.workspace = true
 uuid.workspace = true
 futures.workspace = true
-workspace-hack.workspace = true
 
 [dev-dependencies]
 pretty_assertions.workspace = true

crates/git/src/repository.rs 🔗

@@ -693,10 +693,11 @@ impl GitRepository for RealGitRepository {
                 .args([
                     "--no-optional-locks",
                     "show",
-                    "--format=%P",
+                    "--format=",
                     "-z",
                     "--no-renames",
                     "--name-status",
+                    "--first-parent",
                 ])
                 .arg(&commit)
                 .stdin(Stdio::null())
@@ -707,9 +708,8 @@ impl GitRepository for RealGitRepository {
                 .context("starting git show process")?;
 
             let show_stdout = String::from_utf8_lossy(&show_output.stdout);
-            let mut lines = show_stdout.split('\n');
-            let parent_sha = lines.next().unwrap().trim().trim_end_matches('\0');
-            let changes = parse_git_diff_name_status(lines.next().unwrap_or(""));
+            let changes = parse_git_diff_name_status(&show_stdout);
+            let parent_sha = format!("{}^", commit);
 
             let mut cat_file_process = util::command::new_smol_command(&git_binary_path)
                 .current_dir(&working_directory)

crates/git_hosting_providers/Cargo.toml 🔗

@@ -24,7 +24,6 @@ serde_json.workspace = true
 settings.workspace = true
 url.workspace = true
 util.workspace = true
-workspace-hack.workspace = true
 
 [dev-dependencies]
 indoc.workspace = true

crates/git_ui/Cargo.toml 🔗

@@ -58,7 +58,6 @@ time_format.workspace = true
 ui.workspace = true
 util.workspace = true
 watch.workspace = true
-workspace-hack.workspace = true
 workspace.workspace = true
 zed_actions.workspace = true
 zeroize.workspace = true

crates/git_ui/src/blame_ui.rs 🔗

@@ -98,25 +98,10 @@ impl BlameRenderer for GitBlameRenderer {
                             let workspace = workspace.clone();
                             move |_, window, cx| {
                                 CommitView::open(
-                                    CommitSummary {
-                                        sha: blame_entry.sha.to_string().into(),
-                                        subject: blame_entry
-                                            .summary
-                                            .clone()
-                                            .unwrap_or_default()
-                                            .into(),
-                                        commit_timestamp: blame_entry
-                                            .committer_time
-                                            .unwrap_or_default(),
-                                        author_name: blame_entry
-                                            .committer_name
-                                            .clone()
-                                            .unwrap_or_default()
-                                            .into(),
-                                        has_parent: true,
-                                    },
+                                    blame_entry.sha.to_string(),
                                     repository.downgrade(),
                                     workspace.clone(),
+                                    None,
                                     window,
                                     cx,
                                 )
@@ -335,9 +320,10 @@ impl BlameRenderer for GitBlameRenderer {
                                                 .icon_size(IconSize::Small)
                                                 .on_click(move |_, window, cx| {
                                                     CommitView::open(
-                                                        commit_summary.clone(),
+                                                        commit_summary.sha.clone().into(),
                                                         repository.downgrade(),
                                                         workspace.clone(),
+                                                        None,
                                                         window,
                                                         cx,
                                                     );
@@ -374,15 +360,10 @@ impl BlameRenderer for GitBlameRenderer {
         cx: &mut App,
     ) {
         CommitView::open(
-            CommitSummary {
-                sha: blame_entry.sha.to_string().into(),
-                subject: blame_entry.summary.clone().unwrap_or_default().into(),
-                commit_timestamp: blame_entry.committer_time.unwrap_or_default(),
-                author_name: blame_entry.committer_name.unwrap_or_default().into(),
-                has_parent: true,
-            },
+            blame_entry.sha.to_string(),
             repository.downgrade(),
             workspace,
+            None,
             window,
             cx,
         )

crates/git_ui/src/branch_picker.rs 🔗

@@ -137,13 +137,13 @@ impl BranchList {
                 })
                 .await;
 
-            this.update_in(cx, |this, window, cx| {
+            let _ = this.update_in(cx, |this, window, cx| {
                 this.picker.update(cx, |picker, cx| {
                     picker.delegate.default_branch = default_branch;
                     picker.delegate.all_branches = Some(all_branches);
                     picker.refresh(window, cx);
                 })
-            })?;
+            });
 
             anyhow::Ok(())
         })
@@ -410,37 +410,20 @@ impl PickerDelegate for BranchListDelegate {
             return;
         }
 
-        cx.spawn_in(window, {
-            let branch = entry.branch.clone();
-            async move |picker, cx| {
-                let branch_change_task = picker.update(cx, |this, cx| {
-                    let repo = this
-                        .delegate
-                        .repo
-                        .as_ref()
-                        .context("No active repository")?
-                        .clone();
-
-                    let mut cx = cx.to_async();
-
-                    anyhow::Ok(async move {
-                        repo.update(&mut cx, |repo, _| {
-                            repo.change_branch(branch.name().to_string())
-                        })?
-                        .await?
-                    })
-                })??;
-
-                branch_change_task.await?;
+        let Some(repo) = self.repo.clone() else {
+            return;
+        };
 
-                picker.update(cx, |_, cx| {
-                    cx.emit(DismissEvent);
+        let branch = entry.branch.clone();
+        cx.spawn(async move |_, cx| {
+            repo.update(cx, |repo, _| repo.change_branch(branch.name().to_string()))?
+                .await??;
 
-                    anyhow::Ok(())
-                })
-            }
+            anyhow::Ok(())
         })
         .detach_and_prompt_err("Failed to change branch", window, cx, |_, _, _| None);
+
+        cx.emit(DismissEvent);
     }
 
     fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
@@ -483,11 +466,10 @@ impl PickerDelegate for BranchListDelegate {
                         this.delegate.set_selected_index(ix, window, cx);
                         this.delegate.confirm(true, window, cx);
                     }))
-                    .tooltip(move |window, cx| {
+                    .tooltip(move |_window, cx| {
                         Tooltip::for_action(
                             format!("Create branch based off default: {default_branch}"),
                             &menu::SecondaryConfirm,
-                            window,
                             cx,
                         )
                     }),
@@ -511,8 +493,12 @@ impl PickerDelegate for BranchListDelegate {
                 )
                 .into_any_element()
         } else {
-            HighlightedLabel::new(entry.branch.name().to_owned(), entry.positions.clone())
-                .truncate()
+            h_flex()
+                .max_w_48()
+                .child(
+                    HighlightedLabel::new(entry.branch.name().to_owned(), entry.positions.clone())
+                        .truncate(),
+                )
                 .into_any_element()
         };
 

crates/git_ui/src/commit_modal.rs 🔗

@@ -327,7 +327,7 @@ impl CommitModal {
             .anchor(Corner::TopRight)
     }
 
-    pub fn render_footer(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+    pub fn render_footer(&self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
         let (
             can_commit,
             tooltip,
@@ -388,7 +388,7 @@ impl CommitModal {
             });
         let focus_handle = self.focus_handle(cx);
 
-        let close_kb_hint = ui::KeyBinding::for_action(&menu::Cancel, window, cx).map(|close_kb| {
+        let close_kb_hint = ui::KeyBinding::for_action(&menu::Cancel, cx).map(|close_kb| {
             KeybindingHint::new(close_kb, cx.theme().colors().editor_background).suffix("Cancel")
         });
 
@@ -423,7 +423,7 @@ impl CommitModal {
                     .flex_none()
                     .px_1()
                     .gap_4()
-                    .children(close_kb_hint)
+                    .child(close_kb_hint)
                     .child(SplitButton::new(
                         ui::ButtonLike::new_rounded_left(ElementId::Name(
                             format!("split-button-left-{}", commit_label).into(),
@@ -452,7 +452,7 @@ impl CommitModal {
                         .disabled(!can_commit)
                         .tooltip({
                             let focus_handle = focus_handle.clone();
-                            move |window, cx| {
+                            move |_window, cx| {
                                 if can_commit {
                                     Tooltip::with_meta_in(
                                         tooltip,
@@ -467,7 +467,6 @@ impl CommitModal {
                                             if is_signoff_enabled { " --signoff" } else { "" }
                                         ),
                                         &focus_handle.clone(),
-                                        window,
                                         cx,
                                     )
                                 } else {

crates/git_ui/src/commit_tooltip.rs 🔗

@@ -318,9 +318,10 @@ impl Render for CommitTooltip {
                                             .on_click(
                                                 move |_, window, cx| {
                                                     CommitView::open(
-                                                        commit_summary.clone(),
+                                                        commit_summary.sha.to_string(),
                                                         repo.downgrade(),
                                                         workspace.clone(),
+                                                        None,
                                                         window,
                                                         cx,
                                                     );

crates/git_ui/src/commit_view.rs 🔗

@@ -1,14 +1,15 @@
 use anyhow::{Context as _, Result};
 use buffer_diff::{BufferDiff, BufferDiffSnapshot};
 use editor::{Editor, EditorEvent, MultiBuffer, SelectionEffects, multibuffer_context_lines};
-use git::repository::{CommitDetails, CommitDiff, CommitSummary, RepoPath};
+use git::repository::{CommitDetails, CommitDiff, RepoPath};
 use gpui::{
-    AnyElement, AnyView, App, AppContext as _, AsyncApp, Context, Entity, EventEmitter,
-    FocusHandle, Focusable, IntoElement, Render, WeakEntity, Window,
+    Action, AnyElement, AnyView, App, AppContext as _, AsyncApp, AsyncWindowContext, Context,
+    Entity, EventEmitter, FocusHandle, Focusable, IntoElement, PromptLevel, Render, Task,
+    WeakEntity, Window, actions,
 };
 use language::{
     Anchor, Buffer, Capability, DiskState, File, LanguageRegistry, LineEnding, OffsetRangeExt as _,
-    Point, Rope, TextBuffer,
+    Point, ReplicaId, Rope, TextBuffer,
 };
 use multi_buffer::PathKey;
 use project::{Project, WorktreeId, git_store::Repository};
@@ -18,17 +19,42 @@ use std::{
     path::PathBuf,
     sync::Arc,
 };
-use ui::{Color, Icon, IconName, Label, LabelCommon as _, SharedString};
+use ui::{
+    Button, Color, Icon, IconName, Label, LabelCommon as _, SharedString, Tooltip, prelude::*,
+};
 use util::{ResultExt, paths::PathStyle, rel_path::RelPath, truncate_and_trailoff};
 use workspace::{
-    Item, ItemHandle as _, ItemNavHistory, ToolbarItemLocation, Workspace,
+    Item, ItemHandle, ItemNavHistory, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView,
+    Workspace,
     item::{BreadcrumbText, ItemEvent, TabContentParams},
+    notifications::NotifyTaskExt,
+    pane::SaveIntent,
     searchable::SearchableItemHandle,
 };
 
+use crate::git_panel::GitPanel;
+
+actions!(git, [ApplyCurrentStash, PopCurrentStash, DropCurrentStash,]);
+
+pub fn init(cx: &mut App) {
+    cx.observe_new(|workspace: &mut Workspace, _window, _cx| {
+        register_workspace_action(workspace, |toolbar, _: &ApplyCurrentStash, window, cx| {
+            toolbar.apply_stash(window, cx);
+        });
+        register_workspace_action(workspace, |toolbar, _: &DropCurrentStash, window, cx| {
+            toolbar.remove_stash(window, cx);
+        });
+        register_workspace_action(workspace, |toolbar, _: &PopCurrentStash, window, cx| {
+            toolbar.pop_stash(window, cx);
+        });
+    })
+    .detach();
+}
+
 pub struct CommitView {
     commit: CommitDetails,
     editor: Entity<Editor>,
+    stash: Option<usize>,
     multibuffer: Entity<MultiBuffer>,
 }
 
@@ -48,17 +74,18 @@ const FILE_NAMESPACE_SORT_PREFIX: u64 = 1;
 
 impl CommitView {
     pub fn open(
-        commit: CommitSummary,
+        commit_sha: String,
         repo: WeakEntity<Repository>,
         workspace: WeakEntity<Workspace>,
+        stash: Option<usize>,
         window: &mut Window,
         cx: &mut App,
     ) {
         let commit_diff = repo
-            .update(cx, |repo, _| repo.load_commit_diff(commit.sha.to_string()))
+            .update(cx, |repo, _| repo.load_commit_diff(commit_sha.clone()))
             .ok();
         let commit_details = repo
-            .update(cx, |repo, _| repo.show(commit.sha.to_string()))
+            .update(cx, |repo, _| repo.show(commit_sha.clone()))
             .ok();
 
         window
@@ -77,6 +104,7 @@ impl CommitView {
                                 commit_diff,
                                 repo,
                                 project.clone(),
+                                stash,
                                 window,
                                 cx,
                             )
@@ -87,7 +115,7 @@ impl CommitView {
                             let ix = pane.items().position(|item| {
                                 let commit_view = item.downcast::<CommitView>();
                                 commit_view
-                                    .is_some_and(|view| view.read(cx).commit.sha == commit.sha)
+                                    .is_some_and(|view| view.read(cx).commit.sha == commit_sha)
                             });
                             if let Some(ix) = ix {
                                 pane.activate_item(ix, true, true, window, cx);
@@ -106,6 +134,7 @@ impl CommitView {
         commit_diff: CommitDiff,
         repository: Entity<Repository>,
         project: Entity<Project>,
+        stash: Option<usize>,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) -> Self {
@@ -127,18 +156,21 @@ impl CommitView {
 
         let mut metadata_buffer_id = None;
         if let Some(worktree_id) = first_worktree_id {
+            let title = if let Some(stash) = stash {
+                format!("stash@{{{}}}", stash)
+            } else {
+                format!("commit {}", commit.sha)
+            };
             let file = Arc::new(CommitMetadataFile {
-                title: RelPath::unix(&format!("commit {}", commit.sha))
-                    .unwrap()
-                    .into(),
+                title: RelPath::unix(&title).unwrap().into(),
                 worktree_id,
             });
             let buffer = cx.new(|cx| {
                 let buffer = TextBuffer::new_normalized(
-                    0,
+                    ReplicaId::LOCAL,
                     cx.entity_id().as_non_zero_u64().into(),
                     LineEnding::default(),
-                    format_commit(&commit).into(),
+                    format_commit(&commit, stash.is_some()).into(),
                 );
                 metadata_buffer_id = Some(buffer.remote_id());
                 Buffer::build(buffer, Some(file.clone()), Capability::ReadWrite)
@@ -211,6 +243,7 @@ impl CommitView {
             commit,
             editor,
             multibuffer,
+            stash,
         }
     }
 }
@@ -316,7 +349,7 @@ async fn build_buffer(
     };
     let buffer = cx.new(|cx| {
         let buffer = TextBuffer::new_normalized(
-            0,
+            ReplicaId::LOCAL,
             cx.entity_id().as_non_zero_u64().into(),
             line_ending,
             text,
@@ -369,9 +402,13 @@ async fn build_buffer_diff(
     })
 }
 
-fn format_commit(commit: &CommitDetails) -> String {
+fn format_commit(commit: &CommitDetails, is_stash: bool) -> String {
     let mut result = String::new();
-    writeln!(&mut result, "commit {}", commit.sha).unwrap();
+    if is_stash {
+        writeln!(&mut result, "stash commit {}", commit.sha).unwrap();
+    } else {
+        writeln!(&mut result, "commit {}", commit.sha).unwrap();
+    }
     writeln!(
         &mut result,
         "Author: {} <{}>",
@@ -524,11 +561,11 @@ impl Item for CommitView {
         _workspace_id: Option<workspace::WorkspaceId>,
         window: &mut Window,
         cx: &mut Context<Self>,
-    ) -> Option<Entity<Self>>
+    ) -> Task<Option<Entity<Self>>>
     where
         Self: Sized,
     {
-        Some(cx.new(|cx| {
+        Task::ready(Some(cx.new(|cx| {
             let editor = cx.new(|cx| {
                 self.editor
                     .update(cx, |editor, cx| editor.clone(window, cx))
@@ -538,13 +575,296 @@ impl Item for CommitView {
                 editor,
                 multibuffer,
                 commit: self.commit.clone(),
+                stash: self.stash,
             }
-        }))
+        })))
     }
 }
 
 impl Render for CommitView {
-    fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
-        self.editor.clone()
+    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+        let is_stash = self.stash.is_some();
+        div()
+            .key_context(if is_stash { "StashDiff" } else { "CommitDiff" })
+            .bg(cx.theme().colors().editor_background)
+            .flex()
+            .items_center()
+            .justify_center()
+            .size_full()
+            .child(self.editor.clone())
+    }
+}
+
+pub struct CommitViewToolbar {
+    commit_view: Option<WeakEntity<CommitView>>,
+    workspace: WeakEntity<Workspace>,
+}
+
+impl CommitViewToolbar {
+    pub fn new(workspace: &Workspace, _: &mut Context<Self>) -> Self {
+        Self {
+            commit_view: None,
+            workspace: workspace.weak_handle(),
+        }
+    }
+
+    fn commit_view(&self, _: &App) -> Option<Entity<CommitView>> {
+        self.commit_view.as_ref()?.upgrade()
+    }
+
+    async fn close_commit_view(
+        commit_view: Entity<CommitView>,
+        workspace: WeakEntity<Workspace>,
+        cx: &mut AsyncWindowContext,
+    ) -> anyhow::Result<()> {
+        workspace
+            .update_in(cx, |workspace, window, cx| {
+                let active_pane = workspace.active_pane();
+                let commit_view_id = commit_view.entity_id();
+                active_pane.update(cx, |pane, cx| {
+                    pane.close_item_by_id(commit_view_id, SaveIntent::Skip, window, cx)
+                })
+            })?
+            .await?;
+        anyhow::Ok(())
+    }
+
+    fn apply_stash(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+        self.stash_action(
+            "Apply",
+            window,
+            cx,
+            async move |repository, sha, stash, commit_view, workspace, cx| {
+                let result = repository.update(cx, |repo, cx| {
+                    if !stash_matches_index(&sha, stash, repo) {
+                        return Err(anyhow::anyhow!("Stash has changed, not applying"));
+                    }
+                    Ok(repo.stash_apply(Some(stash), cx))
+                })?;
+
+                match result {
+                    Ok(task) => task.await?,
+                    Err(err) => {
+                        Self::close_commit_view(commit_view, workspace, cx).await?;
+                        return Err(err);
+                    }
+                };
+                Self::close_commit_view(commit_view, workspace, cx).await?;
+                anyhow::Ok(())
+            },
+        );
+    }
+
+    fn pop_stash(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+        self.stash_action(
+            "Pop",
+            window,
+            cx,
+            async move |repository, sha, stash, commit_view, workspace, cx| {
+                let result = repository.update(cx, |repo, cx| {
+                    if !stash_matches_index(&sha, stash, repo) {
+                        return Err(anyhow::anyhow!("Stash has changed, pop aborted"));
+                    }
+                    Ok(repo.stash_pop(Some(stash), cx))
+                })?;
+
+                match result {
+                    Ok(task) => task.await?,
+                    Err(err) => {
+                        Self::close_commit_view(commit_view, workspace, cx).await?;
+                        return Err(err);
+                    }
+                };
+                Self::close_commit_view(commit_view, workspace, cx).await?;
+                anyhow::Ok(())
+            },
+        );
+    }
+
+    fn remove_stash(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+        self.stash_action(
+            "Drop",
+            window,
+            cx,
+            async move |repository, sha, stash, commit_view, workspace, cx| {
+                let result = repository.update(cx, |repo, cx| {
+                    if !stash_matches_index(&sha, stash, repo) {
+                        return Err(anyhow::anyhow!("Stash has changed, drop aborted"));
+                    }
+                    Ok(repo.stash_drop(Some(stash), cx))
+                })?;
+
+                match result {
+                    Ok(task) => task.await??,
+                    Err(err) => {
+                        Self::close_commit_view(commit_view, workspace, cx).await?;
+                        return Err(err);
+                    }
+                };
+                Self::close_commit_view(commit_view, workspace, cx).await?;
+                anyhow::Ok(())
+            },
+        );
+    }
+
+    fn stash_action<AsyncFn>(
+        &mut self,
+        str_action: &str,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+        callback: AsyncFn,
+    ) where
+        AsyncFn: AsyncFnOnce(
+                Entity<Repository>,
+                &SharedString,
+                usize,
+                Entity<CommitView>,
+                WeakEntity<Workspace>,
+                &mut AsyncWindowContext,
+            ) -> anyhow::Result<()>
+            + 'static,
+    {
+        let Some(commit_view) = self.commit_view(cx) else {
+            return;
+        };
+        let Some(stash) = commit_view.read(cx).stash else {
+            return;
+        };
+        let sha = commit_view.read(cx).commit.sha.clone();
+        let answer = window.prompt(
+            PromptLevel::Info,
+            &format!("{} stash@{{{}}}?", str_action, stash),
+            None,
+            &[str_action, "Cancel"],
+            cx,
+        );
+
+        let workspace = self.workspace.clone();
+        cx.spawn_in(window, async move |_, cx| {
+            if answer.await != Ok(0) {
+                return anyhow::Ok(());
+            }
+            let repo = workspace.update(cx, |workspace, cx| {
+                workspace
+                    .panel::<GitPanel>(cx)
+                    .and_then(|p| p.read(cx).active_repository.clone())
+            })?;
+
+            let Some(repo) = repo else {
+                return Ok(());
+            };
+            callback(repo, &sha, stash, commit_view, workspace, cx).await?;
+            anyhow::Ok(())
+        })
+        .detach_and_notify_err(window, cx);
+    }
+}
+
+impl EventEmitter<ToolbarItemEvent> for CommitViewToolbar {}
+
+impl ToolbarItemView for CommitViewToolbar {
+    fn set_active_pane_item(
+        &mut self,
+        active_pane_item: Option<&dyn ItemHandle>,
+        _: &mut Window,
+        cx: &mut Context<Self>,
+    ) -> ToolbarItemLocation {
+        if let Some(entity) = active_pane_item.and_then(|i| i.act_as::<CommitView>(cx))
+            && entity.read(cx).stash.is_some()
+        {
+            self.commit_view = Some(entity.downgrade());
+            return ToolbarItemLocation::PrimaryRight;
+        }
+        ToolbarItemLocation::Hidden
+    }
+
+    fn pane_focus_update(
+        &mut self,
+        _pane_focused: bool,
+        _window: &mut Window,
+        _cx: &mut Context<Self>,
+    ) {
+    }
+}
+
+impl Render for CommitViewToolbar {
+    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+        let Some(commit_view) = self.commit_view(cx) else {
+            return div();
+        };
+
+        let is_stash = commit_view.read(cx).stash.is_some();
+        if !is_stash {
+            return div();
+        }
+
+        let focus_handle = commit_view.focus_handle(cx);
+
+        h_group_xl().my_neg_1().py_1().items_center().child(
+            h_group_sm()
+                .child(
+                    Button::new("apply-stash", "Apply")
+                        .tooltip(Tooltip::for_action_title_in(
+                            "Apply current stash",
+                            &ApplyCurrentStash,
+                            &focus_handle,
+                        ))
+                        .on_click(cx.listener(|this, _, window, cx| this.apply_stash(window, cx))),
+                )
+                .child(
+                    Button::new("pop-stash", "Pop")
+                        .tooltip(Tooltip::for_action_title_in(
+                            "Pop current stash",
+                            &PopCurrentStash,
+                            &focus_handle,
+                        ))
+                        .on_click(cx.listener(|this, _, window, cx| this.pop_stash(window, cx))),
+                )
+                .child(
+                    Button::new("remove-stash", "Remove")
+                        .icon(IconName::Trash)
+                        .tooltip(Tooltip::for_action_title_in(
+                            "Remove current stash",
+                            &DropCurrentStash,
+                            &focus_handle,
+                        ))
+                        .on_click(cx.listener(|this, _, window, cx| this.remove_stash(window, cx))),
+                ),
+        )
+    }
+}
+
+fn register_workspace_action<A: Action>(
+    workspace: &mut Workspace,
+    callback: fn(&mut CommitViewToolbar, &A, &mut Window, &mut Context<CommitViewToolbar>),
+) {
+    workspace.register_action(move |workspace, action: &A, window, cx| {
+        if workspace.has_active_modal(window, cx) {
+            cx.propagate();
+            return;
+        }
+
+        workspace.active_pane().update(cx, |pane, cx| {
+            pane.toolbar().update(cx, move |workspace, cx| {
+                if let Some(toolbar) = workspace.item_of_type::<CommitViewToolbar>() {
+                    toolbar.update(cx, move |toolbar, cx| {
+                        callback(toolbar, action, window, cx);
+                        cx.notify();
+                    });
+                }
+            });
+        })
+    });
+}
+
+fn stash_matches_index(sha: &str, index: usize, repo: &mut Repository) -> bool {
+    match repo
+        .cached_stash()
+        .entries
+        .iter()
+        .find(|entry| entry.index == index)
+    {
+        Some(entry) => entry.oid.to_string() == sha,
+        None => false,
     }
 }

crates/git_ui/src/git_panel.rs 🔗

@@ -425,13 +425,20 @@ impl GitPanel {
                     }
                     GitStoreEvent::RepositoryUpdated(
                         _,
-                        RepositoryEvent::Updated { full_scan, .. },
+                        RepositoryEvent::StatusesChanged { full_scan: true }
+                        | RepositoryEvent::BranchChanged
+                        | RepositoryEvent::MergeHeadsChanged,
                         true,
                     ) => {
-                        this.schedule_update(*full_scan, window, cx);
+                        this.schedule_update(true, window, cx);
                     }
-
-                    GitStoreEvent::RepositoryAdded(_) | GitStoreEvent::RepositoryRemoved(_) => {
+                    GitStoreEvent::RepositoryUpdated(
+                        _,
+                        RepositoryEvent::StatusesChanged { full_scan: false },
+                        true,
+                    )
+                    | GitStoreEvent::RepositoryAdded
+                    | GitStoreEvent::RepositoryRemoved(_) => {
                         this.schedule_update(false, window, cx);
                     }
                     GitStoreEvent::IndexWriteError(error) => {
@@ -3091,13 +3098,12 @@ impl GitPanel {
             IconButton::new("generate-commit-message", IconName::AiEdit)
                 .shape(ui::IconButtonShape::Square)
                 .icon_color(Color::Muted)
-                .tooltip(move |window, cx| {
+                .tooltip(move |_window, cx| {
                     if can_commit {
                         Tooltip::for_action_in(
                             "Generate Commit Message",
                             &git::GenerateCommitMessage,
                             &editor_focus_handle,
-                            window,
                             cx,
                         )
                     } else {
@@ -3459,12 +3465,11 @@ impl GitPanel {
                                 panel_icon_button("expand-commit-editor", IconName::Maximize)
                                     .icon_size(IconSize::Small)
                                     .size(ui::ButtonSize::Default)
-                                    .tooltip(move |window, cx| {
+                                    .tooltip(move |_window, cx| {
                                         Tooltip::for_action_in(
                                             "Open Commit Modal",
                                             &git::ExpandCommitEditor,
                                             &expand_tooltip_focus_handle,
-                                            window,
                                             cx,
                                         )
                                     })
@@ -3526,7 +3531,7 @@ impl GitPanel {
                 .disabled(!can_commit || self.modal_open)
                 .tooltip({
                     let handle = commit_tooltip_focus_handle.clone();
-                    move |window, cx| {
+                    move |_window, cx| {
                         if can_commit {
                             Tooltip::with_meta_in(
                                 tooltip,
@@ -3537,7 +3542,6 @@ impl GitPanel {
                                     if signoff { " --signoff" } else { "" }
                                 ),
                                 &handle.clone(),
-                                window,
                                 cx,
                             )
                         } else {
@@ -3611,9 +3615,10 @@ impl GitPanel {
                             let repo = active_repository.downgrade();
                             move |_, window, cx| {
                                 CommitView::open(
-                                    commit.clone(),
+                                    commit.sha.to_string(),
                                     repo.clone(),
                                     workspace.clone(),
+                                    None,
                                     window,
                                     cx,
                                 );
@@ -3639,7 +3644,7 @@ impl GitPanel {
                         panel_icon_button("undo", IconName::Undo)
                             .icon_size(IconSize::XSmall)
                             .icon_color(Color::Muted)
-                            .tooltip(move |window, cx| {
+                            .tooltip(move |_window, cx| {
                                 Tooltip::with_meta(
                                     "Uncommit",
                                     Some(&git::Uncommit),
@@ -3648,7 +3653,6 @@ impl GitPanel {
                                     } else {
                                         "git reset HEAD^"
                                     },
-                                    window,
                                     cx,
                                 )
                             })
@@ -4119,13 +4123,13 @@ impl GitPanel {
                                     .ok();
                                 }
                             })
-                            .tooltip(move |window, cx| {
+                            .tooltip(move |_window, cx| {
                                 let is_staged = entry_staging.is_fully_staged();
 
                                 let action = if is_staged { "Unstage" } else { "Stage" };
                                 let tooltip_name = action.to_string();
 
-                                Tooltip::for_action(tooltip_name, &ToggleStaged, window, cx)
+                                Tooltip::for_action(tooltip_name, &ToggleStaged, cx)
                             }),
                     ),
             )
@@ -4419,6 +4423,10 @@ impl Panel for GitPanel {
         "GitPanel"
     }
 
+    fn panel_key() -> &'static str {
+        GIT_PANEL_KEY
+    }
+
     fn position(&self, _: &Window, cx: &App) -> DockPosition {
         GitPanelSettings::get_global(cx).dock
     }

crates/git_ui/src/git_panel_settings.rs 🔗

@@ -2,7 +2,7 @@ use editor::EditorSettings;
 use gpui::Pixels;
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsContent, StatusStyle};
+use settings::{Settings, StatusStyle};
 use ui::{
     px,
     scrollbars::{ScrollbarVisibility, ShowScrollbar},
@@ -58,16 +58,4 @@ impl Settings for GitPanelSettings {
             collapse_untracked_diff: git_panel.collapse_untracked_diff.unwrap(),
         }
     }
-
-    fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut SettingsContent) {
-        if let Some(git_enabled) = vscode.read_bool("git.enabled") {
-            current.git_panel.get_or_insert_default().button = Some(git_enabled);
-        }
-        if let Some(default_branch) = vscode.read_string("git.defaultBranchName") {
-            current
-                .git_panel
-                .get_or_insert_default()
-                .fallback_branch_name = Some(default_branch.to_string());
-        }
-    }
 }

crates/git_ui/src/git_ui.rs 🔗

@@ -34,7 +34,7 @@ mod askpass_modal;
 pub mod branch_picker;
 mod commit_modal;
 pub mod commit_tooltip;
-mod commit_view;
+pub mod commit_view;
 mod conflict_view;
 pub mod file_diff_view;
 pub mod git_panel;
@@ -59,6 +59,7 @@ pub fn init(cx: &mut App) {
     GitPanelSettings::register(cx);
 
     editor::set_blame_renderer(blame_ui::GitBlameRenderer, cx);
+    commit_view::init(cx);
 
     cx.observe_new(|editor: &mut Editor, _, cx| {
         conflict_view::register_editor(editor, editor.buffer().clone(), cx);
@@ -434,13 +435,12 @@ mod remote_button {
             move |_, window, cx| {
                 window.dispatch_action(Box::new(git::Fetch), cx);
             },
-            move |window, cx| {
+            move |_window, cx| {
                 git_action_tooltip(
                     "Fetch updates from remote",
                     &git::Fetch,
                     "git fetch",
                     keybinding_target.clone(),
-                    window,
                     cx,
                 )
             },
@@ -462,13 +462,12 @@ mod remote_button {
             move |_, window, cx| {
                 window.dispatch_action(Box::new(git::Push), cx);
             },
-            move |window, cx| {
+            move |_window, cx| {
                 git_action_tooltip(
                     "Push committed changes to remote",
                     &git::Push,
                     "git push",
                     keybinding_target.clone(),
-                    window,
                     cx,
                 )
             },
@@ -491,13 +490,12 @@ mod remote_button {
             move |_, window, cx| {
                 window.dispatch_action(Box::new(git::Pull), cx);
             },
-            move |window, cx| {
+            move |_window, cx| {
                 git_action_tooltip(
                     "Pull",
                     &git::Pull,
                     "git pull",
                     keybinding_target.clone(),
-                    window,
                     cx,
                 )
             },
@@ -518,13 +516,12 @@ mod remote_button {
             move |_, window, cx| {
                 window.dispatch_action(Box::new(git::Push), cx);
             },
-            move |window, cx| {
+            move |_window, cx| {
                 git_action_tooltip(
                     "Publish branch to remote",
                     &git::Push,
                     "git push --set-upstream",
                     keybinding_target.clone(),
-                    window,
                     cx,
                 )
             },
@@ -545,13 +542,12 @@ mod remote_button {
             move |_, window, cx| {
                 window.dispatch_action(Box::new(git::Push), cx);
             },
-            move |window, cx| {
+            move |_window, cx| {
                 git_action_tooltip(
                     "Re-publish branch to remote",
                     &git::Push,
                     "git push --set-upstream",
                     keybinding_target.clone(),
-                    window,
                     cx,
                 )
             },
@@ -563,16 +559,15 @@ mod remote_button {
         action: &dyn Action,
         command: impl Into<SharedString>,
         focus_handle: Option<FocusHandle>,
-        window: &mut Window,
         cx: &mut App,
     ) -> AnyView {
         let label = label.into();
         let command = command.into();
 
         if let Some(handle) = focus_handle {
-            Tooltip::with_meta_in(label, Some(action), command, &handle, window, cx)
+            Tooltip::with_meta_in(label, Some(action), command, &handle, cx)
         } else {
-            Tooltip::with_meta(label, Some(action), command, window, cx)
+            Tooltip::with_meta(label, Some(action), command, cx)
         }
     }
 

crates/git_ui/src/project_diff.rs 🔗

@@ -6,7 +6,7 @@ use crate::{
 };
 use anyhow::Result;
 use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus};
-use collections::HashSet;
+use collections::{HashMap, HashSet};
 use editor::{
     Editor, EditorEvent, SelectionEffects,
     actions::{GoToHunk, GoToPreviousHunk},
@@ -27,7 +27,7 @@ use language::{Anchor, Buffer, Capability, OffsetRangeExt};
 use multi_buffer::{MultiBuffer, PathKey};
 use project::{
     Project, ProjectPath,
-    git_store::{GitStore, GitStoreEvent, Repository},
+    git_store::{GitStore, GitStoreEvent, Repository, RepositoryEvent},
 };
 use settings::{Settings, SettingsStore};
 use std::any::{Any, TypeId};
@@ -57,12 +57,13 @@ pub struct ProjectDiff {
     multibuffer: Entity<MultiBuffer>,
     editor: Entity<Editor>,
     git_store: Entity<GitStore>,
+    buffer_diff_subscriptions: HashMap<RepoPath, (Entity<BufferDiff>, Subscription)>,
     workspace: WeakEntity<Workspace>,
     focus_handle: FocusHandle,
     update_needed: postage::watch::Sender<()>,
     pending_scroll: Option<PathKey>,
     _task: Task<Result<()>>,
-    _subscription: Subscription,
+    _git_store_subscription: Subscription,
 }
 
 #[derive(Debug)]
@@ -177,7 +178,11 @@ impl ProjectDiff {
             window,
             move |this, _git_store, event, _window, _cx| match event {
                 GitStoreEvent::ActiveRepositoryChanged(_)
-                | GitStoreEvent::RepositoryUpdated(_, _, true)
+                | GitStoreEvent::RepositoryUpdated(
+                    _,
+                    RepositoryEvent::StatusesChanged { full_scan: _ },
+                    true,
+                )
                 | GitStoreEvent::ConflictsUpdated => {
                     *this.update_needed.borrow_mut() = ();
                 }
@@ -217,10 +222,11 @@ impl ProjectDiff {
             focus_handle,
             editor,
             multibuffer,
+            buffer_diff_subscriptions: Default::default(),
             pending_scroll: None,
             update_needed: send,
             _task: worker,
-            _subscription: git_store_subscription,
+            _git_store_subscription: git_store_subscription,
         }
     }
 
@@ -365,6 +371,7 @@ impl ProjectDiff {
             self.multibuffer.update(cx, |multibuffer, cx| {
                 multibuffer.clear(cx);
             });
+            self.buffer_diff_subscriptions.clear();
             return vec![];
         };
 
@@ -407,6 +414,8 @@ impl ProjectDiff {
         });
         self.multibuffer.update(cx, |multibuffer, cx| {
             for path in previous_paths {
+                self.buffer_diff_subscriptions
+                    .remove(&path.path.clone().into());
                 multibuffer.remove_excerpts_for_path(path, cx);
             }
         });
@@ -419,9 +428,15 @@ impl ProjectDiff {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        let path_key = diff_buffer.path_key;
-        let buffer = diff_buffer.buffer;
-        let diff = diff_buffer.diff;
+        let path_key = diff_buffer.path_key.clone();
+        let buffer = diff_buffer.buffer.clone();
+        let diff = diff_buffer.diff.clone();
+
+        let subscription = cx.subscribe(&diff, move |this, _, _, _| {
+            *this.update_needed.borrow_mut() = ();
+        });
+        self.buffer_diff_subscriptions
+            .insert(path_key.path.clone().into(), (diff.clone(), subscription));
 
         let conflict_addon = self
             .editor
@@ -440,9 +455,10 @@ impl ProjectDiff {
             .unwrap_or_default();
         let conflicts = conflicts.iter().map(|conflict| conflict.range.clone());
 
-        let excerpt_ranges = merge_anchor_ranges(diff_hunk_ranges, conflicts, &snapshot)
-            .map(|range| range.to_point(&snapshot))
-            .collect::<Vec<_>>();
+        let excerpt_ranges =
+            merge_anchor_ranges(diff_hunk_ranges.into_iter(), conflicts, &snapshot)
+                .map(|range| range.to_point(&snapshot))
+                .collect::<Vec<_>>();
 
         let (was_empty, is_excerpt_newly_added) = self.multibuffer.update(cx, |multibuffer, cx| {
             let was_empty = multibuffer.is_empty();
@@ -519,8 +535,7 @@ impl ProjectDiff {
         self.multibuffer
             .read(cx)
             .excerpt_paths()
-            .map(|key| key.path())
-            .cloned()
+            .map(|key| key.path.clone())
             .collect()
     }
 }
@@ -625,12 +640,16 @@ impl Item for ProjectDiff {
         _workspace_id: Option<workspace::WorkspaceId>,
         window: &mut Window,
         cx: &mut Context<Self>,
-    ) -> Option<Entity<Self>>
+    ) -> Task<Option<Entity<Self>>>
     where
         Self: Sized,
     {
-        let workspace = self.workspace.upgrade()?;
-        Some(cx.new(|cx| ProjectDiff::new(self.project.clone(), workspace, window, cx)))
+        let Some(workspace) = self.workspace.upgrade() else {
+            return Task::ready(None);
+        };
+        Task::ready(Some(cx.new(|cx| {
+            ProjectDiff::new(self.project.clone(), workspace, window, cx)
+        })))
     }
 
     fn is_dirty(&self, cx: &App) -> bool {
@@ -710,7 +729,7 @@ impl Item for ProjectDiff {
 }
 
 impl Render for ProjectDiff {
-    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
         let is_empty = self.multibuffer.read(cx).is_empty();
 
         div()
@@ -755,7 +774,6 @@ impl Render for ProjectDiff {
                                     .key_binding(KeyBinding::for_action_in(
                                         &CloseActiveItem::default(),
                                         &keybinding_focus_handle,
-                                        window,
                                         cx,
                                     ))
                                     .on_click(move |_, window, cx| {
@@ -947,6 +965,11 @@ impl Render for ProjectDiffToolbar {
                                     &StageAndNext,
                                     &focus_handle,
                                 ))
+                                .disabled(
+                                    !button_states.prev_next
+                                        && !button_states.stage_all
+                                        && !button_states.unstage_all,
+                                )
                                 .on_click(cx.listener(|this, _, window, cx| {
                                     this.dispatch_action(&StageAndNext, window, cx)
                                 })),
@@ -958,6 +981,11 @@ impl Render for ProjectDiffToolbar {
                                     &UnstageAndNext,
                                     &focus_handle,
                                 ))
+                                .disabled(
+                                    !button_states.prev_next
+                                        && !button_states.stage_all
+                                        && !button_states.unstage_all,
+                                )
                                 .on_click(cx.listener(|this, _, window, cx| {
                                     this.dispatch_action(&UnstageAndNext, window, cx)
                                 })),
@@ -1608,8 +1636,8 @@ mod tests {
             cx,
             &"
                 - original
-                + different
-                  ˇ"
+                + ˇdifferent
+            "
             .unindent(),
         );
     }
@@ -1937,6 +1965,7 @@ mod tests {
             .unindent(),
         );
 
+        // The project diff updates its excerpts when a new hunk appears in a buffer that already has a diff.
         let buffer = project
             .update(cx, |project, cx| {
                 project.open_local_buffer(path!("/project/foo.txt"), cx)
@@ -1989,4 +2018,63 @@ mod tests {
             .unindent(),
         );
     }
+
+    #[gpui::test]
+    async fn test_update_on_uncommit(cx: &mut TestAppContext) {
+        init_test(cx);
+
+        let fs = FakeFs::new(cx.executor());
+        fs.insert_tree(
+            path!("/project"),
+            json!({
+                ".git": {},
+                "README.md": "# My cool project\n".to_owned()
+            }),
+        )
+        .await;
+        fs.set_head_and_index_for_repo(
+            Path::new(path!("/project/.git")),
+            &[("README.md", "# My cool project\n".to_owned())],
+        );
+        let project = Project::test(fs.clone(), [Path::new(path!("/project"))], cx).await;
+        let worktree_id = project.read_with(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));
+        cx.run_until_parked();
+
+        let _editor = workspace
+            .update_in(cx, |workspace, window, cx| {
+                workspace.open_path((worktree_id, rel_path("README.md")), None, true, window, cx)
+            })
+            .await
+            .unwrap()
+            .downcast::<Editor>()
+            .unwrap();
+
+        cx.focus(&workspace);
+        cx.update(|window, cx| {
+            window.dispatch_action(project_diff::Diff.boxed_clone(), cx);
+        });
+        cx.run_until_parked();
+        let item = workspace.update(cx, |workspace, cx| {
+            workspace.active_item_as::<ProjectDiff>(cx).unwrap()
+        });
+        cx.focus(&item);
+        let editor = item.read_with(cx, |item, _| item.editor.clone());
+
+        fs.set_head_and_index_for_repo(
+            Path::new(path!("/project/.git")),
+            &[(
+                "README.md",
+                "# My cool project\nDetails to come.\n".to_owned(),
+            )],
+        );
+        cx.run_until_parked();
+
+        let mut cx = EditorTestContext::for_editor_in(editor, cx).await;
+
+        cx.assert_excerpts_with_selections("[EXCERPT]\nˇ# My cool project\nDetails to come.\n");
+    }
 }

crates/git_ui/src/stash_picker.rs 🔗

@@ -5,18 +5,21 @@ use git::stash::StashEntry;
 use gpui::{
     Action, AnyElement, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
     InteractiveElement, IntoElement, Modifiers, ModifiersChangedEvent, ParentElement, Render,
-    SharedString, Styled, Subscription, Task, Window, actions, rems,
+    SharedString, Styled, Subscription, Task, WeakEntity, Window, actions, rems, svg,
 };
 use picker::{Picker, PickerDelegate};
 use project::git_store::{Repository, RepositoryEvent};
 use std::sync::Arc;
 use time::{OffsetDateTime, UtcOffset};
 use time_format;
-use ui::{HighlightedLabel, KeyBinding, ListItem, ListItemSpacing, Tooltip, prelude::*};
+use ui::{
+    ButtonLike, HighlightedLabel, KeyBinding, ListItem, ListItemSpacing, Tooltip, prelude::*,
+};
 use util::ResultExt;
 use workspace::notifications::DetachAndPromptErr;
 use workspace::{ModalView, Workspace};
 
+use crate::commit_view::CommitView;
 use crate::stash_picker;
 
 actions!(
@@ -24,6 +27,8 @@ actions!(
     [
         /// Drop the selected stash entry.
         DropStashItem,
+        /// Show the diff view of the selected stash entry.
+        ShowStashItem,
     ]
 );
 
@@ -38,8 +43,9 @@ pub fn open(
     cx: &mut Context<Workspace>,
 ) {
     let repository = workspace.project().read(cx).active_repository(cx);
+    let weak_workspace = workspace.weak_handle();
     workspace.toggle_modal(window, cx, |window, cx| {
-        StashList::new(repository, rems(34.), window, cx)
+        StashList::new(repository, weak_workspace, rems(34.), window, cx)
     })
 }
 
@@ -53,6 +59,7 @@ pub struct StashList {
 impl StashList {
     fn new(
         repository: Option<Entity<Repository>>,
+        workspace: WeakEntity<Workspace>,
         width: Rems,
         window: &mut Window,
         cx: &mut Context<Self>,
@@ -65,7 +72,7 @@ impl StashList {
         if let Some(repo) = repository.clone() {
             _subscriptions.push(
                 cx.subscribe_in(&repo, window, |this, _, event, window, cx| {
-                    if matches!(event, RepositoryEvent::Updated { .. }) {
+                    if matches!(event, RepositoryEvent::StashEntriesChanged) {
                         let stash_entries = this.picker.read_with(cx, |picker, cx| {
                             picker
                                 .delegate
@@ -98,7 +105,7 @@ impl StashList {
         })
         .detach_and_log_err(cx);
 
-        let delegate = StashListDelegate::new(repository, window, cx);
+        let delegate = StashListDelegate::new(repository, workspace, window, cx);
         let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
         let picker_focus_handle = picker.focus_handle(cx);
         picker.update(cx, |picker, _| {
@@ -131,6 +138,20 @@ impl StashList {
         cx.notify();
     }
 
+    fn handle_show_stash(
+        &mut self,
+        _: &ShowStashItem,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        self.picker.update(cx, |picker, cx| {
+            picker
+                .delegate
+                .show_stash_at(picker.delegate.selected_index(), window, cx);
+        });
+        cx.notify();
+    }
+
     fn handle_modifiers_changed(
         &mut self,
         ev: &ModifiersChangedEvent,
@@ -157,6 +178,7 @@ impl Render for StashList {
             .w(self.width)
             .on_modifiers_changed(cx.listener(Self::handle_modifiers_changed))
             .on_action(cx.listener(Self::handle_drop_stash))
+            .on_action(cx.listener(Self::handle_show_stash))
             .child(self.picker.clone())
     }
 }
@@ -172,6 +194,7 @@ pub struct StashListDelegate {
     matches: Vec<StashEntryMatch>,
     all_stash_entries: Option<Vec<StashEntry>>,
     repo: Option<Entity<Repository>>,
+    workspace: WeakEntity<Workspace>,
     selected_index: usize,
     last_query: String,
     modifiers: Modifiers,
@@ -182,6 +205,7 @@ pub struct StashListDelegate {
 impl StashListDelegate {
     fn new(
         repo: Option<Entity<Repository>>,
+        workspace: WeakEntity<Workspace>,
         _window: &mut Window,
         cx: &mut Context<StashList>,
     ) -> Self {
@@ -192,6 +216,7 @@ impl StashListDelegate {
         Self {
             matches: vec![],
             repo,
+            workspace,
             all_stash_entries: None,
             selected_index: 0,
             last_query: Default::default(),
@@ -235,6 +260,25 @@ impl StashListDelegate {
         });
     }
 
+    fn show_stash_at(&self, ix: usize, window: &mut Window, cx: &mut Context<Picker<Self>>) {
+        let Some(entry_match) = self.matches.get(ix) else {
+            return;
+        };
+        let stash_sha = entry_match.entry.oid.to_string();
+        let stash_index = entry_match.entry.index;
+        let Some(repo) = self.repo.clone() else {
+            return;
+        };
+        CommitView::open(
+            stash_sha,
+            repo.downgrade(),
+            self.workspace.clone(),
+            Some(stash_index),
+            window,
+            cx,
+        );
+    }
+
     fn pop_stash(&self, stash_index: usize, window: &mut Window, cx: &mut Context<Picker<Self>>) {
         let Some(repo) = self.repo.clone() else {
             return;
@@ -390,7 +434,7 @@ impl PickerDelegate for StashListDelegate {
         ix: usize,
         selected: bool,
         _window: &mut Window,
-        _cx: &mut Context<Picker<Self>>,
+        cx: &mut Context<Picker<Self>>,
     ) -> Option<Self::ListItem> {
         let entry_match = &self.matches[ix];
 
@@ -432,11 +476,35 @@ impl PickerDelegate for StashListDelegate {
                     .size(LabelSize::Small),
             );
 
+        let show_button = div()
+            .group("show-button-hover")
+            .child(
+                ButtonLike::new("show-button")
+                    .child(
+                        svg()
+                            .size(IconSize::Medium.rems())
+                            .flex_none()
+                            .path(IconName::Eye.path())
+                            .text_color(Color::Default.color(cx))
+                            .group_hover("show-button-hover", |this| {
+                                this.text_color(Color::Accent.color(cx))
+                            })
+                            .hover(|this| this.text_color(Color::Accent.color(cx))),
+                    )
+                    .tooltip(Tooltip::for_action_title("Show Stash", &ShowStashItem))
+                    .on_click(cx.listener(move |picker, _, window, cx| {
+                        cx.stop_propagation();
+                        picker.delegate.show_stash_at(ix, window, cx);
+                    })),
+            )
+            .into_any_element();
+
         Some(
             ListItem::new(SharedString::from(format!("stash-{ix}")))
                 .inset(true)
                 .spacing(ListItemSpacing::Sparse)
                 .toggle_state(selected)
+                .end_slot(show_button)
                 .child(
                     v_flex()
                         .w_full()
@@ -455,11 +523,7 @@ impl PickerDelegate for StashListDelegate {
         Some("No stashes found".into())
     }
 
-    fn render_footer(
-        &self,
-        window: &mut Window,
-        cx: &mut Context<Picker<Self>>,
-    ) -> Option<AnyElement> {
+    fn render_footer(&self, _: &mut Window, cx: &mut Context<Picker<Self>>) -> Option<AnyElement> {
         let focus_handle = self.focus_handle.clone();
 
         Some(
@@ -473,7 +537,7 @@ impl PickerDelegate for StashListDelegate {
                 .child(
                     Button::new("apply-stash", "Apply")
                         .key_binding(
-                            KeyBinding::for_action_in(&menu::Confirm, &focus_handle, window, cx)
+                            KeyBinding::for_action_in(&menu::Confirm, &focus_handle, cx)
                                 .map(|kb| kb.size(rems_from_px(12.))),
                         )
                         .on_click(|_, window, cx| {
@@ -483,13 +547,8 @@ impl PickerDelegate for StashListDelegate {
                 .child(
                     Button::new("pop-stash", "Pop")
                         .key_binding(
-                            KeyBinding::for_action_in(
-                                &menu::SecondaryConfirm,
-                                &focus_handle,
-                                window,
-                                cx,
-                            )
-                            .map(|kb| kb.size(rems_from_px(12.))),
+                            KeyBinding::for_action_in(&menu::SecondaryConfirm, &focus_handle, cx)
+                                .map(|kb| kb.size(rems_from_px(12.))),
                         )
                         .on_click(|_, window, cx| {
                             window.dispatch_action(menu::SecondaryConfirm.boxed_clone(), cx)
@@ -501,7 +560,6 @@ impl PickerDelegate for StashListDelegate {
                             KeyBinding::for_action_in(
                                 &stash_picker::DropStashItem,
                                 &focus_handle,
-                                window,
                                 cx,
                             )
                             .map(|kb| kb.size(rems_from_px(12.))),

crates/git_ui/src/text_diff_view.rs 🔗

@@ -49,7 +49,7 @@ impl TextDiffView {
         let selection_data = source_editor.update(cx, |editor, cx| {
             let multibuffer = editor.buffer().read(cx);
             let source_buffer = multibuffer.as_singleton()?;
-            let selections = editor.selections.all::<Point>(cx);
+            let selections = editor.selections.all::<Point>(&editor.display_snapshot(cx));
             let buffer_snapshot = source_buffer.read(cx);
             let first_selection = selections.first()?;
             let max_point = buffer_snapshot.max_point();

crates/go_to_line/Cargo.toml 🔗

@@ -24,7 +24,6 @@ theme.workspace = true
 ui.workspace = true
 util.workspace = true
 workspace.workspace = true
-workspace-hack.workspace = true
 
 [dev-dependencies]
 editor = { workspace = true, features = ["test-support"] }

crates/go_to_line/src/cursor_position.rs 🔗

@@ -238,18 +238,16 @@ impl Render for CursorPosition {
                             });
                         }
                     }))
-                    .tooltip(move |window, cx| match context.as_ref() {
+                    .tooltip(move |_window, cx| match context.as_ref() {
                         Some(context) => Tooltip::for_action_in(
                             "Go to Line/Column",
                             &editor::actions::ToggleGoToLine,
                             context,
-                            window,
                             cx,
                         ),
                         None => Tooltip::for_action(
                             "Go to Line/Column",
                             &editor::actions::ToggleGoToLine,
-                            window,
                             cx,
                         ),
                     }),

crates/go_to_line/src/go_to_line.rs 🔗

@@ -74,7 +74,9 @@ impl GoToLine {
     ) -> Self {
         let (user_caret, last_line, scroll_position) = active_editor.update(cx, |editor, cx| {
             let user_caret = UserCaretPosition::at_selection_end(
-                &editor.selections.last::<Point>(cx),
+                &editor
+                    .selections
+                    .last::<Point>(&editor.display_snapshot(cx)),
                 &editor.buffer().read(cx).snapshot(cx),
             );
 
@@ -739,7 +741,7 @@ mod tests {
         let selections = editor.update(cx, |editor, cx| {
             editor
                 .selections
-                .all::<rope::Point>(cx)
+                .all::<rope::Point>(&editor.display_snapshot(cx))
                 .into_iter()
                 .map(|s| s.start..s.end)
                 .collect::<Vec<_>>()

crates/google_ai/Cargo.toml 🔗

@@ -23,4 +23,3 @@ serde.workspace = true
 serde_json.workspace = true
 settings.workspace = true
 strum.workspace = true
-workspace-hack.workspace = true

crates/gpui/Cargo.toml 🔗

@@ -1,6 +1,6 @@
 [package]
 name = "gpui"
-version = "0.2.1"
+version = "0.2.2"
 edition.workspace = true
 authors = ["Nathan Sobo <nathan@zed.dev>"]
 description = "Zed's GPU-accelerated UI framework"
@@ -133,13 +133,13 @@ util.workspace = true
 uuid.workspace = true
 waker-fn = "1.2.0"
 lyon = "1.0"
-workspace-hack.workspace = true
 libc.workspace = true
 pin-project = "1.1.10"
 
 [target.'cfg(target_os = "macos")'.dependencies]
 block = "0.1"
 cocoa.workspace = true
+cocoa-foundation.workspace = true
 core-foundation.workspace = true
 core-foundation-sys.workspace = true
 core-graphics = "0.24"

crates/gpui/README.md 🔗

@@ -11,7 +11,7 @@ GPUI is still in active development as we work on the Zed code editor, and is st
 gpui = { version = "*" }
 ```
 
- - [Ownership and data flow](_ownership_and_data_flow)
+ - [Ownership and data flow](src/_ownership_and_data_flow.rs)
 
 Everything in GPUI starts with an `Application`. You can create one with `Application::new()`, and kick off your application by passing a callback to `Application::run()`. Inside this callback, you can create a new window with `App::open_window()`, and register your first root view. See [gpui.rs](https://www.gpui.rs/) for a complete example.
 

crates/gpui/examples/focus_visible.rs 🔗

@@ -0,0 +1,214 @@
+use gpui::{
+    App, Application, Bounds, Context, Div, ElementId, FocusHandle, KeyBinding, SharedString,
+    Stateful, Window, WindowBounds, WindowOptions, actions, div, prelude::*, px, size,
+};
+
+actions!(example, [Tab, TabPrev, Quit]);
+
+struct Example {
+    focus_handle: FocusHandle,
+    items: Vec<(FocusHandle, &'static str)>,
+    message: SharedString,
+}
+
+impl Example {
+    fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
+        let items = vec![
+            (
+                cx.focus_handle().tab_index(1).tab_stop(true),
+                "Button with .focus() - always shows border when focused",
+            ),
+            (
+                cx.focus_handle().tab_index(2).tab_stop(true),
+                "Button with .focus_visible() - only shows border with keyboard",
+            ),
+            (
+                cx.focus_handle().tab_index(3).tab_stop(true),
+                "Button with both .focus() and .focus_visible()",
+            ),
+        ];
+
+        let focus_handle = cx.focus_handle();
+        window.focus(&focus_handle);
+
+        Self {
+            focus_handle,
+            items,
+            message: SharedString::from(
+                "Try clicking vs tabbing! Click shows no border, Tab shows border.",
+            ),
+        }
+    }
+
+    fn on_tab(&mut self, _: &Tab, window: &mut Window, _: &mut Context<Self>) {
+        window.focus_next();
+        self.message = SharedString::from("Pressed Tab - focus-visible border should appear!");
+    }
+
+    fn on_tab_prev(&mut self, _: &TabPrev, window: &mut Window, _: &mut Context<Self>) {
+        window.focus_prev();
+        self.message =
+            SharedString::from("Pressed Shift-Tab - focus-visible border should appear!");
+    }
+
+    fn on_quit(&mut self, _: &Quit, _window: &mut Window, cx: &mut Context<Self>) {
+        cx.quit();
+    }
+}
+
+impl Render for Example {
+    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+        fn button_base(id: impl Into<ElementId>, label: &'static str) -> Stateful<Div> {
+            div()
+                .id(id)
+                .h_16()
+                .w_full()
+                .flex()
+                .justify_center()
+                .items_center()
+                .bg(gpui::rgb(0x2563eb))
+                .text_color(gpui::white())
+                .rounded_md()
+                .cursor_pointer()
+                .hover(|style| style.bg(gpui::rgb(0x1d4ed8)))
+                .child(label)
+        }
+
+        div()
+            .id("app")
+            .track_focus(&self.focus_handle)
+            .on_action(cx.listener(Self::on_tab))
+            .on_action(cx.listener(Self::on_tab_prev))
+            .on_action(cx.listener(Self::on_quit))
+            .size_full()
+            .flex()
+            .flex_col()
+            .p_8()
+            .gap_6()
+            .bg(gpui::rgb(0xf3f4f6))
+            .child(
+                div()
+                    .text_2xl()
+                    .font_weight(gpui::FontWeight::BOLD)
+                    .text_color(gpui::rgb(0x111827))
+                    .child("CSS focus-visible Demo"),
+            )
+            .child(
+                div()
+                    .p_4()
+                    .rounded_md()
+                    .bg(gpui::rgb(0xdbeafe))
+                    .text_color(gpui::rgb(0x1e3a8a))
+                    .child(self.message.clone()),
+            )
+            .child(
+                div()
+                    .flex()
+                    .flex_col()
+                    .gap_4()
+                    .child(
+                        div()
+                            .flex()
+                            .flex_col()
+                            .gap_2()
+                            .child(
+                                div()
+                                    .text_sm()
+                                    .font_weight(gpui::FontWeight::BOLD)
+                                    .text_color(gpui::rgb(0x374151))
+                                    .child("1. Regular .focus() - always visible:"),
+                            )
+                            .child(
+                                button_base("button1", self.items[0].1)
+                                    .track_focus(&self.items[0].0)
+                                    .focus(|style| {
+                                        style.border_4().border_color(gpui::rgb(0xfbbf24))
+                                    })
+                                    .on_click(cx.listener(|this, _, _, cx| {
+                                        this.message =
+                                            "Clicked button 1 - focus border is visible!".into();
+                                        cx.notify();
+                                    })),
+                            ),
+                    )
+                    .child(
+                        div()
+                            .flex()
+                            .flex_col()
+                            .gap_2()
+                            .child(
+                                div()
+                                    .text_sm()
+                                    .font_weight(gpui::FontWeight::BOLD)
+                                    .text_color(gpui::rgb(0x374151))
+                                    .child("2. New .focus_visible() - only keyboard:"),
+                            )
+                            .child(
+                                button_base("button2", self.items[1].1)
+                                    .track_focus(&self.items[1].0)
+                                    .focus_visible(|style| {
+                                        style.border_4().border_color(gpui::rgb(0x10b981))
+                                    })
+                                    .on_click(cx.listener(|this, _, _, cx| {
+                                        this.message =
+                                            "Clicked button 2 - no border! Try Tab instead.".into();
+                                        cx.notify();
+                                    })),
+                            ),
+                    )
+                    .child(
+                        div()
+                            .flex()
+                            .flex_col()
+                            .gap_2()
+                            .child(
+                                div()
+                                    .text_sm()
+                                    .font_weight(gpui::FontWeight::BOLD)
+                                    .text_color(gpui::rgb(0x374151))
+                                    .child(
+                                        "3. Both .focus() (yellow) and .focus_visible() (green):",
+                                    ),
+                            )
+                            .child(
+                                button_base("button3", self.items[2].1)
+                                    .track_focus(&self.items[2].0)
+                                    .focus(|style| {
+                                        style.border_4().border_color(gpui::rgb(0xfbbf24))
+                                    })
+                                    .focus_visible(|style| {
+                                        style.border_4().border_color(gpui::rgb(0x10b981))
+                                    })
+                                    .on_click(cx.listener(|this, _, _, cx| {
+                                        this.message =
+                                            "Clicked button 3 - yellow border. Tab shows green!"
+                                                .into();
+                                        cx.notify();
+                                    })),
+                            ),
+                    ),
+            )
+    }
+}
+
+fn main() {
+    Application::new().run(|cx: &mut App| {
+        cx.bind_keys([
+            KeyBinding::new("tab", Tab, None),
+            KeyBinding::new("shift-tab", TabPrev, None),
+            KeyBinding::new("cmd-q", Quit, None),
+        ]);
+
+        let bounds = Bounds::centered(None, size(px(800.), px(600.0)), cx);
+        cx.open_window(
+            WindowOptions {
+                window_bounds: Some(WindowBounds::Windowed(bounds)),
+                ..Default::default()
+            },
+            |window, cx| cx.new(|cx| Example::new(window, cx)),
+        )
+        .unwrap();
+
+        cx.activate(true);
+    });
+}

crates/gpui/examples/text_layout.rs 🔗

@@ -1,6 +1,6 @@
 use gpui::{
-    App, Application, Bounds, Context, Window, WindowBounds, WindowOptions, div, prelude::*, px,
-    size,
+    App, Application, Bounds, Context, FontStyle, FontWeight, StyledText, Window, WindowBounds,
+    WindowOptions, div, prelude::*, px, size,
 };
 
 struct HelloWorld {}
@@ -71,6 +71,12 @@ impl Render for HelloWorld {
                             .child("100%"),
                     ),
             )
+            .child(div().flex().gap_2().justify_between().child(
+                StyledText::new("ABCD").with_highlights([
+                    (0..1, FontWeight::EXTRA_BOLD.into()),
+                    (2..3, FontStyle::Italic.into()),
+                ]),
+            ))
     }
 }
 

crates/gpui/src/app.rs 🔗

@@ -344,13 +344,9 @@ impl SystemWindowTabController {
         let tab_group = self
             .tab_groups
             .iter()
-            .find_map(|(group, tabs)| tabs.iter().find(|tab| tab.id == id).map(|_| *group));
+            .find_map(|(group, tabs)| tabs.iter().find(|tab| tab.id == id).map(|_| *group))?;
 
-        if let Some(tab_group) = tab_group {
-            self.tab_groups.get(&tab_group)
-        } else {
-            None
-        }
+        self.tab_groups.get(&tab_group)
     }
 
     /// Initialize the visibility of the system window tab controller.
@@ -415,7 +411,8 @@ impl SystemWindowTabController {
         for windows in controller.tab_groups.values_mut() {
             for tab in windows.iter_mut() {
                 if tab.id == id {
-                    tab.title = title.clone();
+                    tab.title = title;
+                    return;
                 }
             }
         }
@@ -556,7 +553,7 @@ pub struct App {
     pub(crate) entities: EntityMap,
     pub(crate) window_update_stack: Vec<WindowId>,
     pub(crate) new_entity_observers: SubscriberSet<TypeId, NewEntityListener>,
-    pub(crate) windows: SlotMap<WindowId, Option<Window>>,
+    pub(crate) windows: SlotMap<WindowId, Option<Box<Window>>>,
     pub(crate) window_handles: FxHashMap<WindowId, AnyWindowHandle>,
     pub(crate) focus_handles: Arc<FocusMap>,
     pub(crate) keymap: Rc<RefCell<Keymap>>,
@@ -967,7 +964,7 @@ impl App {
                     clear.clear();
 
                     cx.window_handles.insert(id, window.handle);
-                    cx.windows.get_mut(id).unwrap().replace(window);
+                    cx.windows.get_mut(id).unwrap().replace(Box::new(window));
                     Ok(handle)
                 }
                 Err(e) => {
@@ -1242,7 +1239,7 @@ impl App {
                     .windows
                     .values()
                     .filter_map(|window| {
-                        let window = window.as_ref()?;
+                        let window = window.as_deref()?;
                         window.invalidator.is_dirty().then_some(window.handle)
                     })
                     .collect::<Vec<_>>()
@@ -1323,7 +1320,7 @@ impl App {
 
     fn apply_refresh_effect(&mut self) {
         for window in self.windows.values_mut() {
-            if let Some(window) = window.as_mut() {
+            if let Some(window) = window.as_deref_mut() {
                 window.refreshing = true;
                 window.invalidator.set_dirty(true);
             }
@@ -2202,7 +2199,7 @@ impl AppContext for App {
             .windows
             .get(window.id)
             .context("window not found")?
-            .as_ref()
+            .as_deref()
             .expect("attempted to read a window that is already on the stack");
 
         let root_view = window.root.clone().unwrap();

crates/gpui/src/app/test_context.rs 🔗

@@ -455,7 +455,7 @@ impl TestAppContext {
             .windows
             .get_mut(window.id)
             .unwrap()
-            .as_mut()
+            .as_deref_mut()
             .unwrap()
             .platform_window
             .as_test()
@@ -836,7 +836,7 @@ impl VisualTestContext {
         })
     }
 
-    /// Simulate an event from the platform, e.g. a SrollWheelEvent
+    /// Simulate an event from the platform, e.g. a ScrollWheelEvent
     /// Make sure you've called [VisualTestContext::draw] first!
     pub fn simulate_event<E: InputEvent>(&mut self, event: E) {
         self.test_window(self.window)

crates/gpui/src/bounds_tree.rs 🔗

@@ -34,15 +34,14 @@ where
 
     pub fn insert(&mut self, new_bounds: Bounds<U>) -> u32 {
         // If the tree is empty, make the root the new leaf.
-        if self.root.is_none() {
+        let Some(mut index) = self.root else {
             let new_node = self.push_leaf(new_bounds, 1);
             self.root = Some(new_node);
             return 1;
-        }
+        };
 
         // Search for the best place to add the new leaf based on heuristics.
         let mut max_intersecting_ordering = 0;
-        let mut index = self.root.unwrap();
         while let Node::Internal {
             left,
             right,

crates/gpui/src/element.rs 🔗

@@ -37,11 +37,11 @@ use crate::{
     util::FluentBuilder,
 };
 use derive_more::{Deref, DerefMut};
-pub(crate) use smallvec::SmallVec;
 use std::{
     any::{Any, type_name},
     fmt::{self, Debug, Display},
     mem, panic,
+    sync::Arc,
 };
 
 /// Implemented by types that participate in laying out and painting the contents of a window.
@@ -272,8 +272,8 @@ impl<C: RenderOnce> IntoElement for Component<C> {
 }
 
 /// A globally unique identifier for an element, used to track state across frames.
-#[derive(Deref, DerefMut, Default, Debug, Eq, PartialEq, Hash)]
-pub struct GlobalElementId(pub(crate) SmallVec<[ElementId; 32]>);
+#[derive(Deref, DerefMut, Clone, Default, Debug, Eq, PartialEq, Hash)]
+pub struct GlobalElementId(pub(crate) Arc<[ElementId]>);
 
 impl Display for GlobalElementId {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
@@ -353,7 +353,7 @@ impl<E: Element> Drawable<E> {
             ElementDrawPhase::Start => {
                 let global_id = self.element.id().map(|element_id| {
                     window.element_id_stack.push(element_id);
-                    GlobalElementId(window.element_id_stack.clone())
+                    GlobalElementId(Arc::from(&*window.element_id_stack))
                 });
 
                 let inspector_id;
@@ -361,7 +361,7 @@ impl<E: Element> Drawable<E> {
                 {
                     inspector_id = self.element.source_location().map(|source| {
                         let path = crate::InspectorElementPath {
-                            global_id: GlobalElementId(window.element_id_stack.clone()),
+                            global_id: GlobalElementId(Arc::from(&*window.element_id_stack)),
                             source_location: source,
                         };
                         window.build_inspector_element_id(path)
@@ -412,7 +412,7 @@ impl<E: Element> Drawable<E> {
             } => {
                 if let Some(element_id) = self.element.id() {
                     window.element_id_stack.push(element_id);
-                    debug_assert_eq!(global_id.as_ref().unwrap().0, window.element_id_stack);
+                    debug_assert_eq!(&*global_id.as_ref().unwrap().0, &*window.element_id_stack);
                 }
 
                 let bounds = window.layout_bounds(layout_id);
@@ -461,7 +461,7 @@ impl<E: Element> Drawable<E> {
             } => {
                 if let Some(element_id) = self.element.id() {
                     window.element_id_stack.push(element_id);
-                    debug_assert_eq!(global_id.as_ref().unwrap().0, window.element_id_stack);
+                    debug_assert_eq!(&*global_id.as_ref().unwrap().0, &*window.element_id_stack);
                 }
 
                 window.next_frame.dispatch_tree.set_active_node(node_id);
@@ -741,7 +741,17 @@ impl Element for Empty {
         window: &mut Window,
         cx: &mut App,
     ) -> (LayoutId, Self::RequestLayoutState) {
-        (window.request_layout(Style::default(), None, cx), ())
+        (
+            window.request_layout(
+                Style {
+                    display: crate::Display::None,
+                    ..Default::default()
+                },
+                None,
+                cx,
+            ),
+            (),
+        )
     }
 
     fn prepaint(

crates/gpui/src/elements/div.rs 🔗

@@ -1034,6 +1034,18 @@ pub trait InteractiveElement: Sized {
         self.interactivity().in_focus_style = Some(Box::new(f(StyleRefinement::default())));
         self
     }
+
+    /// Set the given styles to be applied when this element is focused via keyboard navigation.
+    /// This is similar to CSS's `:focus-visible` pseudo-class - it only applies when the element
+    /// is focused AND the user is navigating via keyboard (not mouse clicks).
+    /// Requires that the element is focusable. Elements can be made focusable using [`InteractiveElement::track_focus`].
+    fn focus_visible(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
+    where
+        Self: Sized,
+    {
+        self.interactivity().focus_visible_style = Some(Box::new(f(StyleRefinement::default())));
+        self
+    }
 }
 
 /// A trait for elements that want to use the standard GPUI interactivity features
@@ -1497,6 +1509,7 @@ pub struct Interactivity {
     pub base_style: Box<StyleRefinement>,
     pub(crate) focus_style: Option<Box<StyleRefinement>>,
     pub(crate) in_focus_style: Option<Box<StyleRefinement>>,
+    pub(crate) focus_visible_style: Option<Box<StyleRefinement>>,
     pub(crate) hover_style: Option<Box<StyleRefinement>>,
     pub(crate) group_hover_style: Option<GroupStyle>,
     pub(crate) active_style: Option<Box<StyleRefinement>>,
@@ -2492,6 +2505,13 @@ impl Interactivity {
             {
                 style.refine(focus_style);
             }
+
+            if let Some(focus_visible_style) = self.focus_visible_style.as_ref()
+                && focus_handle.is_focused(window)
+                && window.last_input_was_keyboard()
+            {
+                style.refine(focus_visible_style);
+            }
         }
 
         if let Some(hitbox) = hitbox {

crates/gpui/src/elements/list.rs 🔗

@@ -509,10 +509,11 @@ impl StateInner {
         if self.alignment == ListAlignment::Bottom && new_scroll_top == scroll_max {
             self.logical_scroll_top = None;
         } else {
-            let mut cursor = self.items.cursor::<ListItemSummary>(());
-            cursor.seek(&Height(new_scroll_top), Bias::Right);
-            let item_ix = cursor.start().count;
-            let offset_in_item = new_scroll_top - cursor.start().height;
+            let (start, ..) =
+                self.items
+                    .find::<ListItemSummary, _>((), &Height(new_scroll_top), Bias::Right);
+            let item_ix = start.count;
+            let offset_in_item = new_scroll_top - start.height;
             self.logical_scroll_top = Some(ListOffset {
                 item_ix,
                 offset_in_item,
@@ -550,9 +551,12 @@ impl StateInner {
     }
 
     fn scroll_top(&self, logical_scroll_top: &ListOffset) -> Pixels {
-        let mut cursor = self.items.cursor::<ListItemSummary>(());
-        cursor.seek(&Count(logical_scroll_top.item_ix), Bias::Right);
-        cursor.start().height + logical_scroll_top.offset_in_item
+        let (start, ..) = self.items.find::<ListItemSummary, _>(
+            (),
+            &Count(logical_scroll_top.item_ix),
+            Bias::Right,
+        );
+        start.height + logical_scroll_top.offset_in_item
     }
 
     fn layout_all_items(
@@ -882,11 +886,12 @@ impl StateInner {
         if self.alignment == ListAlignment::Bottom && new_scroll_top == scroll_max {
             self.logical_scroll_top = None;
         } else {
-            let mut cursor = self.items.cursor::<ListItemSummary>(());
-            cursor.seek(&Height(new_scroll_top), Bias::Right);
+            let (start, _, _) =
+                self.items
+                    .find::<ListItemSummary, _>((), &Height(new_scroll_top), Bias::Right);
 
-            let item_ix = cursor.start().count;
-            let offset_in_item = new_scroll_top - cursor.start().height;
+            let item_ix = start.count;
+            let offset_in_item = new_scroll_top - start.height;
             self.logical_scroll_top = Some(ListOffset {
                 item_ix,
                 offset_in_item,

crates/gpui/src/elements/uniform_list.rs 🔗

@@ -343,7 +343,7 @@ impl Element for UniformList {
         };
         let content_size = Size {
             width: content_width,
-            height: longest_item_size.height * self.item_count + padding.top + padding.bottom,
+            height: longest_item_size.height * self.item_count,
         };
 
         let shared_scroll_offset = self.interactivity.scroll_offset.clone().unwrap();
@@ -364,17 +364,7 @@ impl Element for UniformList {
             content_size,
             window,
             cx,
-            |style, mut scroll_offset, hitbox, window, cx| {
-                let border = style.border_widths.to_pixels(window.rem_size());
-                let padding = style
-                    .padding
-                    .to_pixels(bounds.size.into(), window.rem_size());
-
-                let padded_bounds = Bounds::from_corners(
-                    bounds.origin + point(border.left + padding.left, border.top),
-                    bounds.bottom_right() - point(border.right + padding.right, border.bottom),
-                );
-
+            |_style, mut scroll_offset, hitbox, window, cx| {
                 let y_flipped = if let Some(scroll_handle) = &self.scroll_handle {
                     let scroll_state = scroll_handle.0.borrow();
                     scroll_state.y_flipped
@@ -383,13 +373,14 @@ impl Element for UniformList {
                 };
 
                 if self.item_count > 0 {
-                    let content_height =
-                        item_height * self.item_count + padding.top + padding.bottom;
+                    let content_height = item_height * self.item_count;
+
                     let is_scrolled_vertically = !scroll_offset.y.is_zero();
-                    let min_vertical_scroll_offset = padded_bounds.size.height - content_height;
-                    if is_scrolled_vertically && scroll_offset.y < min_vertical_scroll_offset {
-                        shared_scroll_offset.borrow_mut().y = min_vertical_scroll_offset;
-                        scroll_offset.y = min_vertical_scroll_offset;
+                    let max_scroll_offset = padded_bounds.size.height - content_height;
+
+                    if is_scrolled_vertically && scroll_offset.y < max_scroll_offset {
+                        shared_scroll_offset.borrow_mut().y = max_scroll_offset;
+                        scroll_offset.y = max_scroll_offset;
                     }
 
                     let content_width = content_size.width + padding.left + padding.right;
@@ -407,18 +398,19 @@ impl Element for UniformList {
                         }
                         let list_height = padded_bounds.size.height;
                         let mut updated_scroll_offset = shared_scroll_offset.borrow_mut();
-                        let item_top = item_height * ix + padding.top;
+                        let item_top = item_height * ix;
                         let item_bottom = item_top + item_height;
                         let scroll_top = -updated_scroll_offset.y;
                         let offset_pixels = item_height * deferred_scroll.offset;
                         let mut scrolled_to_top = false;
 
-                        if item_top < scroll_top + padding.top + offset_pixels {
+                        if item_top < scroll_top + offset_pixels {
                             scrolled_to_top = true;
-                            updated_scroll_offset.y = -(item_top) + padding.top + offset_pixels;
-                        } else if item_bottom > scroll_top + list_height - padding.bottom {
+                            // todo: using the padding here is wrong - this only works well for few scenarios
+                            updated_scroll_offset.y = -item_top + padding.top + offset_pixels;
+                        } else if item_bottom > scroll_top + list_height {
                             scrolled_to_top = true;
-                            updated_scroll_offset.y = -(item_bottom - list_height) - padding.bottom;
+                            updated_scroll_offset.y = -(item_bottom - list_height);
                         }
 
                         if deferred_scroll.scroll_strict
@@ -480,14 +472,9 @@ impl Element for UniformList {
                     window.with_content_mask(Some(content_mask), |window| {
                         for (mut item, ix) in items.into_iter().zip(visible_range.clone()) {
                             let item_origin = padded_bounds.origin
-                                + point(
-                                    if can_scroll_horizontally {
-                                        scroll_offset.x + padding.left
-                                    } else {
-                                        scroll_offset.x
-                                    },
-                                    item_height * ix + scroll_offset.y + padding.top,
-                                );
+                                + scroll_offset
+                                + point(Pixels::ZERO, item_height * ix);
+
                             let available_width = if can_scroll_horizontally {
                                 padded_bounds.size.width + scroll_offset.x.abs()
                             } else {
@@ -502,18 +489,8 @@ impl Element for UniformList {
                             frame_state.items.push(item);
                         }
 
-                        let bounds = Bounds::new(
-                            padded_bounds.origin
-                                + point(
-                                    if can_scroll_horizontally {
-                                        scroll_offset.x + padding.left
-                                    } else {
-                                        scroll_offset.x
-                                    },
-                                    scroll_offset.y + padding.top,
-                                ),
-                            padded_bounds.size,
-                        );
+                        let bounds =
+                            Bounds::new(padded_bounds.origin + scroll_offset, padded_bounds.size);
                         for decoration in &self.decorations {
                             let mut decoration = decoration.as_ref().compute(
                                 visible_range.clone(),

crates/gpui/src/inspector.rs 🔗

@@ -39,7 +39,7 @@ mod conditional {
     impl Clone for InspectorElementPath {
         fn clone(&self) -> Self {
             Self {
-                global_id: crate::GlobalElementId(self.global_id.0.clone()),
+                global_id: self.global_id.clone(),
                 source_location: self.source_location,
             }
         }

crates/gpui/src/key_dispatch.rs 🔗

@@ -572,18 +572,14 @@ impl DispatchTree {
         focus_path
     }
 
-    pub fn view_path(&self, view_id: EntityId) -> SmallVec<[EntityId; 8]> {
-        let mut view_path: SmallVec<[EntityId; 8]> = SmallVec::new();
+    pub fn view_path_reversed(&self, view_id: EntityId) -> impl Iterator<Item = EntityId> {
         let mut current_node_id = self.view_node_ids.get(&view_id).copied();
-        while let Some(node_id) = current_node_id {
-            let node = self.node(node_id);
-            if let Some(view_id) = node.view_id {
-                view_path.push(view_id);
-            }
-            current_node_id = node.parent;
-        }
-        view_path.reverse(); // Reverse the path so it goes from the root to the view node.
-        view_path
+
+        std::iter::successors(
+            current_node_id.map(|node_id| self.node(node_id)),
+            |node_id| Some(self.node(node_id.parent?)),
+        )
+        .filter_map(|node| node.view_id)
     }
 
     pub fn node(&self, node_id: DispatchNodeId) -> &DispatchNode {

crates/gpui/src/keymap.rs 🔗

@@ -118,10 +118,12 @@ impl Keymap {
     pub fn all_bindings_for_input(&self, input: &[Keystroke]) -> Vec<KeyBinding> {
         self.bindings()
             .rev()
-            .filter_map(|binding| {
-                binding.match_keystrokes(input).filter(|pending| !pending)?;
-                Some(binding.clone())
+            .filter(|binding| {
+                binding
+                    .match_keystrokes(input)
+                    .is_some_and(|pending| !pending)
             })
+            .cloned()
             .collect()
     }
 

crates/gpui/src/platform.rs 🔗

@@ -289,10 +289,13 @@ pub trait PlatformDisplay: Send + Sync + Debug {
 
     /// Get the default bounds for this display to place a window
     fn default_bounds(&self) -> Bounds<Pixels> {
-        let center = self.bounds().center();
-        let offset = DEFAULT_WINDOW_SIZE / 2.0;
+        let bounds = self.bounds();
+        let center = bounds.center();
+        let clipped_window_size = DEFAULT_WINDOW_SIZE.min(&bounds.size);
+
+        let offset = clipped_window_size / 2.0;
         let origin = point(center.x - offset.width, center.y - offset.height);
-        Bounds::new(origin, DEFAULT_WINDOW_SIZE)
+        Bounds::new(origin, clipped_window_size)
     }
 }
 
@@ -1641,6 +1644,8 @@ pub enum ImageFormat {
     Bmp,
     /// .tif or .tiff
     Tiff,
+    /// .ico
+    Ico,
 }
 
 impl ImageFormat {
@@ -1654,6 +1659,7 @@ impl ImageFormat {
             ImageFormat::Svg => "image/svg+xml",
             ImageFormat::Bmp => "image/bmp",
             ImageFormat::Tiff => "image/tiff",
+            ImageFormat::Ico => "image/ico",
         }
     }
 
@@ -1667,6 +1673,7 @@ impl ImageFormat {
             "image/svg+xml" => Some(Self::Svg),
             "image/bmp" => Some(Self::Bmp),
             "image/tiff" | "image/tif" => Some(Self::Tiff),
+            "image/ico" => Some(Self::Ico),
             _ => None,
         }
     }
@@ -1773,6 +1780,7 @@ impl Image {
             ImageFormat::Webp => frames_for_image(&self.bytes, image::ImageFormat::WebP)?,
             ImageFormat::Bmp => frames_for_image(&self.bytes, image::ImageFormat::Bmp)?,
             ImageFormat::Tiff => frames_for_image(&self.bytes, image::ImageFormat::Tiff)?,
+            ImageFormat::Ico => frames_for_image(&self.bytes, image::ImageFormat::Ico)?,
             ImageFormat::Svg => {
                 let pixmap = svg_renderer.render_pixmap(&self.bytes, SvgSize::ScaleFactor(1.0))?;
 

crates/gpui/src/platform/linux/x11/clipboard.rs 🔗

@@ -86,6 +86,7 @@ x11rb::atom_manager! {
         SVG__MIME: ImageFormat::mime_type(ImageFormat::Svg ).as_bytes(),
         BMP__MIME: ImageFormat::mime_type(ImageFormat::Bmp ).as_bytes(),
         TIFF_MIME: ImageFormat::mime_type(ImageFormat::Tiff).as_bytes(),
+        ICO__MIME: ImageFormat::mime_type(ImageFormat::Ico ).as_bytes(),
 
         // This is just some random name for the property on our window, into which
         // the clipboard owner writes the data we requested.
@@ -1003,6 +1004,7 @@ impl Clipboard {
             ImageFormat::Svg => self.inner.atoms.SVG__MIME,
             ImageFormat::Bmp => self.inner.atoms.BMP__MIME,
             ImageFormat::Tiff => self.inner.atoms.TIFF_MIME,
+            ImageFormat::Ico => self.inner.atoms.ICO__MIME,
         };
         let data = vec![ClipboardData {
             bytes: image.bytes,

crates/gpui/src/platform/mac/platform.rs 🔗

@@ -1607,6 +1607,7 @@ impl From<ImageFormat> for UTType {
             ImageFormat::Gif => Self::gif(),
             ImageFormat::Bmp => Self::bmp(),
             ImageFormat::Svg => Self::svg(),
+            ImageFormat::Ico => Self::ico(),
         }
     }
 }
@@ -1645,6 +1646,11 @@ impl UTType {
         Self(unsafe { ns_string("public.svg-image") })
     }
 
+    pub fn ico() -> Self {
+        // https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/ico
+        Self(unsafe { ns_string("com.microsoft.ico") })
+    }
+
     pub fn tiff() -> Self {
         // https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/tiff
         Self(unsafe { NSPasteboardTypeTIFF }) // This is a rare case where there's a built-in NSPasteboardType

crates/gpui/src/platform/windows/events.rs 🔗

@@ -1307,10 +1307,10 @@ where
     F: FnOnce(Keystroke) -> PlatformInput,
 {
     let virtual_key = VIRTUAL_KEY(wparam.loword());
-    let mut modifiers = current_modifiers();
+    let modifiers = current_modifiers();
 
     match virtual_key {
-        VK_SHIFT | VK_CONTROL | VK_MENU | VK_LWIN | VK_RWIN => {
+        VK_SHIFT | VK_CONTROL | VK_MENU | VK_LMENU | VK_RMENU | VK_LWIN | VK_RWIN => {
             if state
                 .last_reported_modifiers
                 .is_some_and(|prev_modifiers| prev_modifiers == modifiers)
@@ -1460,13 +1460,25 @@ fn is_virtual_key_pressed(vkey: VIRTUAL_KEY) -> bool {
     unsafe { GetKeyState(vkey.0 as i32) < 0 }
 }
 
+fn keyboard_uses_altgr() -> bool {
+    use crate::platform::windows::keyboard::WindowsKeyboardLayout;
+    WindowsKeyboardLayout::new()
+        .map(|layout| layout.uses_altgr())
+        .unwrap_or(false)
+}
+
 #[inline]
 pub(crate) fn current_modifiers() -> Modifiers {
-    let altgr = is_virtual_key_pressed(VK_RMENU) && is_virtual_key_pressed(VK_LCONTROL);
+    let lmenu_pressed = is_virtual_key_pressed(VK_LMENU);
+    let rmenu_pressed = is_virtual_key_pressed(VK_RMENU);
+    let lcontrol_pressed = is_virtual_key_pressed(VK_LCONTROL);
+
+    // Only treat right Alt + left Ctrl as AltGr on keyboards that actually use it
+    let altgr = keyboard_uses_altgr() && rmenu_pressed && lcontrol_pressed;
 
     Modifiers {
         control: is_virtual_key_pressed(VK_CONTROL) && !altgr,
-        alt: is_virtual_key_pressed(VK_MENU) && !altgr,
+        alt: (lmenu_pressed || rmenu_pressed) && !altgr,
         shift: is_virtual_key_pressed(VK_SHIFT),
         platform: is_virtual_key_pressed(VK_LWIN) || is_virtual_key_pressed(VK_RWIN),
         function: false,

crates/gpui/src/platform/windows/keyboard.rs 🔗

@@ -110,6 +110,38 @@ impl WindowsKeyboardLayout {
             name: "unknown".to_string(),
         }
     }
+
+    pub(crate) fn uses_altgr(&self) -> bool {
+        // Check if this is a known AltGr layout by examining the layout ID
+        // The layout ID is a hex string like "00000409" (US) or "00000407" (German)
+        // Extract the language ID (last 4 bytes)
+        let id_bytes = self.id.as_bytes();
+        if id_bytes.len() >= 4 {
+            let lang_id = &id_bytes[id_bytes.len() - 4..];
+            // List of keyboard layouts that use AltGr (non-exhaustive)
+            matches!(
+                lang_id,
+                b"0407" | // German
+                b"040C" | // French
+                b"040A" | // Spanish
+                b"0415" | // Polish
+                b"0413" | // Dutch
+                b"0816" | // Portuguese
+                b"041D" | // Swedish
+                b"0414" | // Norwegian
+                b"040B" | // Finnish
+                b"041F" | // Turkish
+                b"0419" | // Russian
+                b"0405" | // Czech
+                b"040E" | // Hungarian
+                b"0424" | // Slovenian
+                b"041B" | // Slovak
+                b"0418" // Romanian
+            )
+        } else {
+            false
+        }
+    }
 }
 
 impl WindowsKeyboardMapper {

crates/gpui/src/platform/windows/platform.rs 🔗

@@ -951,17 +951,30 @@ fn file_save_dialog(
 ) -> Result<Option<PathBuf>> {
     let dialog: IFileSaveDialog = unsafe { CoCreateInstance(&FileSaveDialog, None, CLSCTX_ALL)? };
     if !directory.to_string_lossy().is_empty()
-        && let Some(full_path) = directory.canonicalize().log_err()
+        && let Some(full_path) = directory
+            .canonicalize()
+            .context("failed to canonicalize directory")
+            .log_err()
     {
         let full_path = SanitizedPath::new(&full_path);
         let full_path_string = full_path.to_string();
         let path_item: IShellItem =
             unsafe { SHCreateItemFromParsingName(&HSTRING::from(full_path_string), None)? };
-        unsafe { dialog.SetFolder(&path_item).log_err() };
+        unsafe {
+            dialog
+                .SetFolder(&path_item)
+                .context("failed to set dialog folder")
+                .log_err()
+        };
     }
 
     if let Some(suggested_name) = suggested_name {
-        unsafe { dialog.SetFileName(&HSTRING::from(suggested_name)).log_err() };
+        unsafe {
+            dialog
+                .SetFileName(&HSTRING::from(suggested_name))
+                .context("failed to set file name")
+                .log_err()
+        };
     }
 
     unsafe {

crates/gpui/src/platform/windows/window.rs 🔗

@@ -169,7 +169,9 @@ impl WindowsWindowState {
                 length: std::mem::size_of::<WINDOWPLACEMENT>() as u32,
                 ..Default::default()
             };
-            GetWindowPlacement(self.hwnd, &mut placement).log_err();
+            GetWindowPlacement(self.hwnd, &mut placement)
+                .context("failed to get window placement")
+                .log_err();
             placement
         };
         (
@@ -254,7 +256,9 @@ impl WindowsWindowInner {
                     lock.fullscreen_restore_bounds = window_bounds;
                     let style = WINDOW_STYLE(unsafe { get_window_long(this.hwnd, GWL_STYLE) } as _);
                     let mut rc = RECT::default();
-                    unsafe { GetWindowRect(this.hwnd, &mut rc) }.log_err();
+                    unsafe { GetWindowRect(this.hwnd, &mut rc) }
+                        .context("failed to get window rect")
+                        .log_err();
                     let _ = lock.fullscreen.insert(StyleAndBounds {
                         style,
                         x: rc.left,
@@ -301,15 +305,20 @@ impl WindowsWindowInner {
         };
         match open_status.state {
             WindowOpenState::Maximized => unsafe {
-                SetWindowPlacement(self.hwnd, &open_status.placement)?;
+                SetWindowPlacement(self.hwnd, &open_status.placement)
+                    .context("failed to set window placement")?;
                 ShowWindowAsync(self.hwnd, SW_MAXIMIZE).ok()?;
             },
             WindowOpenState::Fullscreen => {
-                unsafe { SetWindowPlacement(self.hwnd, &open_status.placement)? };
+                unsafe {
+                    SetWindowPlacement(self.hwnd, &open_status.placement)
+                        .context("failed to set window placement")?
+                };
                 self.toggle_fullscreen();
             }
             WindowOpenState::Windowed => unsafe {
-                SetWindowPlacement(self.hwnd, &open_status.placement)?;
+                SetWindowPlacement(self.hwnd, &open_status.placement)
+                    .context("failed to set window placement")?;
             },
         }
         Ok(())

crates/gpui/src/taffy.rs 🔗

@@ -3,7 +3,6 @@ use crate::{
     point, size,
 };
 use collections::{FxHashMap, FxHashSet};
-use smallvec::SmallVec;
 use stacksafe::{StackSafe, stacksafe};
 use std::{fmt::Debug, ops::Range};
 use taffy::{
@@ -31,6 +30,7 @@ pub struct TaffyLayoutEngine {
     taffy: TaffyTree<NodeContext>,
     absolute_layout_bounds: FxHashMap<LayoutId, Bounds<Pixels>>,
     computed_layouts: FxHashSet<LayoutId>,
+    layout_bounds_scratch_space: Vec<LayoutId>,
 }
 
 const EXPECT_MESSAGE: &str = "we should avoid taffy layout errors by construction if possible";
@@ -43,6 +43,7 @@ impl TaffyLayoutEngine {
             taffy,
             absolute_layout_bounds: FxHashMap::default(),
             computed_layouts: FxHashSet::default(),
+            layout_bounds_scratch_space: Vec::new(),
         }
     }
 
@@ -168,7 +169,7 @@ impl TaffyLayoutEngine {
         //
 
         if !self.computed_layouts.insert(id) {
-            let mut stack = SmallVec::<[LayoutId; 64]>::new();
+            let mut stack = &mut self.layout_bounds_scratch_space;
             stack.push(id);
             while let Some(id) = stack.pop() {
                 self.absolute_layout_bounds.remove(&id);
@@ -177,7 +178,7 @@ impl TaffyLayoutEngine {
                         .children(id.into())
                         .expect(EXPECT_MESSAGE)
                         .into_iter()
-                        .map(Into::into),
+                        .map(LayoutId::from),
                 );
             }
         }

crates/gpui/src/text_system.rs 🔗

@@ -426,7 +426,6 @@ impl WindowTextSystem {
             font_runs.clear();
             let line_end = line_start + line_text.len();
 
-            let mut last_font: Option<FontId> = None;
             let mut decoration_runs = SmallVec::<[DecorationRun; 32]>::new();
             let mut run_start = line_start;
             while run_start < line_end {
@@ -455,14 +454,13 @@ impl WindowTextSystem {
                     true
                 };
 
+                let font_id = self.resolve_font(&run.font);
                 if let Some(font_run) = font_runs.last_mut()
-                    && Some(font_run.font_id) == last_font
+                    && font_id == font_run.font_id
                     && !decoration_changed
                 {
                     font_run.len += run_len_within_line;
                 } else {
-                    let font_id = self.resolve_font(&run.font);
-                    last_font = Some(font_id);
                     font_runs.push(FontRun {
                         len: run_len_within_line,
                         font_id,

crates/gpui/src/window.rs 🔗

@@ -60,6 +60,13 @@ pub use prompts::*;
 
 pub(crate) const DEFAULT_WINDOW_SIZE: Size<Pixels> = size(px(1536.), px(864.));
 
+/// A 6:5 aspect ratio minimum window size to be used for functional,
+/// additional-to-main-Zed windows, like the settings and rules library windows.
+pub const DEFAULT_ADDITIONAL_WINDOW_SIZE: Size<Pixels> = Size {
+    width: Pixels(900.),
+    height: Pixels(750.),
+};
+
 /// Represents the two different phases when dispatching events.
 #[derive(Default, Copy, Clone, Debug, Eq, PartialEq)]
 pub enum DispatchPhase {
@@ -863,6 +870,7 @@ pub struct Window {
     hovered: Rc<Cell<bool>>,
     pub(crate) needs_present: Rc<Cell<bool>>,
     pub(crate) last_input_timestamp: Rc<Cell<Instant>>,
+    last_input_was_keyboard: bool,
     pub(crate) refreshing: bool,
     pub(crate) activation_observers: SubscriberSet<(), AnyObserver>,
     pub(crate) focus: Option<FocusId>,
@@ -1246,6 +1254,7 @@ impl Window {
             hovered,
             needs_present,
             last_input_timestamp,
+            last_input_was_keyboard: false,
             refreshing: false,
             activation_observers: SubscriberSet::new(),
             focus: None,
@@ -1307,9 +1316,7 @@ impl Window {
         for view_id in self
             .rendered_frame
             .dispatch_tree
-            .view_path(view_id)
-            .into_iter()
-            .rev()
+            .view_path_reversed(view_id)
         {
             if !self.dirty_views.insert(view_id) {
                 break;
@@ -1839,7 +1846,8 @@ impl Window {
         f: impl FnOnce(&GlobalElementId, &mut Self) -> R,
     ) -> R {
         self.element_id_stack.push(element_id);
-        let global_id = GlobalElementId(self.element_id_stack.clone());
+        let global_id = GlobalElementId(Arc::from(&*self.element_id_stack));
+
         let result = f(&global_id, self);
         self.element_id_stack.pop();
         result
@@ -1899,6 +1907,12 @@ impl Window {
         self.modifiers
     }
 
+    /// Returns true if the last input event was keyboard-based (key press, tab navigation, etc.)
+    /// This is used for focus-visible styling to show focus indicators only for keyboard navigation.
+    pub fn last_input_was_keyboard(&self) -> bool {
+        self.last_input_was_keyboard
+    }
+
     /// The current state of the keyboard's capslock
     pub fn capslock(&self) -> Capslock {
         self.capslock
@@ -2245,7 +2259,7 @@ impl Window {
             self.rendered_frame.accessed_element_states[range.start.accessed_element_states_index
                 ..range.end.accessed_element_states_index]
                 .iter()
-                .map(|(id, type_id)| (GlobalElementId(id.0.clone()), *type_id)),
+                .map(|(id, type_id)| (id.clone(), *type_id)),
         );
         self.text_system
             .reuse_layouts(range.start.line_layout_index..range.end.line_layout_index);
@@ -2313,7 +2327,7 @@ impl Window {
             self.rendered_frame.accessed_element_states[range.start.accessed_element_states_index
                 ..range.end.accessed_element_states_index]
                 .iter()
-                .map(|(id, type_id)| (GlobalElementId(id.0.clone()), *type_id)),
+                .map(|(id, type_id)| (id.clone(), *type_id)),
         );
         self.next_frame.tab_stops.replay(
             &self.rendered_frame.tab_stops.insertion_history
@@ -2635,10 +2649,8 @@ impl Window {
     {
         self.invalidator.debug_assert_paint_or_prepaint();
 
-        let key = (GlobalElementId(global_id.0.clone()), TypeId::of::<S>());
-        self.next_frame
-            .accessed_element_states
-            .push((GlobalElementId(key.0.clone()), TypeId::of::<S>()));
+        let key = (global_id.clone(), TypeId::of::<S>());
+        self.next_frame.accessed_element_states.push(key.clone());
 
         if let Some(any) = self
             .next_frame
@@ -3580,6 +3592,15 @@ impl Window {
     #[profiling::function]
     pub fn dispatch_event(&mut self, event: PlatformInput, cx: &mut App) -> DispatchEventResult {
         self.last_input_timestamp.set(Instant::now());
+
+        // Track whether this input was keyboard-based for focus-visible styling
+        self.last_input_was_keyboard = matches!(
+            event,
+            PlatformInput::KeyDown(_)
+                | PlatformInput::KeyUp(_)
+                | PlatformInput::ModifiersChanged(_)
+        );
+
         // Handlers may set this to false by calling `stop_propagation`.
         cx.propagate_event = true;
         // Handlers may set this to true by calling `prevent_default`.
@@ -4313,14 +4334,14 @@ impl Window {
     }
 
     /// Returns a generic handler that invokes the given handler with the view and context associated with the given view handle.
-    pub fn handler_for<V: Render, Callback: Fn(&mut V, &mut Window, &mut Context<V>) + 'static>(
+    pub fn handler_for<E: 'static, Callback: Fn(&mut E, &mut Window, &mut Context<E>) + 'static>(
         &self,
-        view: &Entity<V>,
+        entity: &Entity<E>,
         f: Callback,
-    ) -> impl Fn(&mut Window, &mut App) + use<V, Callback> {
-        let view = view.downgrade();
+    ) -> impl Fn(&mut Window, &mut App) + 'static {
+        let entity = entity.downgrade();
         move |window: &mut Window, cx: &mut App| {
-            view.update(cx, |view, cx| f(view, window, cx)).ok();
+            entity.update(cx, |entity, cx| f(entity, window, cx)).ok();
         }
     }
 
@@ -4718,7 +4739,7 @@ impl<V: 'static + Render> WindowHandle<V> {
             .get(self.id)
             .and_then(|window| {
                 window
-                    .as_ref()
+                    .as_deref()
                     .and_then(|window| window.root.clone())
                     .map(|root_view| root_view.downcast::<V>())
             })
@@ -4879,7 +4900,7 @@ pub enum ElementId {
     /// A code location.
     CodeLocation(core::panic::Location<'static>),
     /// A labeled child of an element.
-    NamedChild(Box<ElementId>, SharedString),
+    NamedChild(Arc<ElementId>, SharedString),
 }
 
 impl ElementId {
@@ -4993,7 +5014,7 @@ impl From<(&'static str, u32)> for ElementId {
 
 impl<T: Into<SharedString>> From<(ElementId, T)> for ElementId {
     fn from((id, name): (ElementId, T)) -> Self {
-        ElementId::NamedChild(Box::new(id), name.into())
+        ElementId::NamedChild(Arc::new(id), name.into())
     }
 }
 

crates/gpui_macros/Cargo.toml 🔗

@@ -22,7 +22,6 @@ heck.workspace = true
 proc-macro2.workspace = true
 quote.workspace = true
 syn.workspace = true
-workspace-hack.workspace = true
 
 [dev-dependencies]
 gpui = { workspace = true, features = ["inspector"] }

crates/gpui_tokio/Cargo.toml 🔗

@@ -17,4 +17,3 @@ anyhow.workspace = true
 util.workspace = true
 gpui.workspace = true
 tokio = { workspace = true, features = ["rt", "rt-multi-thread"] }
-workspace-hack.workspace = true

crates/html_to_markdown/Cargo.toml 🔗

@@ -20,7 +20,6 @@ anyhow.workspace = true
 html5ever.workspace = true
 markup5ever_rcdom.workspace = true
 regex.workspace = true
-workspace-hack.workspace = true
 
 [dev-dependencies]
 indoc.workspace = true

crates/http_client/Cargo.toml 🔗

@@ -35,4 +35,3 @@ sha2.workspace = true
 tempfile.workspace = true
 url.workspace = true
 util.workspace = true
-workspace-hack.workspace = true

crates/http_client_tls/Cargo.toml 🔗

@@ -18,4 +18,3 @@ doctest = true
 [dependencies]
 rustls.workspace = true
 rustls-platform-verifier.workspace = true
-workspace-hack.workspace = true

crates/icons/Cargo.toml 🔗

@@ -14,4 +14,3 @@ path = "src/icons.rs"
 [dependencies]
 serde.workspace = true
 strum.workspace = true
-workspace-hack.workspace = true

crates/image_viewer/Cargo.toml 🔗

@@ -30,7 +30,6 @@ theme.workspace = true
 ui.workspace = true
 util.workspace = true
 workspace.workspace = true
-workspace-hack.workspace = true
 
 [dev-dependencies]
 editor = { workspace = true, features = ["test-support"] }

crates/image_viewer/src/image_viewer.rs 🔗

@@ -1,6 +1,8 @@
 mod image_info;
 mod image_viewer_settings;
 
+use std::path::Path;
+
 use anyhow::Context as _;
 use editor::{EditorSettings, items::entry_git_aware_label_color};
 use file_icons::FileIcons;
@@ -13,11 +15,12 @@ use language::{DiskState, File as _};
 use persistence::IMAGE_VIEWER;
 use project::{ImageItem, Project, ProjectPath, image_store::ImageItemEvent};
 use settings::Settings;
-use theme::Theme;
+use theme::{Theme, ThemeSettings};
 use ui::prelude::*;
 use util::paths::PathExt;
 use workspace::{
     ItemId, ItemSettings, Pane, ToolbarItemLocation, Workspace, WorkspaceId, delete_unloaded_items,
+    invalid_item_view::InvalidItemView,
     item::{BreadcrumbText, Item, ProjectItem, SerializableItem, TabContentParams},
 };
 
@@ -162,10 +165,12 @@ impl Item for ImageView {
 
     fn breadcrumbs(&self, _theme: &Theme, cx: &App) -> Option<Vec<BreadcrumbText>> {
         let text = breadcrumbs_text_for_image(self.project.read(cx), self.image_item.read(cx), cx);
+        let settings = ThemeSettings::get_global(cx);
+
         Some(vec![BreadcrumbText {
             text,
             highlights: None,
-            font: None,
+            font: Some(settings.buffer_font.clone()),
         }])
     }
 
@@ -174,15 +179,15 @@ impl Item for ImageView {
         _workspace_id: Option<WorkspaceId>,
         _: &mut Window,
         cx: &mut Context<Self>,
-    ) -> Option<Entity<Self>>
+    ) -> Task<Option<Entity<Self>>>
     where
         Self: Sized,
     {
-        Some(cx.new(|cx| Self {
+        Task::ready(Some(cx.new(|cx| Self {
             image_item: self.image_item.clone(),
             project: self.project.clone(),
             focus_handle: cx.focus_handle(),
-        }))
+        })))
     }
 
     fn has_deleted_file(&self, cx: &App) -> bool {
@@ -387,6 +392,19 @@ impl ProjectItem for ImageView {
     {
         Self::new(item, project, window, cx)
     }
+
+    fn for_broken_project_item(
+        abs_path: &Path,
+        is_local: bool,
+        e: &anyhow::Error,
+        window: &mut Window,
+        cx: &mut App,
+    ) -> Option<InvalidItemView>
+    where
+        Self: Sized,
+    {
+        Some(InvalidItemView::new(abs_path, is_local, e, window, cx))
+    }
 }
 
 pub fn init(cx: &mut App) {

crates/inspector_ui/Cargo.toml 🔗

@@ -26,6 +26,5 @@ title_bar.workspace = true
 ui.workspace = true
 util.workspace = true
 util_macros.workspace = true
-workspace-hack.workspace = true
 workspace.workspace = true
 zed_actions.workspace = true

crates/install_cli/Cargo.toml 🔗

@@ -21,5 +21,4 @@ gpui.workspace = true
 release_channel.workspace = true
 smol.workspace = true
 util.workspace = true
-workspace-hack.workspace = true
 workspace.workspace = true

crates/journal/Cargo.toml 🔗

@@ -22,7 +22,6 @@ serde.workspace = true
 settings.workspace = true
 shellexpand.workspace = true
 workspace.workspace = true
-workspace-hack.workspace = true
 
 [dev-dependencies]
 editor = { workspace = true, features = ["test-support"] }

crates/keymap_editor/Cargo.toml 🔗

@@ -42,7 +42,6 @@ ui_input.workspace = true
 ui.workspace = true
 util.workspace = true
 vim.workspace = true
-workspace-hack.workspace = true
 workspace.workspace = true
 zed_actions.workspace = true
 

crates/keymap_editor/src/keymap_editor.rs 🔗

@@ -1,6 +1,7 @@
 use std::{
     cmp::{self},
     ops::{Not as _, Range},
+    rc::Rc,
     sync::Arc,
     time::Duration,
 };
@@ -32,7 +33,7 @@ use ui::{
     SharedString, Styled as _, Table, TableColumnWidths, TableInteractionState,
     TableResizeBehavior, Tooltip, Window, prelude::*,
 };
-use ui_input::SingleLineInput;
+use ui_input::InputField;
 use util::ResultExt;
 use workspace::{
     Item, ModalView, SerializableItem, Workspace, notifications::NotifyTaskExt as _,
@@ -173,7 +174,7 @@ impl FilterState {
 
 #[derive(Debug, Default, PartialEq, Eq, Clone, Hash)]
 struct ActionMapping {
-    keystrokes: Vec<KeybindingKeystroke>,
+    keystrokes: Rc<[KeybindingKeystroke]>,
     context: Option<SharedString>,
 }
 
@@ -235,7 +236,7 @@ struct ConflictState {
 }
 
 type ConflictKeybindMapping = HashMap<
-    Vec<KeybindingKeystroke>,
+    Rc<[KeybindingKeystroke]>,
     Vec<(
         Option<gpui::KeyBindingContextPredicate>,
         Vec<ConflictOrigin>,
@@ -257,7 +258,7 @@ impl ConflictState {
                 .context
                 .and_then(|ctx| gpui::KeyBindingContextPredicate::parse(&ctx).ok());
             let entry = action_keybind_mapping
-                .entry(mapping.keystrokes)
+                .entry(mapping.keystrokes.clone())
                 .or_default();
             let origin = ConflictOrigin::new(binding.source, index);
             if let Some((_, origins)) =
@@ -685,8 +686,7 @@ impl KeymapEditor {
                 .unwrap_or(KeybindSource::Unknown);
 
             let keystroke_text = ui::text_for_keybinding_keystrokes(key_binding.keystrokes(), cx);
-            let ui_key_binding = ui::KeyBinding::new_from_gpui(key_binding.clone(), cx)
-                .vim_mode(source == KeybindSource::Vim);
+            let binding = KeyBinding::new(key_binding, source);
 
             let context = key_binding
                 .predicate()
@@ -717,7 +717,7 @@ impl KeymapEditor {
                 StringMatchCandidate::new(index, &action_information.humanized_name);
             processed_bindings.push(ProcessedBinding::new_mapped(
                 keystroke_text,
-                ui_key_binding,
+                binding,
                 context,
                 source,
                 action_information,
@@ -975,12 +975,11 @@ impl KeymapEditor {
             if conflict.is_user_keybind_conflict() {
                 base_button_style(index, IconName::Warning)
                     .icon_color(Color::Warning)
-                    .tooltip(|window, cx| {
+                    .tooltip(|_window, cx| {
                         Tooltip::with_meta(
                             "View conflicts",
                             Some(&ToggleConflictFilter),
                             "Use alt+click to show all conflicts",
-                            window,
                             cx,
                         )
                     })
@@ -995,12 +994,11 @@ impl KeymapEditor {
                     }))
             } else if self.search_mode.exact_match() {
                 base_button_style(index, IconName::Info)
-                    .tooltip(|window, cx| {
+                    .tooltip(|_window, cx| {
                         Tooltip::with_meta(
                             "Edit this binding",
                             Some(&ShowMatchingKeybinds),
                             "This binding is overridden by other bindings.",
-                            window,
                             cx,
                         )
                     })
@@ -1011,12 +1009,11 @@ impl KeymapEditor {
                     }))
             } else {
                 base_button_style(index, IconName::Info)
-                    .tooltip(|window, cx| {
+                    .tooltip(|_window, cx|  {
                         Tooltip::with_meta(
                             "Show matching keybinds",
                             Some(&ShowMatchingKeybinds),
                             "This binding is overridden by other bindings.\nUse alt+click to edit this binding",
-                            window,
                             cx,
                         )
                     })
@@ -1348,10 +1345,25 @@ impl HumanizedActionNameCache {
     }
 }
 
+#[derive(Clone)]
+struct KeyBinding {
+    keystrokes: Rc<[KeybindingKeystroke]>,
+    source: KeybindSource,
+}
+
+impl KeyBinding {
+    fn new(binding: &gpui::KeyBinding, source: KeybindSource) -> Self {
+        Self {
+            keystrokes: Rc::from(binding.keystrokes()),
+            source,
+        }
+    }
+}
+
 #[derive(Clone)]
 struct KeybindInformation {
     keystroke_text: SharedString,
-    ui_binding: ui::KeyBinding,
+    binding: KeyBinding,
     context: KeybindContextString,
     source: KeybindSource,
 }
@@ -1359,7 +1371,7 @@ struct KeybindInformation {
 impl KeybindInformation {
     fn get_action_mapping(&self) -> ActionMapping {
         ActionMapping {
-            keystrokes: self.ui_binding.keystrokes.clone(),
+            keystrokes: self.binding.keystrokes.clone(),
             context: self.context.local().cloned(),
         }
     }
@@ -1401,7 +1413,7 @@ enum ProcessedBinding {
 impl ProcessedBinding {
     fn new_mapped(
         keystroke_text: impl Into<SharedString>,
-        ui_key_binding: ui::KeyBinding,
+        binding: KeyBinding,
         context: KeybindContextString,
         source: KeybindSource,
         action_information: ActionInformation,
@@ -1409,7 +1421,7 @@ impl ProcessedBinding {
         Self::Mapped(
             KeybindInformation {
                 keystroke_text: keystroke_text.into(),
-                ui_binding: ui_key_binding,
+                binding,
                 context,
                 source,
             },
@@ -1427,8 +1439,8 @@ impl ProcessedBinding {
     }
 
     fn keystrokes(&self) -> Option<&[KeybindingKeystroke]> {
-        self.ui_key_binding()
-            .map(|binding| binding.keystrokes.as_slice())
+        self.key_binding()
+            .map(|binding| binding.keystrokes.as_ref())
     }
 
     fn keybind_information(&self) -> Option<&KeybindInformation> {
@@ -1446,9 +1458,8 @@ impl ProcessedBinding {
         self.keybind_information().map(|keybind| &keybind.context)
     }
 
-    fn ui_key_binding(&self) -> Option<&ui::KeyBinding> {
-        self.keybind_information()
-            .map(|keybind| &keybind.ui_binding)
+    fn key_binding(&self) -> Option<&KeyBinding> {
+        self.keybind_information().map(|keybind| &keybind.binding)
     }
 
     fn keystroke_text(&self) -> Option<&SharedString> {
@@ -1599,12 +1610,11 @@ impl Render for KeymapEditor {
                                         .tooltip({
                                             let focus_handle = focus_handle.clone();
 
-                                            move |window, cx| {
+                                            move |_window, cx| {
                                                 Tooltip::for_action_in(
                                                     "Search by Keystroke",
                                                     &ToggleKeystrokeSearch,
                                                     &focus_handle.clone(),
-                                                    window,
                                                     cx,
                                                 )
                                             }
@@ -1636,7 +1646,7 @@ impl Render for KeymapEditor {
                                                 let filter_state = self.filter_state;
                                                 let focus_handle = focus_handle.clone();
 
-                                                move |window, cx| {
+                                                move |_window, cx| {
                                                     Tooltip::for_action_in(
                                                         match filter_state {
                                                             FilterState::All => "Show Conflicts",
@@ -1646,7 +1656,6 @@ impl Render for KeymapEditor {
                                                         },
                                                         &ToggleConflictFilter,
                                                         &focus_handle.clone(),
-                                                        window,
                                                         cx,
                                                     )
                                                 }
@@ -1698,12 +1707,11 @@ impl Render for KeymapEditor {
                                                         .icon_size(IconSize::Small),
                                                         {
                                                             let focus_handle = focus_handle.clone();
-                                                            move |window, cx| {
+                                                            move |_window, cx| {
                                                                 Tooltip::for_action_in(
                                                                     "View Default...",
                                                                     &zed_actions::OpenKeymapFile,
                                                                     &focus_handle,
-                                                                    window,
                                                                     cx,
                                                                 )
                                                             }
@@ -1745,12 +1753,11 @@ impl Render for KeymapEditor {
                                                     let keystroke_focus_handle =
                                                         self.keystroke_editor.read(cx).focus_handle(cx);
 
-                                                    move |window, cx| {
+                                                    move |_window, cx| {
                                                         Tooltip::for_action_in(
                                                             "Toggle Exact Match Mode",
                                                             &ToggleExactKeystrokeMatching,
                                                             &keystroke_focus_handle,
-                                                            window,
                                                             cx,
                                                         )
                                                     }
@@ -1856,13 +1863,13 @@ impl Render for KeymapEditor {
                                         )
                                         .into_any_element();
 
-                                    let keystrokes = binding.ui_key_binding().cloned().map_or(
+                                    let keystrokes = binding.key_binding().map_or(
                                         binding
                                             .keystroke_text()
                                             .cloned()
                                             .unwrap_or_default()
                                             .into_any_element(),
-                                        IntoElement::into_any_element,
+                                        |binding| ui::KeyBinding::from_keystrokes(binding.keystrokes.clone(), binding.source).into_any_element()
                                     );
 
                                     let action_arguments = match binding.action().arguments.clone()
@@ -2114,7 +2121,7 @@ struct KeybindingEditorModal {
     editing_keybind: ProcessedBinding,
     editing_keybind_idx: usize,
     keybind_editor: Entity<KeystrokeInput>,
-    context_editor: Entity<SingleLineInput>,
+    context_editor: Entity<InputField>,
     action_arguments_editor: Option<Entity<ActionArgumentsEditor>>,
     fs: Arc<dyn Fs>,
     error: Option<InputError>,
@@ -2148,8 +2155,8 @@ impl KeybindingEditorModal {
         let keybind_editor = cx
             .new(|cx| KeystrokeInput::new(editing_keybind.keystrokes().map(Vec::from), window, cx));
 
-        let context_editor: Entity<SingleLineInput> = cx.new(|cx| {
-            let input = SingleLineInput::new(window, cx, "Keybinding Context")
+        let context_editor: Entity<InputField> = cx.new(|cx| {
+            let input = InputField::new(window, cx, "Keybinding Context")
                 .label("Edit Context")
                 .label_size(LabelSize::Default);
 
@@ -2301,7 +2308,7 @@ impl KeybindingEditorModal {
             .map_err(InputError::error)?;
 
         let action_mapping = ActionMapping {
-            keystrokes: new_keystrokes,
+            keystrokes: Rc::from(new_keystrokes.as_slice()),
             context: new_context.map(SharedString::from),
         };
 

crates/language/Cargo.toml 🔗

@@ -67,7 +67,7 @@ tree-sitter.workspace = true
 unicase = "2.6"
 util.workspace = true
 watch.workspace = true
-workspace-hack.workspace = true
+zlog.workspace = true
 diffy = "0.4.2"
 
 [dev-dependencies]

crates/language/src/buffer.rs 🔗

@@ -18,8 +18,8 @@ pub use crate::{
     proto,
 };
 use anyhow::{Context as _, Result};
+use clock::Lamport;
 pub use clock::ReplicaId;
-use clock::{AGENT_REPLICA_ID, Lamport};
 use collections::HashMap;
 use fs::MTime;
 use futures::channel::oneshot;
@@ -506,15 +506,15 @@ pub struct Chunk<'a> {
     pub highlight_style: Option<HighlightStyle>,
     /// The severity of diagnostic associated with this chunk, if any.
     pub diagnostic_severity: Option<DiagnosticSeverity>,
-    /// Whether this chunk of text is marked as unnecessary.
-    pub is_unnecessary: bool,
-    /// Whether this chunk of text was originally a tab character.
-    pub is_tab: bool,
     /// A bitset of which characters are tabs in this string.
     pub tabs: u128,
     /// Bitmap of character indices in this chunk
     pub chars: u128,
+    /// Whether this chunk of text is marked as unnecessary.
+    pub is_unnecessary: bool,
     /// Whether this chunk of text was originally a tab character.
+    pub is_tab: bool,
+    /// Whether this chunk of text was originally an inlay.
     pub is_inlay: bool,
     /// Whether to underline the corresponding text range in the editor.
     pub underline: bool,
@@ -828,7 +828,11 @@ impl Buffer {
     /// Create a new buffer with the given base text.
     pub fn local<T: Into<String>>(base_text: T, cx: &Context<Self>) -> Self {
         Self::build(
-            TextBuffer::new(0, cx.entity_id().as_non_zero_u64().into(), base_text.into()),
+            TextBuffer::new(
+                ReplicaId::LOCAL,
+                cx.entity_id().as_non_zero_u64().into(),
+                base_text.into(),
+            ),
             None,
             Capability::ReadWrite,
         )
@@ -842,7 +846,7 @@ impl Buffer {
     ) -> Self {
         Self::build(
             TextBuffer::new_normalized(
-                0,
+                ReplicaId::LOCAL,
                 cx.entity_id().as_non_zero_u64().into(),
                 line_ending,
                 base_text_normalized,
@@ -991,10 +995,10 @@ impl Buffer {
             language: None,
             remote_selections: Default::default(),
             diagnostics: Default::default(),
-            diagnostics_timestamp: Default::default(),
+            diagnostics_timestamp: Lamport::MIN,
             completion_triggers: Default::default(),
             completion_triggers_per_language_server: Default::default(),
-            completion_triggers_timestamp: Default::default(),
+            completion_triggers_timestamp: Lamport::MIN,
             deferred_ops: OperationQueue::new(),
             has_conflict: false,
             change_bits: Default::default(),
@@ -1012,7 +1016,8 @@ impl Buffer {
         let buffer_id = entity_id.as_non_zero_u64().into();
         async move {
             let text =
-                TextBuffer::new_normalized(0, buffer_id, Default::default(), text).snapshot();
+                TextBuffer::new_normalized(ReplicaId::LOCAL, buffer_id, Default::default(), text)
+                    .snapshot();
             let mut syntax = SyntaxMap::new(&text).snapshot();
             if let Some(language) = language.clone() {
                 let language_registry = language_registry.clone();
@@ -1033,8 +1038,13 @@ impl Buffer {
     pub fn build_empty_snapshot(cx: &mut App) -> BufferSnapshot {
         let entity_id = cx.reserve_entity::<Self>().entity_id();
         let buffer_id = entity_id.as_non_zero_u64().into();
-        let text =
-            TextBuffer::new_normalized(0, buffer_id, Default::default(), Rope::new()).snapshot();
+        let text = TextBuffer::new_normalized(
+            ReplicaId::LOCAL,
+            buffer_id,
+            Default::default(),
+            Rope::new(),
+        )
+        .snapshot();
         let syntax = SyntaxMap::new(&text).snapshot();
         BufferSnapshot {
             text,
@@ -1056,7 +1066,9 @@ impl Buffer {
     ) -> BufferSnapshot {
         let entity_id = cx.reserve_entity::<Self>().entity_id();
         let buffer_id = entity_id.as_non_zero_u64().into();
-        let text = TextBuffer::new_normalized(0, buffer_id, Default::default(), text).snapshot();
+        let text =
+            TextBuffer::new_normalized(ReplicaId::LOCAL, buffer_id, Default::default(), text)
+                .snapshot();
         let mut syntax = SyntaxMap::new(&text).snapshot();
         if let Some(language) = language.clone() {
             syntax.reparse(&text, language_registry, language);
@@ -2066,12 +2078,15 @@ impl Buffer {
         }
     }
 
+    /// Set the change bit for all "listeners".
     fn was_changed(&mut self) {
         self.change_bits.retain(|change_bit| {
-            change_bit.upgrade().is_some_and(|bit| {
-                bit.replace(true);
-                true
-            })
+            change_bit
+                .upgrade()
+                .inspect(|bit| {
+                    _ = bit.replace(true);
+                })
+                .is_some()
         });
     }
 
@@ -2260,7 +2275,7 @@ impl Buffer {
     ) {
         let lamport_timestamp = self.text.lamport_clock.tick();
         self.remote_selections.insert(
-            AGENT_REPLICA_ID,
+            ReplicaId::AGENT,
             SelectionSet {
                 selections,
                 lamport_timestamp,
@@ -2917,7 +2932,7 @@ impl Buffer {
 
             edits.push((range, new_text));
         }
-        log::info!("mutating buffer {} with {:?}", self.replica_id(), edits);
+        log::info!("mutating buffer {:?} with {:?}", self.replica_id(), edits);
         self.edit(edits, None, cx);
     }
 
@@ -4970,7 +4985,7 @@ impl<'a> Iterator for BufferChunks<'a> {
             text: chunk,
             chars: chars_map,
             tabs,
-        }) = self.chunks.peek_tabs()
+        }) = self.chunks.peek_with_bitmaps()
         {
             let chunk_start = self.range.start;
             let mut chunk_end = (self.chunks.offset() + chunk.len())
@@ -4983,18 +4998,14 @@ impl<'a> Iterator for BufferChunks<'a> {
                 chunk_end = chunk_end.min(*parent_capture_end);
                 highlight_id = Some(*parent_highlight_id);
             }
-
-            let slice =
-                &chunk[chunk_start - self.chunks.offset()..chunk_end - self.chunks.offset()];
+            let bit_start = chunk_start - self.chunks.offset();
             let bit_end = chunk_end - self.chunks.offset();
 
-            let mask = if bit_end >= 128 {
-                u128::MAX
-            } else {
-                (1u128 << bit_end) - 1
-            };
-            let tabs = (tabs >> (chunk_start - self.chunks.offset())) & mask;
-            let chars_map = (chars_map >> (chunk_start - self.chunks.offset())) & mask;
+            let slice = &chunk[bit_start..bit_end];
+
+            let mask = 1u128.unbounded_shl(bit_end as u32).wrapping_sub(1);
+            let tabs = (tabs >> bit_start) & mask;
+            let chars = (chars_map >> bit_start) & mask;
 
             self.range.start = chunk_end;
             if self.range.start == self.chunks.offset() + chunk.len() {
@@ -5008,7 +5019,7 @@ impl<'a> Iterator for BufferChunks<'a> {
                 diagnostic_severity: self.current_diagnostic_severity(),
                 is_unnecessary: self.current_code_is_unnecessary(),
                 tabs,
-                chars: chars_map,
+                chars,
                 ..Chunk::default()
             })
         } else {

crates/language/src/buffer_tests.rs 🔗

@@ -70,7 +70,13 @@ fn test_line_endings(cx: &mut gpui::App) {
 fn test_set_line_ending(cx: &mut TestAppContext) {
     let base = cx.new(|cx| Buffer::local("one\ntwo\nthree\n", cx));
     let base_replica = cx.new(|cx| {
-        Buffer::from_proto(1, Capability::ReadWrite, base.read(cx).to_proto(cx), None).unwrap()
+        Buffer::from_proto(
+            ReplicaId::new(1),
+            Capability::ReadWrite,
+            base.read(cx).to_proto(cx),
+            None,
+        )
+        .unwrap()
     });
     base.update(cx, |_buffer, cx| {
         cx.subscribe(&base_replica, |this, _, event, cx| {
@@ -397,7 +403,7 @@ fn test_edit_events(cx: &mut gpui::App) {
     let buffer2 = cx.new(|cx| {
         Buffer::remote(
             BufferId::from(cx.entity_id().as_non_zero_u64()),
-            1,
+            ReplicaId::new(1),
             Capability::ReadWrite,
             "abcdef",
         )
@@ -2775,7 +2781,8 @@ fn test_serialization(cx: &mut gpui::App) {
         .background_executor()
         .block(buffer1.read(cx).serialize_ops(None, cx));
     let buffer2 = cx.new(|cx| {
-        let mut buffer = Buffer::from_proto(1, Capability::ReadWrite, state, None).unwrap();
+        let mut buffer =
+            Buffer::from_proto(ReplicaId::new(1), Capability::ReadWrite, state, None).unwrap();
         buffer.apply_ops(
             ops.into_iter()
                 .map(|op| proto::deserialize_operation(op).unwrap()),
@@ -2794,7 +2801,13 @@ fn test_branch_and_merge(cx: &mut TestAppContext) {
 
     // Create a remote replica of the base buffer.
     let base_replica = cx.new(|cx| {
-        Buffer::from_proto(1, Capability::ReadWrite, base.read(cx).to_proto(cx), None).unwrap()
+        Buffer::from_proto(
+            ReplicaId::new(1),
+            Capability::ReadWrite,
+            base.read(cx).to_proto(cx),
+            None,
+        )
+        .unwrap()
     });
     base.update(cx, |_buffer, cx| {
         cx.subscribe(&base_replica, |this, _, event, cx| {
@@ -3108,7 +3121,8 @@ fn test_random_collaboration(cx: &mut App, mut rng: StdRng) {
                 .background_executor()
                 .block(base_buffer.read(cx).serialize_ops(None, cx));
             let mut buffer =
-                Buffer::from_proto(i as ReplicaId, Capability::ReadWrite, state, None).unwrap();
+                Buffer::from_proto(ReplicaId::new(i as u16), Capability::ReadWrite, state, None)
+                    .unwrap();
             buffer.apply_ops(
                 ops.into_iter()
                     .map(|op| proto::deserialize_operation(op).unwrap()),
@@ -3133,9 +3147,9 @@ fn test_random_collaboration(cx: &mut App, mut rng: StdRng) {
         });
 
         buffers.push(buffer);
-        replica_ids.push(i as ReplicaId);
-        network.lock().add_peer(i as ReplicaId);
-        log::info!("Adding initial peer with replica id {}", i);
+        replica_ids.push(ReplicaId::new(i as u16));
+        network.lock().add_peer(ReplicaId::new(i as u16));
+        log::info!("Adding initial peer with replica id {:?}", replica_ids[i]);
     }
 
     log::info!("initial text: {:?}", base_text);
@@ -3155,14 +3169,14 @@ fn test_random_collaboration(cx: &mut App, mut rng: StdRng) {
                     buffer.start_transaction_at(now);
                     buffer.randomly_edit(&mut rng, 5, cx);
                     buffer.end_transaction_at(now, cx);
-                    log::info!("buffer {} text: {:?}", buffer.replica_id(), buffer.text());
+                    log::info!("buffer {:?} text: {:?}", buffer.replica_id(), buffer.text());
                 });
                 mutation_count -= 1;
             }
             30..=39 if mutation_count != 0 => {
                 buffer.update(cx, |buffer, cx| {
                     if rng.random_bool(0.2) {
-                        log::info!("peer {} clearing active selections", replica_id);
+                        log::info!("peer {:?} clearing active selections", replica_id);
                         active_selections.remove(&replica_id);
                         buffer.remove_active_selections(cx);
                     } else {
@@ -3179,7 +3193,7 @@ fn test_random_collaboration(cx: &mut App, mut rng: StdRng) {
                         }
                         let selections: Arc<[Selection<Anchor>]> = selections.into();
                         log::info!(
-                            "peer {} setting active selections: {:?}",
+                            "peer {:?} setting active selections: {:?}",
                             replica_id,
                             selections
                         );
@@ -3189,7 +3203,7 @@ fn test_random_collaboration(cx: &mut App, mut rng: StdRng) {
                 });
                 mutation_count -= 1;
             }
-            40..=49 if mutation_count != 0 && replica_id == 0 => {
+            40..=49 if mutation_count != 0 && replica_id == ReplicaId::REMOTE_SERVER => {
                 let entry_count = rng.random_range(1..=5);
                 buffer.update(cx, |buffer, cx| {
                     let diagnostics = DiagnosticSet::new(
@@ -3207,7 +3221,11 @@ fn test_random_collaboration(cx: &mut App, mut rng: StdRng) {
                         }),
                         buffer,
                     );
-                    log::info!("peer {} setting diagnostics: {:?}", replica_id, diagnostics);
+                    log::info!(
+                        "peer {:?} setting diagnostics: {:?}",
+                        replica_id,
+                        diagnostics
+                    );
                     buffer.update_diagnostics(LanguageServerId(0), diagnostics, cx);
                 });
                 mutation_count -= 1;
@@ -3217,12 +3235,13 @@ fn test_random_collaboration(cx: &mut App, mut rng: StdRng) {
                 let old_buffer_ops = cx
                     .background_executor()
                     .block(buffer.read(cx).serialize_ops(None, cx));
-                let new_replica_id = (0..=replica_ids.len() as ReplicaId)
+                let new_replica_id = (0..=replica_ids.len() as u16)
+                    .map(ReplicaId::new)
                     .filter(|replica_id| *replica_id != buffer.read(cx).replica_id())
                     .choose(&mut rng)
                     .unwrap();
                 log::info!(
-                    "Adding new replica {} (replicating from {})",
+                    "Adding new replica {:?} (replicating from {:?})",
                     new_replica_id,
                     replica_id
                 );
@@ -3241,7 +3260,7 @@ fn test_random_collaboration(cx: &mut App, mut rng: StdRng) {
                         cx,
                     );
                     log::info!(
-                        "New replica {} text: {:?}",
+                        "New replica {:?} text: {:?}",
                         new_buffer.replica_id(),
                         new_buffer.text()
                     );
@@ -3264,7 +3283,7 @@ fn test_random_collaboration(cx: &mut App, mut rng: StdRng) {
                 }));
                 network.lock().replicate(replica_id, new_replica_id);
 
-                if new_replica_id as usize == replica_ids.len() {
+                if new_replica_id.as_u16() as usize == replica_ids.len() {
                     replica_ids.push(new_replica_id);
                 } else {
                     let new_buffer = new_buffer.take().unwrap();
@@ -3276,7 +3295,7 @@ fn test_random_collaboration(cx: &mut App, mut rng: StdRng) {
                             .map(|op| proto::deserialize_operation(op).unwrap());
                         if ops.len() > 0 {
                             log::info!(
-                                "peer {} (version: {:?}) applying {} ops from the network. {:?}",
+                                "peer {:?} (version: {:?}) applying {} ops from the network. {:?}",
                                 new_replica_id,
                                 buffer.read(cx).version(),
                                 ops.len(),
@@ -3287,13 +3306,13 @@ fn test_random_collaboration(cx: &mut App, mut rng: StdRng) {
                             });
                         }
                     }
-                    buffers[new_replica_id as usize] = new_buffer;
+                    buffers[new_replica_id.as_u16() as usize] = new_buffer;
                 }
             }
             60..=69 if mutation_count != 0 => {
                 buffer.update(cx, |buffer, cx| {
                     buffer.randomly_undo_redo(&mut rng, cx);
-                    log::info!("buffer {} text: {:?}", buffer.replica_id(), buffer.text());
+                    log::info!("buffer {:?} text: {:?}", buffer.replica_id(), buffer.text());
                 });
                 mutation_count -= 1;
             }
@@ -3305,7 +3324,7 @@ fn test_random_collaboration(cx: &mut App, mut rng: StdRng) {
                     .map(|op| proto::deserialize_operation(op).unwrap());
                 if ops.len() > 0 {
                     log::info!(
-                        "peer {} (version: {:?}) applying {} ops from the network. {:?}",
+                        "peer {:?} (version: {:?}) applying {} ops from the network. {:?}",
                         replica_id,
                         buffer.read(cx).version(),
                         ops.len(),
@@ -3335,13 +3354,13 @@ fn test_random_collaboration(cx: &mut App, mut rng: StdRng) {
         assert_eq!(
             buffer.version(),
             first_buffer.version(),
-            "Replica {} version != Replica 0 version",
+            "Replica {:?} version != Replica 0 version",
             buffer.replica_id()
         );
         assert_eq!(
             buffer.text(),
             first_buffer.text(),
-            "Replica {} text != Replica 0 text",
+            "Replica {:?} text != Replica 0 text",
             buffer.replica_id()
         );
         assert_eq!(
@@ -3351,7 +3370,7 @@ fn test_random_collaboration(cx: &mut App, mut rng: StdRng) {
             first_buffer
                 .diagnostics_in_range::<_, usize>(0..first_buffer.len(), false)
                 .collect::<Vec<_>>(),
-            "Replica {} diagnostics != Replica 0 diagnostics",
+            "Replica {:?} diagnostics != Replica 0 diagnostics",
             buffer.replica_id()
         );
     }
@@ -3370,7 +3389,7 @@ fn test_random_collaboration(cx: &mut App, mut rng: StdRng) {
         assert_eq!(
             actual_remote_selections,
             expected_remote_selections,
-            "Replica {} remote selections != expected selections",
+            "Replica {:?} remote selections != expected selections",
             buffer.replica_id()
         );
     }

crates/language/src/language.rs 🔗

@@ -2589,12 +2589,76 @@ pub fn range_from_lsp(range: lsp::Range) -> Range<Unclipped<PointUtf16>> {
     let mut start = point_from_lsp(range.start);
     let mut end = point_from_lsp(range.end);
     if start > end {
-        log::warn!("range_from_lsp called with inverted range {start:?}-{end:?}");
+        // We debug instead of warn so that this is not logged by default unless explicitly requested.
+        // Using warn would write to the log file, and since we receive an enormous amount of
+        // range_from_lsp calls (especially during completions), that can hang the main thread.
+        //
+        // See issue #36223.
+        zlog::debug!("range_from_lsp called with inverted range {start:?}-{end:?}");
         mem::swap(&mut start, &mut end);
     }
     start..end
 }
 
+#[doc(hidden)]
+#[cfg(any(test, feature = "test-support"))]
+pub fn rust_lang() -> Arc<Language> {
+    use std::borrow::Cow;
+
+    let language = Language::new(
+        LanguageConfig {
+            name: "Rust".into(),
+            matcher: LanguageMatcher {
+                path_suffixes: vec!["rs".to_string()],
+                ..Default::default()
+            },
+            line_comments: vec!["// ".into(), "/// ".into(), "//! ".into()],
+            ..Default::default()
+        },
+        Some(tree_sitter_rust::LANGUAGE.into()),
+    )
+    .with_queries(LanguageQueries {
+        indents: Some(Cow::from(
+            r#"
+[
+    ((where_clause) _ @end)
+    (field_expression)
+    (call_expression)
+    (assignment_expression)
+    (let_declaration)
+    (let_chain)
+    (await_expression)
+] @indent
+
+(_ "[" "]" @end) @indent
+(_ "<" ">" @end) @indent
+(_ "{" "}" @end) @indent
+(_ "(" ")" @end) @indent"#,
+        )),
+        brackets: Some(Cow::from(
+            r#"
+("(" @open ")" @close)
+("[" @open "]" @close)
+("{" @open "}" @close)
+("<" @open ">" @close)
+("\"" @open "\"" @close)
+(closure_parameters "|" @open "|" @close)"#,
+        )),
+        text_objects: Some(Cow::from(
+            r#"
+(function_item
+    body: (_
+        "{"
+        (_)* @function.inside
+        "}" )) @function.around
+        "#,
+        )),
+        ..LanguageQueries::default()
+    })
+    .expect("Could not parse queries");
+    Arc::new(language)
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;

crates/language/src/language_settings.rs 🔗

@@ -15,7 +15,7 @@ pub use settings::{
     Formatter, FormatterList, InlayHintKind, LanguageSettingsContent, LspInsertMode,
     RewrapBehavior, ShowWhitespaceSetting, SoftWrap, WordsCompletionMode,
 };
-use settings::{ExtendingVec, Settings, SettingsContent, SettingsLocation, SettingsStore};
+use settings::{Settings, SettingsLocation, SettingsStore};
 use shellexpand;
 use std::{borrow::Cow, num::NonZeroU32, path::Path, sync::Arc};
 
@@ -679,131 +679,6 @@ impl settings::Settings for AllLanguageSettings {
             file_types,
         }
     }
-
-    fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut SettingsContent) {
-        let d = &mut current.project.all_languages.defaults;
-        if let Some(size) = vscode
-            .read_value("editor.tabSize")
-            .and_then(|v| v.as_u64())
-            .and_then(|n| NonZeroU32::new(n as u32))
-        {
-            d.tab_size = Some(size);
-        }
-        if let Some(v) = vscode.read_bool("editor.insertSpaces") {
-            d.hard_tabs = Some(!v);
-        }
-
-        vscode.enum_setting("editor.wordWrap", &mut d.soft_wrap, |s| match s {
-            "on" => Some(SoftWrap::EditorWidth),
-            "wordWrapColumn" => Some(SoftWrap::PreferLine),
-            "bounded" => Some(SoftWrap::Bounded),
-            "off" => Some(SoftWrap::None),
-            _ => None,
-        });
-        vscode.u32_setting("editor.wordWrapColumn", &mut d.preferred_line_length);
-
-        if let Some(arr) = vscode
-            .read_value("editor.rulers")
-            .and_then(|v| v.as_array())
-            .map(|v| v.iter().map(|n| n.as_u64().map(|n| n as usize)).collect())
-        {
-            d.wrap_guides = arr;
-        }
-        if let Some(b) = vscode.read_bool("editor.guides.indentation") {
-            d.indent_guides.get_or_insert_default().enabled = Some(b);
-        }
-
-        if let Some(b) = vscode.read_bool("editor.guides.formatOnSave") {
-            d.format_on_save = Some(if b {
-                FormatOnSave::On
-            } else {
-                FormatOnSave::Off
-            });
-        }
-        vscode.bool_setting(
-            "editor.trimAutoWhitespace",
-            &mut d.remove_trailing_whitespace_on_save,
-        );
-        vscode.bool_setting(
-            "files.insertFinalNewline",
-            &mut d.ensure_final_newline_on_save,
-        );
-        vscode.bool_setting("editor.inlineSuggest.enabled", &mut d.show_edit_predictions);
-        vscode.enum_setting("editor.renderWhitespace", &mut d.show_whitespaces, |s| {
-            Some(match s {
-                "boundary" => ShowWhitespaceSetting::Boundary,
-                "trailing" => ShowWhitespaceSetting::Trailing,
-                "selection" => ShowWhitespaceSetting::Selection,
-                "all" => ShowWhitespaceSetting::All,
-                _ => ShowWhitespaceSetting::None,
-            })
-        });
-        vscode.enum_setting(
-            "editor.autoSurround",
-            &mut d.use_auto_surround,
-            |s| match s {
-                "languageDefined" | "quotes" | "brackets" => Some(true),
-                "never" => Some(false),
-                _ => None,
-            },
-        );
-        vscode.bool_setting("editor.formatOnType", &mut d.use_on_type_format);
-        vscode.bool_setting("editor.linkedEditing", &mut d.linked_edits);
-        vscode.bool_setting("editor.formatOnPaste", &mut d.auto_indent_on_paste);
-        vscode.bool_setting(
-            "editor.suggestOnTriggerCharacters",
-            &mut d.show_completions_on_input,
-        );
-        if let Some(b) = vscode.read_bool("editor.suggest.showWords") {
-            let mode = if b {
-                WordsCompletionMode::Enabled
-            } else {
-                WordsCompletionMode::Disabled
-            };
-            d.completions.get_or_insert_default().words = Some(mode);
-        }
-        // TODO: pull ^ out into helper and reuse for per-language settings
-
-        // vscodes file association map is inverted from ours, so we flip the mapping before merging
-        let mut associations: HashMap<Arc<str>, ExtendingVec<String>> = HashMap::default();
-        if let Some(map) = vscode
-            .read_value("files.associations")
-            .and_then(|v| v.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());
-            }
-        }
-
-        // TODO: do we want to merge imported globs per filetype? for now we'll just replace
-        current
-            .project
-            .all_languages
-            .file_types
-            .get_or_insert_default()
-            .extend(associations);
-
-        // cursor global ignore list applies to cursor-tab, so transfer it to edit_predictions.disabled_globs
-        if let Some(disabled_globs) = vscode
-            .read_value("cursor.general.globalCursorIgnoreList")
-            .and_then(|v| v.as_array())
-        {
-            current
-                .project
-                .all_languages
-                .edit_predictions
-                .get_or_insert_default()
-                .disabled_globs
-                .get_or_insert_default()
-                .extend(
-                    disabled_globs
-                        .iter()
-                        .filter_map(|glob| glob.as_str())
-                        .map(|s| s.to_string()),
-                );
-        }
-    }
 }
 
 #[derive(Default, Debug, Clone, PartialEq, Eq)]

crates/language/src/proto.rs 🔗

@@ -39,14 +39,14 @@ pub fn serialize_operation(operation: &crate::Operation) -> proto::Operation {
 
             crate::Operation::Buffer(text::Operation::Undo(undo)) => {
                 proto::operation::Variant::Undo(proto::operation::Undo {
-                    replica_id: undo.timestamp.replica_id as u32,
+                    replica_id: undo.timestamp.replica_id.as_u16() as u32,
                     lamport_timestamp: undo.timestamp.value,
                     version: serialize_version(&undo.version),
                     counts: undo
                         .counts
                         .iter()
                         .map(|(edit_id, count)| proto::UndoCount {
-                            replica_id: edit_id.replica_id as u32,
+                            replica_id: edit_id.replica_id.as_u16() as u32,
                             lamport_timestamp: edit_id.value,
                             count: *count,
                         })
@@ -60,7 +60,7 @@ pub fn serialize_operation(operation: &crate::Operation) -> proto::Operation {
                 lamport_timestamp,
                 cursor_shape,
             } => proto::operation::Variant::UpdateSelections(proto::operation::UpdateSelections {
-                replica_id: lamport_timestamp.replica_id as u32,
+                replica_id: lamport_timestamp.replica_id.as_u16() as u32,
                 lamport_timestamp: lamport_timestamp.value,
                 selections: serialize_selections(selections),
                 line_mode: *line_mode,
@@ -72,7 +72,7 @@ pub fn serialize_operation(operation: &crate::Operation) -> proto::Operation {
                 server_id,
                 diagnostics,
             } => proto::operation::Variant::UpdateDiagnostics(proto::UpdateDiagnostics {
-                replica_id: lamport_timestamp.replica_id as u32,
+                replica_id: lamport_timestamp.replica_id.as_u16() as u32,
                 lamport_timestamp: lamport_timestamp.value,
                 server_id: server_id.0 as u64,
                 diagnostics: serialize_diagnostics(diagnostics.iter()),
@@ -84,7 +84,7 @@ pub fn serialize_operation(operation: &crate::Operation) -> proto::Operation {
                 server_id,
             } => proto::operation::Variant::UpdateCompletionTriggers(
                 proto::operation::UpdateCompletionTriggers {
-                    replica_id: lamport_timestamp.replica_id as u32,
+                    replica_id: lamport_timestamp.replica_id.as_u16() as u32,
                     lamport_timestamp: lamport_timestamp.value,
                     triggers: triggers.clone(),
                     language_server_id: server_id.to_proto(),
@@ -95,7 +95,7 @@ pub fn serialize_operation(operation: &crate::Operation) -> proto::Operation {
                 line_ending,
                 lamport_timestamp,
             } => proto::operation::Variant::UpdateLineEnding(proto::operation::UpdateLineEnding {
-                replica_id: lamport_timestamp.replica_id as u32,
+                replica_id: lamport_timestamp.replica_id.as_u16() as u32,
                 lamport_timestamp: lamport_timestamp.value,
                 line_ending: serialize_line_ending(*line_ending) as i32,
             }),
@@ -106,7 +106,7 @@ pub fn serialize_operation(operation: &crate::Operation) -> proto::Operation {
 /// Serializes an [`EditOperation`] to be sent over RPC.
 pub fn serialize_edit_operation(operation: &EditOperation) -> proto::operation::Edit {
     proto::operation::Edit {
-        replica_id: operation.timestamp.replica_id as u32,
+        replica_id: operation.timestamp.replica_id.as_u16() as u32,
         lamport_timestamp: operation.timestamp.value,
         version: serialize_version(&operation.version),
         ranges: operation.ranges.iter().map(serialize_range).collect(),
@@ -123,12 +123,12 @@ pub fn serialize_undo_map_entry(
     (edit_id, counts): (&clock::Lamport, &[(clock::Lamport, u32)]),
 ) -> proto::UndoMapEntry {
     proto::UndoMapEntry {
-        replica_id: edit_id.replica_id as u32,
+        replica_id: edit_id.replica_id.as_u16() as u32,
         local_timestamp: edit_id.value,
         counts: counts
             .iter()
             .map(|(undo_id, count)| proto::UndoCount {
-                replica_id: undo_id.replica_id as u32,
+                replica_id: undo_id.replica_id.as_u16() as u32,
                 lamport_timestamp: undo_id.value,
                 count: *count,
             })
@@ -246,7 +246,7 @@ pub fn serialize_diagnostics<'a>(
 /// Serializes an [`Anchor`] to be sent over RPC.
 pub fn serialize_anchor(anchor: &Anchor) -> proto::Anchor {
     proto::Anchor {
-        replica_id: anchor.timestamp.replica_id as u32,
+        replica_id: anchor.timestamp.replica_id.as_u16() as u32,
         timestamp: anchor.timestamp.value,
         offset: anchor.offset as u64,
         bias: match anchor.bias {
@@ -283,7 +283,7 @@ pub fn deserialize_operation(message: proto::Operation) -> Result<crate::Operati
             proto::operation::Variant::Undo(undo) => {
                 crate::Operation::Buffer(text::Operation::Undo(UndoOperation {
                     timestamp: clock::Lamport {
-                        replica_id: undo.replica_id as ReplicaId,
+                        replica_id: ReplicaId::new(undo.replica_id as u16),
                         value: undo.lamport_timestamp,
                     },
                     version: deserialize_version(&undo.version),
@@ -293,7 +293,7 @@ pub fn deserialize_operation(message: proto::Operation) -> Result<crate::Operati
                         .map(|c| {
                             (
                                 clock::Lamport {
-                                    replica_id: c.replica_id as ReplicaId,
+                                    replica_id: ReplicaId::new(c.replica_id as u16),
                                     value: c.lamport_timestamp,
                                 },
                                 c.count,
@@ -319,7 +319,7 @@ pub fn deserialize_operation(message: proto::Operation) -> Result<crate::Operati
 
                 crate::Operation::UpdateSelections {
                     lamport_timestamp: clock::Lamport {
-                        replica_id: message.replica_id as ReplicaId,
+                        replica_id: ReplicaId::new(message.replica_id as u16),
                         value: message.lamport_timestamp,
                     },
                     selections: Arc::from(selections),
@@ -333,7 +333,7 @@ pub fn deserialize_operation(message: proto::Operation) -> Result<crate::Operati
             proto::operation::Variant::UpdateDiagnostics(message) => {
                 crate::Operation::UpdateDiagnostics {
                     lamport_timestamp: clock::Lamport {
-                        replica_id: message.replica_id as ReplicaId,
+                        replica_id: ReplicaId::new(message.replica_id as u16),
                         value: message.lamport_timestamp,
                     },
                     server_id: LanguageServerId(message.server_id as usize),
@@ -344,7 +344,7 @@ pub fn deserialize_operation(message: proto::Operation) -> Result<crate::Operati
                 crate::Operation::UpdateCompletionTriggers {
                     triggers: message.triggers,
                     lamport_timestamp: clock::Lamport {
-                        replica_id: message.replica_id as ReplicaId,
+                        replica_id: ReplicaId::new(message.replica_id as u16),
                         value: message.lamport_timestamp,
                     },
                     server_id: LanguageServerId::from_proto(message.language_server_id),
@@ -353,7 +353,7 @@ pub fn deserialize_operation(message: proto::Operation) -> Result<crate::Operati
             proto::operation::Variant::UpdateLineEnding(message) => {
                 crate::Operation::UpdateLineEnding {
                     lamport_timestamp: clock::Lamport {
-                        replica_id: message.replica_id as ReplicaId,
+                        replica_id: ReplicaId::new(message.replica_id as u16),
                         value: message.lamport_timestamp,
                     },
                     line_ending: deserialize_line_ending(
@@ -370,7 +370,7 @@ pub fn deserialize_operation(message: proto::Operation) -> Result<crate::Operati
 pub fn deserialize_edit_operation(edit: proto::operation::Edit) -> EditOperation {
     EditOperation {
         timestamp: clock::Lamport {
-            replica_id: edit.replica_id as ReplicaId,
+            replica_id: ReplicaId::new(edit.replica_id as u16),
             value: edit.lamport_timestamp,
         },
         version: deserialize_version(&edit.version),
@@ -385,7 +385,7 @@ pub fn deserialize_undo_map_entry(
 ) -> (clock::Lamport, Vec<(clock::Lamport, u32)>) {
     (
         clock::Lamport {
-            replica_id: entry.replica_id as u16,
+            replica_id: ReplicaId::new(entry.replica_id as u16),
             value: entry.local_timestamp,
         },
         entry
@@ -394,7 +394,7 @@ pub fn deserialize_undo_map_entry(
             .map(|undo_count| {
                 (
                     clock::Lamport {
-                        replica_id: undo_count.replica_id as u16,
+                        replica_id: ReplicaId::new(undo_count.replica_id as u16),
                         value: undo_count.lamport_timestamp,
                     },
                     undo_count.count,
@@ -480,7 +480,7 @@ pub fn deserialize_anchor(anchor: proto::Anchor) -> Option<Anchor> {
     };
     Some(Anchor {
         timestamp: clock::Lamport {
-            replica_id: anchor.replica_id as ReplicaId,
+            replica_id: ReplicaId::new(anchor.replica_id as u16),
             value: anchor.timestamp,
         },
         offset: anchor.offset as usize,
@@ -524,7 +524,7 @@ pub fn lamport_timestamp_for_operation(operation: &proto::Operation) -> Option<c
     }
 
     Some(clock::Lamport {
-        replica_id: replica_id as ReplicaId,
+        replica_id: ReplicaId::new(replica_id as u16),
         value,
     })
 }
@@ -559,7 +559,7 @@ pub fn deserialize_transaction(transaction: proto::Transaction) -> Result<Transa
 /// Serializes a [`clock::Lamport`] timestamp to be sent over RPC.
 pub fn serialize_timestamp(timestamp: clock::Lamport) -> proto::LamportTimestamp {
     proto::LamportTimestamp {
-        replica_id: timestamp.replica_id as u32,
+        replica_id: timestamp.replica_id.as_u16() as u32,
         value: timestamp.value,
     }
 }
@@ -567,7 +567,7 @@ pub fn serialize_timestamp(timestamp: clock::Lamport) -> proto::LamportTimestamp
 /// Deserializes a [`clock::Lamport`] timestamp from the RPC representation.
 pub fn deserialize_timestamp(timestamp: proto::LamportTimestamp) -> clock::Lamport {
     clock::Lamport {
-        replica_id: timestamp.replica_id as ReplicaId,
+        replica_id: ReplicaId::new(timestamp.replica_id as u16),
         value: timestamp.value,
     }
 }
@@ -590,7 +590,7 @@ pub fn deserialize_version(message: &[proto::VectorClockEntry]) -> clock::Global
     let mut version = clock::Global::new();
     for entry in message {
         version.observe(clock::Lamport {
-            replica_id: entry.replica_id as ReplicaId,
+            replica_id: ReplicaId::new(entry.replica_id as u16),
             value: entry.timestamp,
         });
     }
@@ -602,7 +602,7 @@ pub fn serialize_version(version: &clock::Global) -> Vec<proto::VectorClockEntry
     version
         .iter()
         .map(|entry| proto::VectorClockEntry {
-            replica_id: entry.replica_id as u32,
+            replica_id: entry.replica_id.as_u16() as u32,
             timestamp: entry.value,
         })
         .collect()

crates/language/src/syntax_map/syntax_map_tests.rs 🔗

@@ -6,7 +6,7 @@ use crate::{
 use gpui::App;
 use rand::rngs::StdRng;
 use std::{env, ops::Range, sync::Arc};
-use text::{Buffer, BufferId};
+use text::{Buffer, BufferId, ReplicaId};
 use tree_sitter::Node;
 use unindent::Unindent as _;
 use util::test::marked_text_ranges;
@@ -88,7 +88,7 @@ fn test_syntax_map_layers_for_range(cx: &mut App) {
     registry.add(language.clone());
 
     let mut buffer = Buffer::new(
-        0,
+        ReplicaId::LOCAL,
         BufferId::new(1).unwrap(),
         r#"
             fn a() {
@@ -189,7 +189,7 @@ fn test_dynamic_language_injection(cx: &mut App) {
     registry.add(Arc::new(ruby_lang()));
 
     let mut buffer = Buffer::new(
-        0,
+        ReplicaId::LOCAL,
         BufferId::new(1).unwrap(),
         r#"
             This is a code block:
@@ -811,7 +811,7 @@ fn test_syntax_map_languages_loading_with_erb(cx: &mut App) {
     .unindent();
 
     let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
-    let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), text);
+    let mut buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), text);
 
     let mut syntax_map = SyntaxMap::new(&buffer);
     syntax_map.set_language_registry(registry.clone());
@@ -978,7 +978,7 @@ fn test_random_edits(
         .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
         .unwrap_or(10);
 
-    let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), text);
+    let mut buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), text);
 
     let mut syntax_map = SyntaxMap::new(&buffer);
     syntax_map.set_language_registry(registry.clone());
@@ -1159,7 +1159,7 @@ fn test_edit_sequence(language_name: &str, steps: &[&str], cx: &mut App) -> (Buf
         .now_or_never()
         .unwrap()
         .unwrap();
-    let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), "");
+    let mut buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), "");
 
     let mut mutated_syntax_map = SyntaxMap::new(&buffer);
     mutated_syntax_map.set_language_registry(registry.clone());

crates/language/src/toolchain.rs 🔗

@@ -98,6 +98,7 @@ pub trait ToolchainLister: Send + Sync + 'static {
         worktree_root: PathBuf,
         subroot_relative_path: Arc<RelPath>,
         project_env: Option<HashMap<String, String>>,
+        fs: &dyn Fs,
     ) -> ToolchainList;
 
     /// Given a user-created toolchain, resolve lister-specific details.
@@ -106,14 +107,11 @@ pub trait ToolchainLister: Send + Sync + 'static {
         &self,
         path: PathBuf,
         project_env: Option<HashMap<String, String>>,
+        fs: &dyn Fs,
     ) -> anyhow::Result<Toolchain>;
 
-    async fn activation_script(
-        &self,
-        toolchain: &Toolchain,
-        shell: ShellKind,
-        fs: &dyn Fs,
-    ) -> Vec<String>;
+    fn activation_script(&self, toolchain: &Toolchain, shell: ShellKind) -> Vec<String>;
+
     /// Returns various "static" bits of information about this toolchain lister. This function should be pure.
     fn meta(&self) -> ToolchainMetadata;
 }

crates/language_model/Cargo.toml 🔗

@@ -32,7 +32,6 @@ image.workspace = true
 log.workspace = true
 parking_lot.workspace = true
 proto.workspace = true
-schemars.workspace = true
 serde.workspace = true
 serde_json.workspace = true
 settings.workspace = true
@@ -40,7 +39,6 @@ smol.workspace = true
 telemetry_events.workspace = true
 thiserror.workspace = true
 util.workspace = true
-workspace-hack.workspace = true
 
 [dev-dependencies]
 gpui = { workspace = true, features = ["test-support"] }

crates/language_model/src/language_model.rs 🔗

@@ -19,8 +19,7 @@ use http_client::{StatusCode, http};
 use icons::IconName;
 use open_router::OpenRouterError;
 use parking_lot::Mutex;
-use schemars::JsonSchema;
-use serde::{Deserialize, Serialize, de::DeserializeOwned};
+use serde::{Deserialize, Serialize};
 pub use settings::LanguageModelCacheConfiguration;
 use std::ops::{Add, Sub};
 use std::str::FromStr;
@@ -669,11 +668,6 @@ pub trait LanguageModelExt: LanguageModel {
 }
 impl LanguageModelExt for dyn LanguageModel {}
 
-pub trait LanguageModelTool: 'static + DeserializeOwned + JsonSchema {
-    fn name() -> String;
-    fn description() -> String;
-}
-
 /// An error that occurred when trying to authenticate the language model provider.
 #[derive(Debug, Error)]
 pub enum AuthenticateError {

crates/language_model/src/request.rs 🔗

@@ -77,7 +77,7 @@ impl std::fmt::Debug for LanguageModelImage {
 }
 
 /// Anthropic wants uploaded images to be smaller than this in both dimensions.
-const ANTHROPIC_SIZE_LIMT: f32 = 1568.;
+const ANTHROPIC_SIZE_LIMIT: f32 = 1568.;
 
 impl LanguageModelImage {
     pub fn empty() -> Self {
@@ -112,13 +112,13 @@ impl LanguageModelImage {
             let image_size = size(DevicePixels(width as i32), DevicePixels(height as i32));
 
             let base64_image = {
-                if image_size.width.0 > ANTHROPIC_SIZE_LIMT as i32
-                    || image_size.height.0 > ANTHROPIC_SIZE_LIMT as i32
+                if image_size.width.0 > ANTHROPIC_SIZE_LIMIT as i32
+                    || image_size.height.0 > ANTHROPIC_SIZE_LIMIT as i32
                 {
                     let new_bounds = ObjectFit::ScaleDown.get_bounds(
                         gpui::Bounds {
                             origin: point(px(0.0), px(0.0)),
-                            size: size(px(ANTHROPIC_SIZE_LIMT), px(ANTHROPIC_SIZE_LIMT)),
+                            size: size(px(ANTHROPIC_SIZE_LIMIT), px(ANTHROPIC_SIZE_LIMIT)),
                         },
                         image_size,
                     );

crates/language_models/Cargo.toml 🔗

@@ -58,7 +58,6 @@ ui.workspace = true
 ui_input.workspace = true
 util.workspace = true
 vercel = { workspace = true, features = ["schemars"] }
-workspace-hack.workspace = true
 x_ai = { workspace = true, features = ["schemars"] }
 zed_env_vars.workspace = true
 

crates/language_models/src/provider/anthropic.rs 🔗

@@ -21,7 +21,7 @@ use std::str::FromStr;
 use std::sync::{Arc, LazyLock};
 use strum::IntoEnumIterator;
 use ui::{Icon, IconName, List, Tooltip, prelude::*};
-use ui_input::SingleLineInput;
+use ui_input::InputField;
 use util::{ResultExt, truncate_and_trailoff};
 use zed_env_vars::{EnvVar, env_var};
 
@@ -823,7 +823,7 @@ fn convert_usage(usage: &Usage) -> language_model::TokenUsage {
 }
 
 struct ConfigurationView {
-    api_key_editor: Entity<SingleLineInput>,
+    api_key_editor: Entity<InputField>,
     state: Entity<State>,
     load_credentials_task: Option<Task<()>>,
     target_agent: ConfigurationViewTargetAgent,
@@ -862,7 +862,7 @@ impl ConfigurationView {
         }));
 
         Self {
-            api_key_editor: cx.new(|cx| SingleLineInput::new(window, cx, Self::PLACEHOLDER_TEXT)),
+            api_key_editor: cx.new(|cx| InputField::new(window, cx, Self::PLACEHOLDER_TEXT)),
             state,
             load_credentials_task,
             target_agent,

crates/language_models/src/provider/bedrock.rs 🔗

@@ -42,7 +42,7 @@ use settings::{BedrockAvailableModel as AvailableModel, Settings, SettingsStore}
 use smol::lock::OnceCell;
 use strum::{EnumIter, IntoEnumIterator, IntoStaticStr};
 use ui::{Icon, IconName, List, Tooltip, prelude::*};
-use ui_input::SingleLineInput;
+use ui_input::InputField;
 use util::ResultExt;
 
 use crate::AllLanguageModelSettings;
@@ -1006,10 +1006,10 @@ pub fn map_to_language_model_completion_events(
 }
 
 struct ConfigurationView {
-    access_key_id_editor: Entity<SingleLineInput>,
-    secret_access_key_editor: Entity<SingleLineInput>,
-    session_token_editor: Entity<SingleLineInput>,
-    region_editor: Entity<SingleLineInput>,
+    access_key_id_editor: Entity<InputField>,
+    secret_access_key_editor: Entity<InputField>,
+    session_token_editor: Entity<InputField>,
+    region_editor: Entity<InputField>,
     state: Entity<State>,
     load_credentials_task: Option<Task<()>>,
 }
@@ -1047,20 +1047,19 @@ impl ConfigurationView {
 
         Self {
             access_key_id_editor: cx.new(|cx| {
-                SingleLineInput::new(window, cx, Self::PLACEHOLDER_ACCESS_KEY_ID_TEXT)
+                InputField::new(window, cx, Self::PLACEHOLDER_ACCESS_KEY_ID_TEXT)
                     .label("Access Key ID")
             }),
             secret_access_key_editor: cx.new(|cx| {
-                SingleLineInput::new(window, cx, Self::PLACEHOLDER_SECRET_ACCESS_KEY_TEXT)
+                InputField::new(window, cx, Self::PLACEHOLDER_SECRET_ACCESS_KEY_TEXT)
                     .label("Secret Access Key")
             }),
             session_token_editor: cx.new(|cx| {
-                SingleLineInput::new(window, cx, Self::PLACEHOLDER_SESSION_TOKEN_TEXT)
+                InputField::new(window, cx, Self::PLACEHOLDER_SESSION_TOKEN_TEXT)
                     .label("Session Token (Optional)")
             }),
-            region_editor: cx.new(|cx| {
-                SingleLineInput::new(window, cx, Self::PLACEHOLDER_REGION).label("Region")
-            }),
+            region_editor: cx
+                .new(|cx| InputField::new(window, cx, Self::PLACEHOLDER_REGION).label("Region")),
             state,
             load_credentials_task,
         }

crates/language_models/src/provider/deepseek.rs 🔗

@@ -20,7 +20,7 @@ use std::str::FromStr;
 use std::sync::{Arc, LazyLock};
 
 use ui::{Icon, IconName, List, prelude::*};
-use ui_input::SingleLineInput;
+use ui_input::InputField;
 use util::{ResultExt, truncate_and_trailoff};
 use zed_env_vars::{EnvVar, env_var};
 
@@ -525,7 +525,7 @@ impl DeepSeekEventMapper {
 }
 
 struct ConfigurationView {
-    api_key_editor: Entity<SingleLineInput>,
+    api_key_editor: Entity<InputField>,
     state: Entity<State>,
     load_credentials_task: Option<Task<()>>,
 }
@@ -533,7 +533,7 @@ struct ConfigurationView {
 impl ConfigurationView {
     fn new(state: Entity<State>, window: &mut Window, cx: &mut Context<Self>) -> Self {
         let api_key_editor =
-            cx.new(|cx| SingleLineInput::new(window, cx, "sk-00000000000000000000000000000000"));
+            cx.new(|cx| InputField::new(window, cx, "sk-00000000000000000000000000000000"));
 
         cx.observe(&state, |_, _, cx| {
             cx.notify();

crates/language_models/src/provider/google.rs 🔗

@@ -29,7 +29,7 @@ use std::sync::{
 };
 use strum::IntoEnumIterator;
 use ui::{Icon, IconName, List, Tooltip, prelude::*};
-use ui_input::SingleLineInput;
+use ui_input::InputField;
 use util::{ResultExt, truncate_and_trailoff};
 use zed_env_vars::EnvVar;
 
@@ -751,7 +751,7 @@ fn convert_usage(usage: &UsageMetadata) -> language_model::TokenUsage {
 }
 
 struct ConfigurationView {
-    api_key_editor: Entity<SingleLineInput>,
+    api_key_editor: Entity<InputField>,
     state: Entity<State>,
     target_agent: language_model::ConfigurationViewTargetAgent,
     load_credentials_task: Option<Task<()>>,
@@ -788,7 +788,7 @@ impl ConfigurationView {
         }));
 
         Self {
-            api_key_editor: cx.new(|cx| SingleLineInput::new(window, cx, "AIzaSy...")),
+            api_key_editor: cx.new(|cx| InputField::new(window, cx, "AIzaSy...")),
             target_agent,
             state,
             load_credentials_task,

crates/language_models/src/provider/mistral.rs 🔗

@@ -20,7 +20,7 @@ use std::str::FromStr;
 use std::sync::{Arc, LazyLock};
 use strum::IntoEnumIterator;
 use ui::{Icon, IconName, List, Tooltip, prelude::*};
-use ui_input::SingleLineInput;
+use ui_input::InputField;
 use util::{ResultExt, truncate_and_trailoff};
 use zed_env_vars::{EnvVar, env_var};
 
@@ -744,8 +744,8 @@ struct RawToolCall {
 }
 
 struct ConfigurationView {
-    api_key_editor: Entity<SingleLineInput>,
-    codestral_api_key_editor: Entity<SingleLineInput>,
+    api_key_editor: Entity<InputField>,
+    codestral_api_key_editor: Entity<InputField>,
     state: Entity<State>,
     load_credentials_task: Option<Task<()>>,
 }
@@ -753,9 +753,9 @@ struct ConfigurationView {
 impl ConfigurationView {
     fn new(state: Entity<State>, window: &mut Window, cx: &mut Context<Self>) -> Self {
         let api_key_editor =
-            cx.new(|cx| SingleLineInput::new(window, cx, "0aBCDEFGhIjKLmNOpqrSTUVwxyzabCDE1f2"));
+            cx.new(|cx| InputField::new(window, cx, "0aBCDEFGhIjKLmNOpqrSTUVwxyzabCDE1f2"));
         let codestral_api_key_editor =
-            cx.new(|cx| SingleLineInput::new(window, cx, "0aBCDEFGhIjKLmNOpqrSTUVwxyzabCDE1f2"));
+            cx.new(|cx| InputField::new(window, cx, "0aBCDEFGhIjKLmNOpqrSTUVwxyzabCDE1f2"));
 
         cx.observe(&state, |_, _, cx| {
             cx.notify();

crates/language_models/src/provider/ollama.rs 🔗

@@ -23,7 +23,7 @@ use std::sync::LazyLock;
 use std::sync::atomic::{AtomicU64, Ordering};
 use std::{collections::HashMap, sync::Arc};
 use ui::{ButtonLike, ElevationIndex, List, Tooltip, prelude::*};
-use ui_input::SingleLineInput;
+use ui_input::InputField;
 use zed_env_vars::{EnvVar, env_var};
 
 use crate::AllLanguageModelSettings;
@@ -623,18 +623,17 @@ fn map_to_language_model_completion_events(
 }
 
 struct ConfigurationView {
-    api_key_editor: Entity<SingleLineInput>,
-    api_url_editor: Entity<SingleLineInput>,
+    api_key_editor: Entity<InputField>,
+    api_url_editor: Entity<InputField>,
     state: Entity<State>,
 }
 
 impl ConfigurationView {
     pub fn new(state: Entity<State>, window: &mut Window, cx: &mut Context<Self>) -> Self {
-        let api_key_editor =
-            cx.new(|cx| SingleLineInput::new(window, cx, "63e02e...").label("API key"));
+        let api_key_editor = cx.new(|cx| InputField::new(window, cx, "63e02e...").label("API key"));
 
         let api_url_editor = cx.new(|cx| {
-            let input = SingleLineInput::new(window, cx, OLLAMA_API_URL).label("API URL");
+            let input = InputField::new(window, cx, OLLAMA_API_URL).label("API URL");
             input.set_text(OllamaLanguageModelProvider::api_url(cx), window, cx);
             input
         });

crates/language_models/src/provider/open_ai.rs 🔗

@@ -21,7 +21,7 @@ use std::str::FromStr as _;
 use std::sync::{Arc, LazyLock};
 use strum::IntoEnumIterator;
 use ui::{ElevationIndex, List, Tooltip, prelude::*};
-use ui_input::SingleLineInput;
+use ui_input::InputField;
 use util::{ResultExt, truncate_and_trailoff};
 use zed_env_vars::{EnvVar, env_var};
 
@@ -675,7 +675,7 @@ pub fn count_open_ai_tokens(
 }
 
 struct ConfigurationView {
-    api_key_editor: Entity<SingleLineInput>,
+    api_key_editor: Entity<InputField>,
     state: Entity<State>,
     load_credentials_task: Option<Task<()>>,
 }
@@ -683,7 +683,7 @@ struct ConfigurationView {
 impl ConfigurationView {
     fn new(state: Entity<State>, window: &mut Window, cx: &mut Context<Self>) -> Self {
         let api_key_editor = cx.new(|cx| {
-            SingleLineInput::new(
+            InputField::new(
                 window,
                 cx,
                 "sk-000000000000000000000000000000000000000000000000",

crates/language_models/src/provider/open_ai_compatible.rs 🔗

@@ -14,7 +14,7 @@ use open_ai::{ResponseStreamEvent, stream_completion};
 use settings::{Settings, SettingsStore};
 use std::sync::Arc;
 use ui::{ElevationIndex, Tooltip, prelude::*};
-use ui_input::SingleLineInput;
+use ui_input::InputField;
 use util::{ResultExt, truncate_and_trailoff};
 use zed_env_vars::EnvVar;
 
@@ -340,7 +340,7 @@ impl LanguageModel for OpenAiCompatibleLanguageModel {
 }
 
 struct ConfigurationView {
-    api_key_editor: Entity<SingleLineInput>,
+    api_key_editor: Entity<InputField>,
     state: Entity<State>,
     load_credentials_task: Option<Task<()>>,
 }
@@ -348,7 +348,7 @@ struct ConfigurationView {
 impl ConfigurationView {
     fn new(state: Entity<State>, window: &mut Window, cx: &mut Context<Self>) -> Self {
         let api_key_editor = cx.new(|cx| {
-            SingleLineInput::new(
+            InputField::new(
                 window,
                 cx,
                 "000000000000000000000000000000000000000000000000000",

crates/language_models/src/provider/open_router.rs 🔗

@@ -18,7 +18,7 @@ use std::pin::Pin;
 use std::str::FromStr as _;
 use std::sync::{Arc, LazyLock};
 use ui::{Icon, IconName, List, Tooltip, prelude::*};
-use ui_input::SingleLineInput;
+use ui_input::InputField;
 use util::{ResultExt, truncate_and_trailoff};
 use zed_env_vars::{EnvVar, env_var};
 
@@ -692,7 +692,7 @@ pub fn count_open_router_tokens(
 }
 
 struct ConfigurationView {
-    api_key_editor: Entity<SingleLineInput>,
+    api_key_editor: Entity<InputField>,
     state: Entity<State>,
     load_credentials_task: Option<Task<()>>,
 }
@@ -700,7 +700,7 @@ struct ConfigurationView {
 impl ConfigurationView {
     fn new(state: Entity<State>, window: &mut Window, cx: &mut Context<Self>) -> Self {
         let api_key_editor = cx.new(|cx| {
-            SingleLineInput::new(
+            InputField::new(
                 window,
                 cx,
                 "sk_or_000000000000000000000000000000000000000000000000",

crates/language_models/src/provider/vercel.rs 🔗

@@ -15,7 +15,7 @@ use settings::{Settings, SettingsStore};
 use std::sync::{Arc, LazyLock};
 use strum::IntoEnumIterator;
 use ui::{ElevationIndex, List, Tooltip, prelude::*};
-use ui_input::SingleLineInput;
+use ui_input::InputField;
 use util::{ResultExt, truncate_and_trailoff};
 use vercel::{Model, VERCEL_API_URL};
 use zed_env_vars::{EnvVar, env_var};
@@ -362,7 +362,7 @@ pub fn count_vercel_tokens(
 }
 
 struct ConfigurationView {
-    api_key_editor: Entity<SingleLineInput>,
+    api_key_editor: Entity<InputField>,
     state: Entity<State>,
     load_credentials_task: Option<Task<()>>,
 }
@@ -370,7 +370,7 @@ struct ConfigurationView {
 impl ConfigurationView {
     fn new(state: Entity<State>, window: &mut Window, cx: &mut Context<Self>) -> Self {
         let api_key_editor = cx.new(|cx| {
-            SingleLineInput::new(
+            InputField::new(
                 window,
                 cx,
                 "v1:0000000000000000000000000000000000000000000000000",

crates/language_models/src/provider/x_ai.rs 🔗

@@ -15,7 +15,7 @@ use settings::{Settings, SettingsStore};
 use std::sync::{Arc, LazyLock};
 use strum::IntoEnumIterator;
 use ui::{ElevationIndex, List, Tooltip, prelude::*};
-use ui_input::SingleLineInput;
+use ui_input::InputField;
 use util::{ResultExt, truncate_and_trailoff};
 use x_ai::{Model, XAI_API_URL};
 use zed_env_vars::{EnvVar, env_var};
@@ -359,7 +359,7 @@ pub fn count_xai_tokens(
 }
 
 struct ConfigurationView {
-    api_key_editor: Entity<SingleLineInput>,
+    api_key_editor: Entity<InputField>,
     state: Entity<State>,
     load_credentials_task: Option<Task<()>>,
 }
@@ -367,7 +367,7 @@ struct ConfigurationView {
 impl ConfigurationView {
     fn new(state: Entity<State>, window: &mut Window, cx: &mut Context<Self>) -> Self {
         let api_key_editor = cx.new(|cx| {
-            SingleLineInput::new(
+            InputField::new(
                 window,
                 cx,
                 "xai-0000000000000000000000000000000000000000000000000",

crates/language_onboarding/Cargo.toml 🔗

@@ -21,7 +21,6 @@ gpui.workspace = true
 project.workspace = true
 ui.workspace = true
 workspace.workspace = true
-workspace-hack.workspace = true
 
 # Uncomment other workspace dependencies as needed
 # assistant.workspace = true

crates/language_selector/Cargo.toml 🔗

@@ -26,7 +26,6 @@ settings.workspace = true
 ui.workspace = true
 util.workspace = true
 workspace.workspace = true
-workspace-hack.workspace = true
 
 [dev-dependencies]
 editor = { workspace = true, features = ["test-support"] }

crates/language_selector/src/active_buffer_language.rs 🔗

@@ -62,9 +62,7 @@ impl Render for ActiveBufferLanguage {
                             });
                         }
                     }))
-                    .tooltip(|window, cx| {
-                        Tooltip::for_action("Select Language", &Toggle, window, cx)
-                    }),
+                    .tooltip(|_window, cx| Tooltip::for_action("Select Language", &Toggle, cx)),
             )
         })
     }

crates/language_tools/Cargo.toml 🔗

@@ -34,7 +34,6 @@ ui.workspace = true
 util.workspace = true
 workspace.workspace = true
 zed_actions.workspace = true
-workspace-hack.workspace = true
 
 [dev-dependencies]
 editor = { workspace = true, features = ["test-support"] }

crates/language_tools/src/key_context_view.rs 🔗

@@ -1,6 +1,7 @@
 use gpui::{
     Action, App, AppContext as _, Entity, EventEmitter, FocusHandle, Focusable,
-    KeyBindingContextPredicate, KeyContext, Keystroke, MouseButton, Render, Subscription, actions,
+    KeyBindingContextPredicate, KeyContext, Keystroke, MouseButton, Render, Subscription, Task,
+    actions,
 };
 use itertools::Itertools;
 use serde_json::json;
@@ -157,16 +158,16 @@ impl Item for KeyContextView {
         _workspace_id: Option<workspace::WorkspaceId>,
         window: &mut Window,
         cx: &mut Context<Self>,
-    ) -> Option<Entity<Self>>
+    ) -> Task<Option<Entity<Self>>>
     where
         Self: Sized,
     {
-        Some(cx.new(|cx| KeyContextView::new(window, cx)))
+        Task::ready(Some(cx.new(|cx| KeyContextView::new(window, cx))))
     }
 }
 
 impl Render for KeyContextView {
-    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
+    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
         use itertools::Itertools;
 
         let key_equivalents = cx.keyboard_mapper().get_key_equivalents();
@@ -211,7 +212,6 @@ impl Render for KeyContextView {
                             .style(ButtonStyle::Filled)
                             .key_binding(ui::KeyBinding::for_action(
                                 &zed_actions::OpenDefaultKeymap,
-                                window,
                                 cx
                             ))
                             .on_click(|_, window, cx| {
@@ -221,7 +221,7 @@ impl Render for KeyContextView {
                     .child(
                         Button::new("edit_your_keymap", "Edit Keymap File")
                             .style(ButtonStyle::Filled)
-                            .key_binding(ui::KeyBinding::for_action(&zed_actions::OpenKeymapFile, window, cx))
+                            .key_binding(ui::KeyBinding::for_action(&zed_actions::OpenKeymapFile, cx))
                             .on_click(|_, window, cx| {
                                 window.dispatch_action(zed_actions::OpenKeymapFile.boxed_clone(), cx);
                             }),

crates/language_tools/src/lsp_button.rs 🔗

@@ -1065,14 +1065,8 @@ impl Render for LspButton {
                         .when_some(indicator, IconButton::indicator)
                         .icon_size(IconSize::Small)
                         .indicator_border_color(Some(cx.theme().colors().status_bar_background)),
-                    move |window, cx| {
-                        Tooltip::with_meta(
-                            "Language Servers",
-                            Some(&ToggleMenu),
-                            description,
-                            window,
-                            cx,
-                        )
+                    move |_window, cx| {
+                        Tooltip::with_meta("Language Servers", Some(&ToggleMenu), description, cx)
                     },
                 ),
         )

crates/language_tools/src/lsp_log_view.rs 🔗

@@ -3,7 +3,7 @@ use copilot::Copilot;
 use editor::{Editor, EditorEvent, actions::MoveToEnd, scroll::Autoscroll};
 use gpui::{
     AnyView, App, Context, Corner, Entity, EventEmitter, FocusHandle, Focusable, IntoElement,
-    ParentElement, Render, Styled, Subscription, WeakEntity, Window, actions, div,
+    ParentElement, Render, Styled, Subscription, Task, WeakEntity, Window, actions, div,
 };
 use itertools::Itertools;
 use language::{LanguageServerId, language_settings::SoftWrap};
@@ -229,8 +229,11 @@ impl LspLogView {
                         log_view.editor.update(cx, |editor, cx| {
                             editor.set_read_only(false);
                             let last_offset = editor.buffer().read(cx).len(cx);
-                            let newest_cursor_is_at_end =
-                                editor.selections.newest::<usize>(cx).start >= last_offset;
+                            let newest_cursor_is_at_end = editor
+                                .selections
+                                .newest::<usize>(&editor.display_snapshot(cx))
+                                .start
+                                >= last_offset;
                             editor.edit(
                                 vec![
                                     (last_offset..last_offset, text.as_str()),
@@ -760,11 +763,11 @@ impl Item for LspLogView {
         _workspace_id: Option<WorkspaceId>,
         window: &mut Window,
         cx: &mut Context<Self>,
-    ) -> Option<Entity<Self>>
+    ) -> Task<Option<Entity<Self>>>
     where
         Self: Sized,
     {
-        Some(cx.new(|cx| {
+        Task::ready(Some(cx.new(|cx| {
             let mut new_view = Self::new(self.project.clone(), self.log_store.clone(), window, cx);
             if let Some(server_id) = self.current_server_id {
                 match self.active_entry_kind {
@@ -775,7 +778,7 @@ impl Item for LspLogView {
                 }
             }
             new_view
-        }))
+        })))
     }
 }
 

crates/language_tools/src/syntax_tree_view.rs 🔗

@@ -3,7 +3,7 @@ use editor::{Anchor, Editor, ExcerptId, SelectionEffects, scroll::Autoscroll};
 use gpui::{
     App, AppContext as _, Context, Div, Entity, EntityId, EventEmitter, FocusHandle, Focusable,
     Hsla, InteractiveElement, IntoElement, MouseButton, MouseDownEvent, MouseMoveEvent,
-    ParentElement, Render, ScrollStrategy, SharedString, Styled, UniformListScrollHandle,
+    ParentElement, Render, ScrollStrategy, SharedString, Styled, Task, UniformListScrollHandle,
     WeakEntity, Window, actions, div, rems, uniform_list,
 };
 use language::{Buffer, OwnedSyntaxLayer};
@@ -252,7 +252,10 @@ impl SyntaxTreeView {
             .editor
             .update(cx, |editor, cx| editor.snapshot(window, cx));
         let (buffer, range, excerpt_id) = editor_state.editor.update(cx, |editor, cx| {
-            let selection_range = editor.selections.last::<usize>(cx).range();
+            let selection_range = editor
+                .selections
+                .last::<usize>(&editor.display_snapshot(cx))
+                .range();
             let multi_buffer = editor.buffer().read(cx);
             let (buffer, range, excerpt_id) = snapshot
                 .buffer_snapshot()
@@ -570,17 +573,17 @@ impl Item for SyntaxTreeView {
         _: Option<workspace::WorkspaceId>,
         window: &mut Window,
         cx: &mut Context<Self>,
-    ) -> Option<Entity<Self>>
+    ) -> Task<Option<Entity<Self>>>
     where
         Self: Sized,
     {
-        Some(cx.new(|cx| {
+        Task::ready(Some(cx.new(|cx| {
             let mut clone = Self::new(self.workspace_handle.clone(), None, window, cx);
             if let Some(editor) = &self.editor {
                 clone.set_editor(editor.editor.clone(), window, cx)
             }
             clone
-        }))
+        })))
     }
 }
 

crates/languages/Cargo.toml 🔗

@@ -90,7 +90,6 @@ tree-sitter-rust = { workspace = true, optional = true }
 tree-sitter-typescript = { workspace = true, optional = true }
 tree-sitter-yaml = { workspace = true, optional = true }
 util.workspace = true
-workspace-hack.workspace = true
 
 [dev-dependencies]
 pretty_assertions.workspace = true

crates/languages/src/c/highlights.scm 🔗

@@ -1,27 +1,30 @@
+[
+  "const"
+  "enum"
+  "extern"
+  "inline"
+  "sizeof"
+  "static"
+  "struct"
+  "typedef"
+  "union"
+  "volatile"
+] @keyword
+
 [
   "break"
   "case"
-  "const"
   "continue"
   "default"
   "do"
   "else"
-  "enum"
-  "extern"
   "for"
   "goto"
   "if"
-  "inline"
   "return"
-  "sizeof"
-  "static"
-  "struct"
   "switch"
-  "typedef"
-  "union"
-  "volatile"
   "while"
-] @keyword
+] @keyword.control
 
 [
   "#define"

crates/languages/src/c/injections.scm 🔗

@@ -1,3 +1,7 @@
+((comment) @injection.content
+ (#set! injection.language "comment")
+)
+
 (preproc_def
     value: (preproc_arg) @injection.content
     (#set! injection.language "c"))

crates/languages/src/cpp/highlights.scm 🔗

@@ -106,32 +106,19 @@ type: (primitive_type) @type.builtin
 [
   "alignas"
   "alignof"
-  "break"
-  "case"
-  "catch"
   "class"
-  "co_await"
-  "co_return"
-  "co_yield"
   "concept"
   "consteval"
   "constexpr"
   "constinit"
-  "continue"
   "decltype"
-  "default"
   "delete"
-  "do"
-  "else"
   "enum"
   "explicit"
   "export"
   "extern"
   "final"
-  "for"
   "friend"
-  "goto"
-  "if"
   "import"
   "inline"
   "module"
@@ -144,24 +131,40 @@ type: (primitive_type) @type.builtin
   "protected"
   "public"
   "requires"
-  "return"
   "sizeof"
   "struct"
-  "switch"
   "template"
   "thread_local"
-  "throw"
-  "try"
   "typedef"
   "typename"
   "union"
   "using"
   "virtual"
-  "while"
   (storage_class_specifier)
   (type_qualifier)
 ] @keyword
 
+[
+  "break"
+  "case"
+  "catch"
+  "co_await"
+  "co_return"
+  "co_yield"
+  "continue"
+  "default"
+  "do"
+  "else"
+  "for"
+  "goto"
+  "if"
+  "return"
+  "switch"
+  "throw"
+  "try"
+  "while"
+] @keyword.control
+
 [
   "#define"
   "#elif"

crates/languages/src/cpp/injections.scm 🔗

@@ -1,3 +1,7 @@
+((comment) @injection.content
+ (#set! injection.language "comment")
+)
+
 (preproc_def
     value: (preproc_arg) @injection.content
     (#set! injection.language "c++"))

crates/languages/src/go/injections.scm 🔗

@@ -1,4 +1,8 @@
 ; Refer to https://github.com/nvim-treesitter/nvim-treesitter/blob/master/queries/go/injections.scm#L4C1-L16C41
+((comment) @injection.content
+ (#set! injection.language "comment")
+)
+
 (call_expression
   (selector_expression) @_function
   (#any-of? @_function

crates/languages/src/javascript/highlights.scm 🔗

@@ -171,47 +171,52 @@
   "as"
   "async"
   "await"
-  "break"
-  "case"
-  "catch"
   "class"
   "const"
-  "continue"
   "debugger"
   "default"
   "delete"
-  "do"
-  "else"
   "export"
   "extends"
-  "finally"
-  "for"
   "from"
   "function"
   "get"
-  "if"
   "import"
   "in"
   "instanceof"
   "let"
   "new"
   "of"
-  "return"
   "set"
   "static"
-  "switch"
   "target"
-  "throw"
-  "try"
   "typeof"
   "using"
   "var"
   "void"
-  "while"
   "with"
-  "yield"
 ] @keyword
 
+[
+  "break"
+  "case"
+  "catch"
+  "continue"
+  "do"
+  "else"
+  "finally"
+  "for"
+  "if"
+  "return"
+  "switch"
+  "throw"
+  "try"
+  "while"
+  "yield"
+] @keyword.control
+
+(switch_default "default" @keyword.control)
+
 (template_substitution
   "${" @punctuation.special
   "}" @punctuation.special) @embedded

crates/languages/src/javascript/injections.scm 🔗

@@ -1,3 +1,7 @@
+((comment) @injection.content
+ (#set! injection.language "comment")
+)
+
 (((comment) @_jsdoc_comment
   (#match? @_jsdoc_comment "(?s)^/[*][*][^*].*[*]/$")) @injection.content
   (#set! injection.language "jsdoc"))

crates/languages/src/javascript/outline.scm 🔗

@@ -31,38 +31,103 @@
     (export_statement
         (lexical_declaration
             ["let" "const"] @context
-            ; Multiple names may be exported - @item is on the declarator to keep
-            ; ranges distinct.
             (variable_declarator
-                name: (_) @name) @item)))
+                name: (identifier) @name) @item)))
+
+; Exported array destructuring
+(program
+    (export_statement
+        (lexical_declaration
+            ["let" "const"] @context
+            (variable_declarator
+                name: (array_pattern
+                    [
+                        (identifier) @name @item
+                        (assignment_pattern left: (identifier) @name @item)
+                        (rest_pattern (identifier) @name @item)
+                    ])))))
+
+; Exported object destructuring
+(program
+    (export_statement
+        (lexical_declaration
+            ["let" "const"] @context
+            (variable_declarator
+                name: (object_pattern
+                    [(shorthand_property_identifier_pattern) @name @item
+                     (pair_pattern
+                         value: (identifier) @name @item)
+                     (pair_pattern
+                         value: (assignment_pattern left: (identifier) @name @item))
+                     (rest_pattern (identifier) @name @item)])))))
 
 (program
     (lexical_declaration
         ["let" "const"] @context
-        ; Multiple names may be defined - @item is on the declarator to keep
-        ; ranges distinct.
         (variable_declarator
-            name: (_) @name) @item))
+            name: (identifier) @name) @item))
+
+; Top-level array destructuring
+(program
+    (lexical_declaration
+        ["let" "const"] @context
+        (variable_declarator
+            name: (array_pattern
+                [
+                    (identifier) @name @item
+                    (assignment_pattern left: (identifier) @name @item)
+                    (rest_pattern (identifier) @name @item)
+                ]))))
+
+; Top-level object destructuring
+(program
+    (lexical_declaration
+        ["let" "const"] @context
+        (variable_declarator
+            name: (object_pattern
+                [(shorthand_property_identifier_pattern) @name @item
+                 (pair_pattern
+                     value: (identifier) @name @item)
+                 (pair_pattern
+                     value: (assignment_pattern left: (identifier) @name @item))
+                 (rest_pattern (identifier) @name @item)]))))
 
 (class_declaration
     "class" @context
     name: (_) @name) @item
 
-(method_definition
-    [
-        "get"
-        "set"
-        "async"
-        "*"
-        "readonly"
-        "static"
-        (override_modifier)
-        (accessibility_modifier)
-    ]* @context
-    name: (_) @name
-    parameters: (formal_parameters
-      "(" @context
-      ")" @context)) @item
+; Method definitions in classes (not in object literals)
+(class_body
+    (method_definition
+        [
+            "get"
+            "set"
+            "async"
+            "*"
+            "readonly"
+            "static"
+            (override_modifier)
+            (accessibility_modifier)
+        ]* @context
+        name: (_) @name
+        parameters: (formal_parameters
+          "(" @context
+          ")" @context)) @item)
+
+; Object literal methods
+(variable_declarator
+    value: (object
+        (method_definition
+            [
+                "get"
+                "set"
+                "async"
+                "*"
+            ]* @context
+            name: (_) @name
+            parameters: (formal_parameters
+              "(" @context
+              ")" @context)) @item))
 
 (public_field_definition
     [
@@ -116,4 +181,43 @@
     )
 ) @item
 
+; Object properties
+(pair
+    key: [
+        (property_identifier) @name
+        (string (string_fragment) @name)
+        (number) @name
+        (computed_property_name) @name
+    ]) @item
+
+; Nested variables in function bodies
+(statement_block
+    (lexical_declaration
+        ["let" "const"] @context
+        (variable_declarator
+            name: (identifier) @name) @item))
+
+; Nested array destructuring in functions
+(statement_block
+    (lexical_declaration
+        ["let" "const"] @context
+        (variable_declarator
+            name: (array_pattern
+                [
+                    (identifier) @name @item
+                    (assignment_pattern left: (identifier) @name @item)
+                    (rest_pattern (identifier) @name @item)
+                ]))))
+
+; Nested object destructuring in functions
+(statement_block
+    (lexical_declaration
+        ["let" "const"] @context
+        (variable_declarator
+            name: (object_pattern
+                [(shorthand_property_identifier_pattern) @name @item
+                 (pair_pattern value: (identifier) @name @item)
+                 (pair_pattern value: (assignment_pattern left: (identifier) @name @item))
+                 (rest_pattern (identifier) @name @item)]))))
+
 (comment) @annotation

crates/languages/src/python.rs 🔗

@@ -19,6 +19,7 @@ use pet_core::python_environment::{PythonEnvironment, PythonEnvironmentKind};
 use pet_virtualenv::is_virtualenv_dir;
 use project::Fs;
 use project::lsp_store::language_server_settings;
+use serde::{Deserialize, Serialize};
 use serde_json::{Value, json};
 use smol::lock::OnceCell;
 use std::cmp::Ordering;
@@ -39,6 +40,14 @@ use std::{
 use task::{ShellKind, TaskTemplate, TaskTemplates, VariableName};
 use util::{ResultExt, maybe};
 
+#[derive(Debug, Serialize, Deserialize)]
+pub(crate) struct PythonToolchainData {
+    #[serde(flatten)]
+    environment: PythonEnvironment,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    activation_scripts: Option<HashMap<ShellKind, PathBuf>>,
+}
+
 pub(crate) struct PyprojectTomlManifestProvider;
 
 impl ManifestProvider for PyprojectTomlManifestProvider {
@@ -165,11 +174,12 @@ impl LspAdapter for TyLspAdapter {
             })?
             .unwrap_or_else(|| json!({}));
         if let Some(toolchain) = toolchain.and_then(|toolchain| {
-            serde_json::from_value::<PythonEnvironment>(toolchain.as_json).ok()
+            serde_json::from_value::<PythonToolchainData>(toolchain.as_json).ok()
         }) {
             _ = maybe!({
-                let uri = url::Url::from_file_path(toolchain.executable?).ok()?;
-                let sys_prefix = toolchain.prefix.clone()?;
+                let uri =
+                    url::Url::from_file_path(toolchain.environment.executable.as_ref()?).ok()?;
+                let sys_prefix = toolchain.environment.prefix.clone()?;
                 let environment = json!({
                     "executable": {
                         "uri": uri,
@@ -474,9 +484,8 @@ impl LspAdapter for PyrightLspAdapter {
 
             // If we have a detected toolchain, configure Pyright to use it
             if let Some(toolchain) = toolchain
-                && let Ok(env) = serde_json::from_value::<
-                    pet_core::python_environment::PythonEnvironment,
-                >(toolchain.as_json.clone())
+                && let Ok(env) =
+                    serde_json::from_value::<PythonToolchainData>(toolchain.as_json.clone())
             {
                 if !user_settings.is_object() {
                     user_settings = Value::Object(serde_json::Map::default());
@@ -484,7 +493,7 @@ impl LspAdapter for PyrightLspAdapter {
                 let object = user_settings.as_object_mut().unwrap();
 
                 let interpreter_path = toolchain.path.to_string();
-                if let Some(venv_dir) = env.prefix {
+                if let Some(venv_dir) = &env.environment.prefix {
                     // Set venvPath and venv at the root level
                     // This matches the format of a pyrightconfig.json file
                     if let Some(parent) = venv_dir.parent() {
@@ -1023,6 +1032,7 @@ impl ToolchainLister for PythonToolchainProvider {
         worktree_root: PathBuf,
         subroot_relative_path: Arc<RelPath>,
         project_env: Option<HashMap<String, String>>,
+        fs: &dyn Fs,
     ) -> ToolchainList {
         let env = project_env.unwrap_or_default();
         let environment = EnvironmentApi::from_env(&env);
@@ -1114,13 +1124,16 @@ impl ToolchainLister for PythonToolchainProvider {
                 .then_with(exe_ordering)
         });
 
-        let mut toolchains: Vec<_> = toolchains
-            .into_iter()
-            .filter_map(venv_to_toolchain)
-            .collect();
-        toolchains.dedup();
+        let mut out_toolchains = Vec::new();
+        for toolchain in toolchains {
+            let Some(toolchain) = venv_to_toolchain(toolchain, fs).await else {
+                continue;
+            };
+            out_toolchains.push(toolchain);
+        }
+        out_toolchains.dedup();
         ToolchainList {
-            toolchains,
+            toolchains: out_toolchains,
             default: None,
             groups: Default::default(),
         }
@@ -1139,6 +1152,7 @@ impl ToolchainLister for PythonToolchainProvider {
         &self,
         path: PathBuf,
         env: Option<HashMap<String, String>>,
+        fs: &dyn Fs,
     ) -> anyhow::Result<Toolchain> {
         let env = env.unwrap_or_default();
         let environment = EnvironmentApi::from_env(&env);
@@ -1150,58 +1164,48 @@ impl ToolchainLister for PythonToolchainProvider {
         let toolchain = pet::resolve::resolve_environment(&path, &locators, &environment)
             .context("Could not find a virtual environment in provided path")?;
         let venv = toolchain.resolved.unwrap_or(toolchain.discovered);
-        venv_to_toolchain(venv).context("Could not convert a venv into a toolchain")
+        venv_to_toolchain(venv, fs)
+            .await
+            .context("Could not convert a venv into a toolchain")
     }
 
-    async fn activation_script(
-        &self,
-        toolchain: &Toolchain,
-        shell: ShellKind,
-        fs: &dyn Fs,
-    ) -> Vec<String> {
-        let Ok(toolchain) = serde_json::from_value::<pet_core::python_environment::PythonEnvironment>(
-            toolchain.as_json.clone(),
-        ) else {
+    fn activation_script(&self, toolchain: &Toolchain, shell: ShellKind) -> Vec<String> {
+        let Ok(toolchain) =
+            serde_json::from_value::<PythonToolchainData>(toolchain.as_json.clone())
+        else {
             return vec![];
         };
+
+        log::debug!("(Python) Composing activation script for toolchain {toolchain:?}");
+
         let mut activation_script = vec![];
 
-        match toolchain.kind {
+        match toolchain.environment.kind {
             Some(PythonEnvironmentKind::Conda) => {
-                if let Some(name) = &toolchain.name {
+                if let Some(name) = &toolchain.environment.name {
                     activation_script.push(format!("conda activate {name}"));
                 } else {
                     activation_script.push("conda activate".to_string());
                 }
             }
             Some(PythonEnvironmentKind::Venv | PythonEnvironmentKind::VirtualEnv) => {
-                if let Some(prefix) = &toolchain.prefix {
-                    let activate_keyword = shell.activate_keyword();
-                    let activate_script_name = match shell {
-                        ShellKind::Posix | ShellKind::Rc => "activate",
-                        ShellKind::Csh => "activate.csh",
-                        ShellKind::Tcsh => "activate.csh",
-                        ShellKind::Fish => "activate.fish",
-                        ShellKind::Nushell => "activate.nu",
-                        ShellKind::PowerShell => "activate.ps1",
-                        ShellKind::Cmd => "activate.bat",
-                        ShellKind::Xonsh => "activate.xsh",
-                    };
-                    let path = prefix.join(BINARY_DIR).join(activate_script_name);
-
-                    if let Some(quoted) = shell.try_quote(&path.to_string_lossy())
-                        && fs.is_file(&path).await
-                    {
-                        activation_script.push(format!("{activate_keyword} {quoted}"));
+                if let Some(activation_scripts) = &toolchain.activation_scripts {
+                    if let Some(activate_script_path) = activation_scripts.get(&shell) {
+                        let activate_keyword = shell.activate_keyword();
+                        if let Some(quoted) =
+                            shell.try_quote(&activate_script_path.to_string_lossy())
+                        {
+                            activation_script.push(format!("{activate_keyword} {quoted}"));
+                        }
                     }
                 }
             }
             Some(PythonEnvironmentKind::Pyenv) => {
-                let Some(manager) = toolchain.manager else {
+                let Some(manager) = &toolchain.environment.manager else {
                     return vec![];
                 };
-                let version = toolchain.version.as_deref().unwrap_or("system");
-                let pyenv = manager.executable;
+                let version = toolchain.environment.version.as_deref().unwrap_or("system");
+                let pyenv = &manager.executable;
                 let pyenv = pyenv.display();
                 activation_script.extend(match shell {
                     ShellKind::Fish => Some(format!("\"{pyenv}\" shell - fish {version}")),
@@ -1221,7 +1225,7 @@ impl ToolchainLister for PythonToolchainProvider {
     }
 }
 
-fn venv_to_toolchain(venv: PythonEnvironment) -> Option<Toolchain> {
+async fn venv_to_toolchain(venv: PythonEnvironment, fs: &dyn Fs) -> Option<Toolchain> {
     let mut name = String::from("Python");
     if let Some(ref version) = venv.version {
         _ = write!(name, " {version}");
@@ -1238,14 +1242,61 @@ fn venv_to_toolchain(venv: PythonEnvironment) -> Option<Toolchain> {
         _ = write!(name, " {nk}");
     }
 
+    let mut activation_scripts = HashMap::default();
+    match venv.kind {
+        Some(PythonEnvironmentKind::Venv | PythonEnvironmentKind::VirtualEnv) => {
+            resolve_venv_activation_scripts(&venv, fs, &mut activation_scripts).await
+        }
+        _ => {}
+    }
+    let data = PythonToolchainData {
+        environment: venv,
+        activation_scripts: Some(activation_scripts),
+    };
+
     Some(Toolchain {
         name: name.into(),
-        path: venv.executable.as_ref()?.to_str()?.to_owned().into(),
+        path: data
+            .environment
+            .executable
+            .as_ref()?
+            .to_str()?
+            .to_owned()
+            .into(),
         language_name: LanguageName::new("Python"),
-        as_json: serde_json::to_value(venv).ok()?,
+        as_json: serde_json::to_value(data).ok()?,
     })
 }
 
+async fn resolve_venv_activation_scripts(
+    venv: &PythonEnvironment,
+    fs: &dyn Fs,
+    activation_scripts: &mut HashMap<ShellKind, PathBuf>,
+) {
+    log::debug!("(Python) Resolving activation scripts for venv toolchain {venv:?}");
+    if let Some(prefix) = &venv.prefix {
+        for (shell_kind, script_name) in &[
+            (ShellKind::Posix, "activate"),
+            (ShellKind::Rc, "activate"),
+            (ShellKind::Csh, "activate.csh"),
+            (ShellKind::Tcsh, "activate.csh"),
+            (ShellKind::Fish, "activate.fish"),
+            (ShellKind::Nushell, "activate.nu"),
+            (ShellKind::PowerShell, "activate.ps1"),
+            (ShellKind::Cmd, "activate.bat"),
+            (ShellKind::Xonsh, "activate.xsh"),
+        ] {
+            let path = prefix.join(BINARY_DIR).join(script_name);
+
+            log::debug!("Trying path: {}", path.display());
+
+            if fs.is_file(&path).await {
+                activation_scripts.insert(*shell_kind, path);
+            }
+        }
+    }
+}
+
 pub struct EnvironmentApi<'a> {
     global_search_locations: Arc<Mutex<Vec<PathBuf>>>,
     project_env: &'a HashMap<String, String>,
@@ -1293,9 +1344,13 @@ impl pet_core::os_environment::Environment for EnvironmentApi<'_> {
 
     fn get_know_global_search_locations(&self) -> Vec<PathBuf> {
         if self.global_search_locations.lock().is_empty() {
-            let mut paths =
-                std::env::split_paths(&self.get_env_var("PATH".to_string()).unwrap_or_default())
-                    .collect::<Vec<PathBuf>>();
+            let mut paths = std::env::split_paths(
+                &self
+                    .get_env_var("PATH".to_string())
+                    .or_else(|| self.get_env_var("Path".to_string()))
+                    .unwrap_or_default(),
+            )
+            .collect::<Vec<PathBuf>>();
 
             log::trace!("Env PATH: {:?}", paths);
             for p in self.pet_env.get_know_global_search_locations() {

crates/languages/src/rust/highlights.scm 🔗

@@ -83,29 +83,20 @@
   "as"
   "async"
   "await"
-  "break"
   "const"
-  "continue"
   "default"
   "dyn"
-  "else"
   "enum"
   "extern"
   "fn"
-  "for"
-  "if"
   "impl"
-  "in"
   "let"
-  "loop"
   "macro_rules!"
-  "match"
   "mod"
   "move"
   "pub"
   "raw"
   "ref"
-  "return"
   "static"
   "struct"
   "trait"
@@ -114,13 +105,25 @@
   "unsafe"
   "use"
   "where"
-  "while"
-  "yield"
   (crate)
   (mutable_specifier)
   (super)
 ] @keyword
 
+[
+  "break"
+  "continue"
+  "else"
+  "for"
+  "if"
+  "in"
+  "loop"
+  "match"
+  "return"
+  "while"
+  "yield"
+] @keyword.control
+
 [
   (string_literal)
   (raw_string_literal)

crates/languages/src/rust/injections.scm 🔗

@@ -2,7 +2,7 @@
     (#set! injection.language "comment"))
 
 (macro_invocation
-    macro: (identifier) @_macro_name
+    macro: [(identifier) (scoped_identifier)] @_macro_name
     (#not-any-of? @_macro_name "view" "html")
     (token_tree) @injection.content
     (#set! injection.language "rust"))
@@ -11,7 +11,7 @@
 ; it wants to inject inside of rust, instead of modifying the rust
 ; injections to support leptos injections
 (macro_invocation
-    macro: (identifier) @_macro_name
+    macro: [(identifier) (scoped_identifier)] @_macro_name
     (#any-of? @_macro_name "view" "html")
     (token_tree) @injection.content
     (#set! injection.language "rstml")

crates/languages/src/tsx/highlights.scm 🔗

@@ -171,25 +171,16 @@
   "as"
   "async"
   "await"
-  "break"
-  "case"
-  "catch"
   "class"
   "const"
-  "continue"
   "debugger"
   "default"
   "delete"
-  "do"
-  "else"
   "export"
   "extends"
-  "finally"
-  "for"
   "from"
   "function"
   "get"
-  "if"
   "import"
   "in"
   "instanceof"
@@ -197,23 +188,37 @@
   "let"
   "new"
   "of"
-  "return"
   "satisfies"
   "set"
   "static"
-  "switch"
   "target"
-  "throw"
-  "try"
   "typeof"
   "using"
   "var"
   "void"
-  "while"
   "with"
-  "yield"
 ] @keyword
 
+[
+  "break"
+  "case"
+  "catch"
+  "continue"
+  "do"
+  "else"
+  "finally"
+  "for"
+  "if"
+  "return"
+  "switch"
+  "throw"
+  "try"
+  "while"
+  "yield"
+] @keyword.control
+
+(switch_default "default" @keyword.control)
+
 (template_substitution
   "${" @punctuation.special
   "}" @punctuation.special) @embedded

crates/languages/src/tsx/injections.scm 🔗

@@ -1,3 +1,7 @@
+((comment) @injection.content
+ (#set! injection.language "comment")
+)
+
 (((comment) @_jsdoc_comment
   (#match? @_jsdoc_comment "(?s)^/[*][*][^*].*[*]/$")) @injection.content
   (#set! injection.language "jsdoc"))

crates/languages/src/typescript.rs 🔗

@@ -1110,7 +1110,7 @@ mod tests {
 
         let text = r#"
             function a() {
-              // local variables are omitted
+              // local variables are included
               let a1 = 1;
               // all functions are included
               async function a2() {}
@@ -1133,6 +1133,7 @@ mod tests {
                 .collect::<Vec<_>>(),
             &[
                 ("function a()", 0),
+                ("let a1", 1),
                 ("async function a2()", 1),
                 ("let b", 0),
                 ("function getB()", 0),
@@ -1141,6 +1142,223 @@ mod tests {
         );
     }
 
+    #[gpui::test]
+    async fn test_outline_with_destructuring(cx: &mut TestAppContext) {
+        let language = crate::language(
+            "typescript",
+            tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
+        );
+
+        let text = r#"
+            // Top-level destructuring
+            const { a1, a2 } = a;
+            const [b1, b2] = b;
+
+            // Defaults and rest
+            const [c1 = 1, , c2, ...rest1] = c;
+            const { d1, d2: e1, f1 = 2, g1: h1 = 3, ...rest2 } = d;
+
+            function processData() {
+              // Nested object destructuring
+              const { c1, c2 } = c;
+              // Nested array destructuring
+              const [d1, d2, d3] = d;
+              // Destructuring with renaming
+              const { f1: g1 } = f;
+              // With defaults
+              const [x = 10, y] = xy;
+            }
+
+            class DataHandler {
+              method() {
+                // Destructuring in class method
+                const { a1, a2 } = a;
+                const [b1, ...b2] = b;
+              }
+            }
+        "#
+        .unindent();
+
+        let buffer = cx.new(|cx| language::Buffer::local(text, cx).with_language(language, cx));
+        let outline = buffer.read_with(cx, |buffer, _| buffer.snapshot().outline(None));
+        assert_eq!(
+            outline
+                .items
+                .iter()
+                .map(|item| (item.text.as_str(), item.depth))
+                .collect::<Vec<_>>(),
+            &[
+                ("const a1", 0),
+                ("const a2", 0),
+                ("const b1", 0),
+                ("const b2", 0),
+                ("const c1", 0),
+                ("const c2", 0),
+                ("const rest1", 0),
+                ("const d1", 0),
+                ("const e1", 0),
+                ("const h1", 0),
+                ("const rest2", 0),
+                ("function processData()", 0),
+                ("const c1", 1),
+                ("const c2", 1),
+                ("const d1", 1),
+                ("const d2", 1),
+                ("const d3", 1),
+                ("const g1", 1),
+                ("const x", 1),
+                ("const y", 1),
+                ("class DataHandler", 0),
+                ("method()", 1),
+                ("const a1", 2),
+                ("const a2", 2),
+                ("const b1", 2),
+                ("const b2", 2),
+            ]
+        );
+    }
+
+    #[gpui::test]
+    async fn test_outline_with_object_properties(cx: &mut TestAppContext) {
+        let language = crate::language(
+            "typescript",
+            tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
+        );
+
+        let text = r#"
+            // Object with function properties
+            const o = { m() {}, async n() {}, g: function* () {}, h: () => {}, k: function () {} };
+
+            // Object with primitive properties
+            const p = { p1: 1, p2: "hello", p3: true };
+
+            // Nested objects
+            const q = {
+                r: {
+                    // won't be included due to one-level depth limit
+                    s: 1
+                },
+                t: 2
+            };
+
+            function getData() {
+                const local = { x: 1, y: 2 };
+                return local;
+            }
+        "#
+        .unindent();
+
+        let buffer = cx.new(|cx| language::Buffer::local(text, cx).with_language(language, cx));
+        let outline = buffer.read_with(cx, |buffer, _| buffer.snapshot().outline(None));
+        assert_eq!(
+            outline
+                .items
+                .iter()
+                .map(|item| (item.text.as_str(), item.depth))
+                .collect::<Vec<_>>(),
+            &[
+                ("const o", 0),
+                ("m()", 1),
+                ("async n()", 1),
+                ("g", 1),
+                ("h", 1),
+                ("k", 1),
+                ("const p", 0),
+                ("p1", 1),
+                ("p2", 1),
+                ("p3", 1),
+                ("const q", 0),
+                ("r", 1),
+                ("s", 2),
+                ("t", 1),
+                ("function getData()", 0),
+                ("const local", 1),
+                ("x", 2),
+                ("y", 2),
+            ]
+        );
+    }
+
+    #[gpui::test]
+    async fn test_outline_with_computed_property_names(cx: &mut TestAppContext) {
+        let language = crate::language(
+            "typescript",
+            tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
+        );
+
+        let text = r#"
+            // Symbols as object keys
+            const sym = Symbol("test");
+            const obj1 = {
+                [sym]: 1,
+                [Symbol("inline")]: 2,
+                normalKey: 3
+            };
+
+            // Enums as object keys
+            enum Color { Red, Blue, Green }
+
+            const obj2 = {
+                [Color.Red]: "red value",
+                [Color.Blue]: "blue value",
+                regularProp: "normal"
+            };
+
+            // Mixed computed properties
+            const key = "dynamic";
+            const obj3 = {
+                [key]: 1,
+                ["string" + "concat"]: 2,
+                [1 + 1]: 3,
+                static: 4
+            };
+
+            // Nested objects with computed properties
+            const obj4 = {
+                [sym]: {
+                    nested: 1
+                },
+                regular: {
+                    [key]: 2
+                }
+            };
+        "#
+        .unindent();
+
+        let buffer = cx.new(|cx| language::Buffer::local(text, cx).with_language(language, cx));
+        let outline = buffer.read_with(cx, |buffer, _| buffer.snapshot().outline(None));
+        assert_eq!(
+            outline
+                .items
+                .iter()
+                .map(|item| (item.text.as_str(), item.depth))
+                .collect::<Vec<_>>(),
+            &[
+                ("const sym", 0),
+                ("const obj1", 0),
+                ("[sym]", 1),
+                ("[Symbol(\"inline\")]", 1),
+                ("normalKey", 1),
+                ("enum Color", 0),
+                ("const obj2", 0),
+                ("[Color.Red]", 1),
+                ("[Color.Blue]", 1),
+                ("regularProp", 1),
+                ("const key", 0),
+                ("const obj3", 0),
+                ("[key]", 1),
+                ("[\"string\" + \"concat\"]", 1),
+                ("[1 + 1]", 1),
+                ("static", 1),
+                ("const obj4", 0),
+                ("[sym]", 1),
+                ("nested", 2),
+                ("regular", 1),
+                ("[key]", 2),
+            ]
+        );
+    }
+
     #[gpui::test]
     async fn test_generator_function_outline(cx: &mut TestAppContext) {
         let language = crate::language("javascript", tree_sitter_typescript::LANGUAGE_TSX.into());

crates/languages/src/typescript/highlights.scm 🔗

@@ -218,27 +218,18 @@
   "as"
   "async"
   "await"
-  "break"
-  "case"
-  "catch"
   "class"
   "const"
-  "continue"
   "debugger"
   "declare"
   "default"
   "delete"
-  "do"
-  "else"
   "enum"
   "export"
   "extends"
-  "finally"
-  "for"
   "from"
   "function"
   "get"
-  "if"
   "implements"
   "import"
   "in"
@@ -257,20 +248,34 @@
   "protected"
   "public"
   "readonly"
-  "return"
   "satisfies"
   "set"
   "static"
-  "switch"
   "target"
-  "throw"
-  "try"
   "type"
   "typeof"
   "using"
   "var"
   "void"
-  "while"
   "with"
-  "yield"
 ] @keyword
+
+[
+  "break"
+  "case"
+  "catch"
+  "continue"
+  "do"
+  "else"
+  "finally"
+  "for"
+  "if"
+  "return"
+  "switch"
+  "throw"
+  "try"
+  "while"
+  "yield"
+] @keyword.control
+
+(switch_default "default" @keyword.control)

crates/languages/src/typescript/injections.scm 🔗

@@ -1,3 +1,7 @@
+((comment) @injection.content
+ (#set! injection.language "comment")
+)
+
 (((comment) @_jsdoc_comment
   (#match? @_jsdoc_comment "(?s)^/[*][*][^*].*[*]/$")) @injection.content
   (#set! injection.language "jsdoc"))

crates/languages/src/typescript/outline.scm 🔗

@@ -34,18 +34,64 @@
 (export_statement
     (lexical_declaration
         ["let" "const"] @context
-        ; Multiple names may be exported - @item is on the declarator to keep
-        ; ranges distinct.
         (variable_declarator
-            name: (_) @name) @item))
+            name: (identifier) @name) @item))
 
+; Exported array destructuring
+(export_statement
+    (lexical_declaration
+        ["let" "const"] @context
+        (variable_declarator
+            name: (array_pattern
+                [
+                    (identifier) @name @item
+                    (assignment_pattern left: (identifier) @name @item)
+                    (rest_pattern (identifier) @name @item)
+                ]))))
+
+; Exported object destructuring
+(export_statement
+    (lexical_declaration
+        ["let" "const"] @context
+        (variable_declarator
+            name: (object_pattern
+                [(shorthand_property_identifier_pattern) @name @item
+                 (pair_pattern
+                     value: (identifier) @name @item)
+                 (pair_pattern
+                     value: (assignment_pattern left: (identifier) @name @item))
+                 (rest_pattern (identifier) @name @item)]))))
+
+(program
+    (lexical_declaration
+        ["let" "const"] @context
+        (variable_declarator
+            name: (identifier) @name) @item))
+
+; Top-level array destructuring
 (program
     (lexical_declaration
         ["let" "const"] @context
-        ; Multiple names may be defined - @item is on the declarator to keep
-        ; ranges distinct.
         (variable_declarator
-            name: (_) @name) @item))
+            name: (array_pattern
+                [
+                    (identifier) @name @item
+                    (assignment_pattern left: (identifier) @name @item)
+                    (rest_pattern (identifier) @name @item)
+                ]))))
+
+; Top-level object destructuring
+(program
+    (lexical_declaration
+        ["let" "const"] @context
+        (variable_declarator
+            name: (object_pattern
+                [(shorthand_property_identifier_pattern) @name @item
+                 (pair_pattern
+                     value: (identifier) @name @item)
+                 (pair_pattern
+                     value: (assignment_pattern left: (identifier) @name @item))
+                 (rest_pattern (identifier) @name @item)]))))
 
 (class_declaration
     "class" @context
@@ -56,21 +102,38 @@
     "class" @context
     name: (_) @name) @item
 
-(method_definition
-    [
-        "get"
-        "set"
-        "async"
-        "*"
-        "readonly"
-        "static"
-        (override_modifier)
-        (accessibility_modifier)
-    ]* @context
-    name: (_) @name
-    parameters: (formal_parameters
-      "(" @context
-      ")" @context)) @item
+; Method definitions in classes (not in object literals)
+(class_body
+    (method_definition
+        [
+            "get"
+            "set"
+            "async"
+            "*"
+            "readonly"
+            "static"
+            (override_modifier)
+            (accessibility_modifier)
+        ]* @context
+        name: (_) @name
+        parameters: (formal_parameters
+          "(" @context
+          ")" @context)) @item)
+
+; Object literal methods
+(variable_declarator
+    value: (object
+        (method_definition
+            [
+                "get"
+                "set"
+                "async"
+                "*"
+            ]* @context
+            name: (_) @name
+            parameters: (formal_parameters
+              "(" @context
+              ")" @context)) @item))
 
 (public_field_definition
     [
@@ -124,4 +187,44 @@
     )
 ) @item
 
+; Object properties
+(pair
+    key: [
+        (property_identifier) @name
+        (string (string_fragment) @name)
+        (number) @name
+        (computed_property_name) @name
+    ]) @item
+
+
+; Nested variables in function bodies
+(statement_block
+    (lexical_declaration
+        ["let" "const"] @context
+        (variable_declarator
+            name: (identifier) @name) @item))
+
+; Nested array destructuring in functions
+(statement_block
+    (lexical_declaration
+        ["let" "const"] @context
+        (variable_declarator
+            name: (array_pattern
+                [
+                    (identifier) @name @item
+                    (assignment_pattern left: (identifier) @name @item)
+                    (rest_pattern (identifier) @name @item)
+                ]))))
+
+; Nested object destructuring in functions
+(statement_block
+    (lexical_declaration
+        ["let" "const"] @context
+        (variable_declarator
+            name: (object_pattern
+                [(shorthand_property_identifier_pattern) @name @item
+                 (pair_pattern value: (identifier) @name @item)
+                 (pair_pattern value: (assignment_pattern left: (identifier) @name @item))
+                 (rest_pattern (identifier) @name @item)]))))
+
 (comment) @annotation

crates/languages/src/vtsls.rs 🔗

@@ -12,7 +12,7 @@ use std::{
     path::{Path, PathBuf},
     sync::Arc,
 };
-use util::{ResultExt, maybe, merge_json_value_into, rel_path::RelPath};
+use util::{ResultExt, maybe, merge_json_value_into};
 
 fn typescript_server_binary_arguments(server_path: &Path) -> Vec<OsString> {
     vec![server_path.into(), "--stdio".into()]
@@ -29,19 +29,19 @@ impl VtslsLspAdapter {
 
     const TYPESCRIPT_PACKAGE_NAME: &'static str = "typescript";
     const TYPESCRIPT_TSDK_PATH: &'static str = "node_modules/typescript/lib";
+    const TYPESCRIPT_YARN_TSDK_PATH: &'static str = ".yarn/sdks/typescript/lib";
 
     pub fn new(node: NodeRuntime, fs: Arc<dyn Fs>) -> Self {
         VtslsLspAdapter { node, fs }
     }
 
     async fn tsdk_path(&self, adapter: &Arc<dyn LspAdapterDelegate>) -> Option<&'static str> {
-        let is_yarn = adapter
-            .read_text_file(RelPath::unix(".yarn/sdks/typescript/lib/typescript.js").unwrap())
-            .await
-            .is_ok();
+        let yarn_sdk = adapter
+            .worktree_root_path()
+            .join(Self::TYPESCRIPT_YARN_TSDK_PATH);
 
-        let tsdk_path = if is_yarn {
-            ".yarn/sdks/typescript/lib"
+        let tsdk_path = if self.fs.is_dir(&yarn_sdk).await {
+            Self::TYPESCRIPT_YARN_TSDK_PATH
         } else {
             Self::TYPESCRIPT_TSDK_PATH
         };

crates/line_ending_selector/src/line_ending_indicator.rs 🔗

@@ -0,0 +1,68 @@
+use editor::Editor;
+use gpui::{Entity, Subscription, WeakEntity};
+use language::LineEnding;
+use ui::{Tooltip, prelude::*};
+use workspace::{StatusBarSettings, StatusItemView, item::ItemHandle, item::Settings};
+
+use crate::{LineEndingSelector, Toggle};
+
+#[derive(Default)]
+pub struct LineEndingIndicator {
+    line_ending: Option<LineEnding>,
+    active_editor: Option<WeakEntity<Editor>>,
+    _observe_active_editor: Option<Subscription>,
+}
+
+impl LineEndingIndicator {
+    fn update(&mut self, editor: Entity<Editor>, _: &mut Window, cx: &mut Context<Self>) {
+        self.line_ending = None;
+        self.active_editor = None;
+
+        if let Some((_, buffer, _)) = editor.read(cx).active_excerpt(cx) {
+            let line_ending = buffer.read(cx).line_ending();
+            self.line_ending = Some(line_ending);
+            self.active_editor = Some(editor.downgrade());
+        }
+
+        cx.notify();
+    }
+}
+
+impl Render for LineEndingIndicator {
+    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+        if !StatusBarSettings::get_global(cx).line_endings_button {
+            return div();
+        }
+
+        div().when_some(self.line_ending.as_ref(), |el, line_ending| {
+            el.child(
+                Button::new("change-line-ending", line_ending.label())
+                    .label_size(LabelSize::Small)
+                    .on_click(cx.listener(|this, _, window, cx| {
+                        if let Some(editor) = this.active_editor.as_ref() {
+                            LineEndingSelector::toggle(editor, window, cx);
+                        }
+                    }))
+                    .tooltip(|_window, cx| Tooltip::for_action("Select Line Ending", &Toggle, cx)),
+            )
+        })
+    }
+}
+
+impl StatusItemView for LineEndingIndicator {
+    fn set_active_pane_item(
+        &mut self,
+        active_pane_item: Option<&dyn ItemHandle>,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        if let Some(editor) = active_pane_item.and_then(|item| item.downcast::<Editor>()) {
+            self._observe_active_editor = Some(cx.observe_in(&editor, window, Self::update));
+            self.update(editor, window, cx);
+        } else {
+            self.line_ending = None;
+            self._observe_active_editor = None;
+        }
+        cx.notify();
+    }
+}

crates/line_ending_selector/src/line_ending_selector.rs 🔗

@@ -1,6 +1,9 @@
+mod line_ending_indicator;
+
 use editor::Editor;
 use gpui::{DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Task, WeakEntity, actions};
 use language::{Buffer, LineEnding};
+pub use line_ending_indicator::LineEndingIndicator;
 use picker::{Picker, PickerDelegate};
 use project::Project;
 use std::sync::Arc;
@@ -9,7 +12,7 @@ use util::ResultExt;
 use workspace::ModalView;
 
 actions!(
-    line_ending,
+    line_ending_selector,
     [
         /// Toggles the line ending selector modal.
         Toggle
@@ -172,10 +175,7 @@ impl PickerDelegate for LineEndingSelectorDelegate {
         _: &mut Context<Picker<Self>>,
     ) -> Option<Self::ListItem> {
         let line_ending = self.matches.get(ix)?;
-        let label = match line_ending {
-            LineEnding::Unix => "LF",
-            LineEnding::Windows => "CRLF",
-        };
+        let label = line_ending.label();
 
         let mut list_item = ListItem::new(ix)
             .inset(true)

crates/livekit_api/Cargo.toml 🔗

@@ -22,7 +22,6 @@ prost.workspace = true
 prost-types.workspace = true
 reqwest.workspace = true
 serde.workspace = true
-workspace-hack.workspace = true
 
 [build-dependencies]
 prost-build.workspace = true

crates/livekit_client/Cargo.toml 🔗

@@ -35,7 +35,7 @@ log.workspace = true
 nanoid.workspace = true
 parking_lot.workspace = true
 postage.workspace = true
-rodio = { workspace = true, features = ["wav_output", "recording"] }
+rodio.workspace = true
 serde.workspace = true
 serde_urlencoded.workspace = true
 settings.workspace = true
@@ -43,7 +43,6 @@ smallvec.workspace = true
 tokio-tungstenite.workspace = true
 ui.workspace = true
 util.workspace = true
-workspace-hack.workspace = true
 
 [target.'cfg(not(any(all(target_os = "windows", target_env = "gnu"), target_os = "freebsd")))'.dependencies]
 libwebrtc = { rev = "5f04705ac3f356350ae31534ffbc476abc9ea83d", git = "https://github.com/zed-industries/livekit-rust-sdks" }

crates/lmstudio/Cargo.toml 🔗

@@ -22,4 +22,3 @@ http_client.workspace = true
 schemars = { workspace = true, optional = true }
 serde.workspace = true
 serde_json.workspace = true
-workspace-hack.workspace = true

crates/lsp/Cargo.toml 🔗

@@ -31,7 +31,6 @@ schemars.workspace = true
 smol.workspace = true
 util.workspace = true
 release_channel.workspace = true
-workspace-hack.workspace = true
 
 [dev-dependencies]
 async-pipe.workspace = true

crates/markdown/Cargo.toml 🔗

@@ -31,7 +31,6 @@ sum_tree.workspace = true
 theme.workspace = true
 ui.workspace = true
 util.workspace = true
-workspace-hack.workspace = true
 
 [dev-dependencies]
 assets.workspace = true

crates/markdown_preview/Cargo.toml 🔗

@@ -32,7 +32,6 @@ settings.workspace = true
 theme.workspace = true
 ui.workspace = true
 util.workspace = true
-workspace-hack.workspace = true
 workspace.workspace = true
 
 [dev-dependencies]

crates/markdown_preview/src/markdown_elements.rs 🔗

@@ -1,5 +1,5 @@
 use gpui::{
-    DefiniteLength, FontStyle, FontWeight, HighlightStyle, Hsla, SharedString, StrikethroughStyle,
+    DefiniteLength, FontStyle, FontWeight, HighlightStyle, SharedString, StrikethroughStyle,
     UnderlineStyle, px,
 };
 use language::HighlightId;
@@ -104,25 +104,34 @@ pub enum HeadingLevel {
 #[derive(Debug)]
 pub struct ParsedMarkdownTable {
     pub source_range: Range<usize>,
-    pub header: ParsedMarkdownTableRow,
+    pub header: Vec<ParsedMarkdownTableRow>,
     pub body: Vec<ParsedMarkdownTableRow>,
     pub column_alignments: Vec<ParsedMarkdownTableAlignment>,
 }
 
-#[derive(Debug, Clone, Copy)]
+#[derive(Debug, Clone, Copy, Default)]
 #[cfg_attr(test, derive(PartialEq))]
 pub enum ParsedMarkdownTableAlignment {
-    /// Default text alignment.
+    #[default]
     None,
     Left,
     Center,
     Right,
 }
 
+#[derive(Debug)]
+#[cfg_attr(test, derive(PartialEq))]
+pub struct ParsedMarkdownTableColumn {
+    pub col_span: usize,
+    pub row_span: usize,
+    pub is_header: bool,
+    pub children: MarkdownParagraph,
+}
+
 #[derive(Debug)]
 #[cfg_attr(test, derive(PartialEq))]
 pub struct ParsedMarkdownTableRow {
-    pub children: Vec<MarkdownParagraph>,
+    pub columns: Vec<ParsedMarkdownTableColumn>,
 }
 
 impl Default for ParsedMarkdownTableRow {
@@ -134,12 +143,12 @@ impl Default for ParsedMarkdownTableRow {
 impl ParsedMarkdownTableRow {
     pub fn new() -> Self {
         Self {
-            children: Vec::new(),
+            columns: Vec::new(),
         }
     }
 
-    pub fn with_children(children: Vec<MarkdownParagraph>) -> Self {
-        Self { children }
+    pub fn with_columns(columns: Vec<ParsedMarkdownTableColumn>) -> Self {
+        Self { columns }
     }
 }
 
@@ -175,11 +184,7 @@ pub enum MarkdownHighlight {
 
 impl MarkdownHighlight {
     /// Converts this [`MarkdownHighlight`] to a [`HighlightStyle`].
-    pub fn to_highlight_style(
-        &self,
-        theme: &theme::SyntaxTheme,
-        link_color: Hsla,
-    ) -> Option<HighlightStyle> {
+    pub fn to_highlight_style(&self, theme: &theme::SyntaxTheme) -> Option<HighlightStyle> {
         match self {
             MarkdownHighlight::Style(style) => {
                 let mut highlight = HighlightStyle::default();
@@ -209,10 +214,8 @@ impl MarkdownHighlight {
                 if style.link {
                     highlight.underline = Some(UnderlineStyle {
                         thickness: px(1.),
-                        color: Some(link_color),
                         ..Default::default()
                     });
-                    highlight.color = Some(link_color);
                 }
 
                 Some(highlight)

crates/markdown_preview/src/markdown_parser.rs 🔗

@@ -462,9 +462,9 @@ impl<'a> MarkdownParser<'a> {
     fn parse_table(&mut self, alignment: Vec<Alignment>) -> ParsedMarkdownTable {
         let (_event, source_range) = self.previous().unwrap();
         let source_range = source_range.clone();
-        let mut header = ParsedMarkdownTableRow::new();
+        let mut header = vec![];
         let mut body = vec![];
-        let mut current_row = vec![];
+        let mut row_columns = vec![];
         let mut in_header = true;
         let column_alignments = alignment.iter().map(Self::convert_alignment).collect();
 
@@ -484,17 +484,21 @@ impl<'a> MarkdownParser<'a> {
                 Event::Start(Tag::TableCell) => {
                     self.cursor += 1;
                     let cell_contents = self.parse_text(false, Some(source_range));
-                    current_row.push(cell_contents);
+                    row_columns.push(ParsedMarkdownTableColumn {
+                        col_span: 1,
+                        row_span: 1,
+                        is_header: in_header,
+                        children: cell_contents,
+                    });
                 }
                 Event::End(TagEnd::TableHead) | Event::End(TagEnd::TableRow) => {
                     self.cursor += 1;
-                    let new_row = std::mem::take(&mut current_row);
+                    let columns = std::mem::take(&mut row_columns);
                     if in_header {
-                        header.children = new_row;
+                        header.push(ParsedMarkdownTableRow { columns: columns });
                         in_header = false;
                     } else {
-                        let row = ParsedMarkdownTableRow::with_children(new_row);
-                        body.push(row);
+                        body.push(ParsedMarkdownTableRow::with_columns(columns));
                     }
                 }
                 Event::End(TagEnd::Table) => {
@@ -941,6 +945,70 @@ impl<'a> MarkdownParser<'a> {
         }
     }
 
+    fn parse_table_row(
+        &self,
+        source_range: Range<usize>,
+        node: &Rc<markup5ever_rcdom::Node>,
+    ) -> Option<ParsedMarkdownTableRow> {
+        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<usize>,
+        node: &Rc<markup5ever_rcdom::Node>,
+    ) -> Option<ParsedMarkdownTableColumn> {
+        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);
+
+                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: matches!(name.local, local_name!("th")),
+                    children,
+                })
+            }
+            _ => None,
+        }
+    }
+
     fn consume_children(
         &self,
         source_range: Range<usize>,
@@ -1056,7 +1124,7 @@ impl<'a> MarkdownParser<'a> {
         node: &Rc<markup5ever_rcdom::Node>,
         source_range: Range<usize>,
     ) -> Option<ParsedMarkdownTable> {
-        let mut header_columns = Vec::new();
+        let mut header_rows = Vec::new();
         let mut body_rows = Vec::new();
 
         // node should be a thead or tbody element
@@ -1066,21 +1134,16 @@ impl<'a> MarkdownParser<'a> {
                     if local_name!("thead") == name.local {
                         // node should be a tr element
                         for node in node.children.borrow().iter() {
-                            let mut paragraph = MarkdownParagraph::new();
-                            self.consume_paragraph(source_range.clone(), node, &mut paragraph);
-
-                            for paragraph in paragraph.into_iter() {
-                                header_columns.push(vec![paragraph]);
+                            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() {
-                            let mut row = MarkdownParagraph::new();
-                            self.consume_paragraph(source_range.clone(), node, &mut row);
-                            body_rows.push(ParsedMarkdownTableRow::with_children(
-                                row.into_iter().map(|column| vec![column]).collect(),
-                            ));
+                            if let Some(row) = self.parse_table_row(source_range.clone(), node) {
+                                body_rows.push(row);
+                            }
                         }
                     }
                 }
@@ -1088,12 +1151,12 @@ impl<'a> MarkdownParser<'a> {
             }
         }
 
-        if !header_columns.is_empty() || !body_rows.is_empty() {
+        if !header_rows.is_empty() || !body_rows.is_empty() {
             Some(ParsedMarkdownTable {
                 source_range,
                 body: body_rows,
                 column_alignments: Vec::default(),
-                header: ParsedMarkdownTableRow::with_children(header_columns),
+                header: header_rows,
             })
         } else {
             None
@@ -1589,10 +1652,19 @@ mod tests {
             ParsedMarkdown {
                 children: vec![ParsedMarkdownElement::Table(table(
                     0..366,
-                    row(vec![text("Id", 0..366), text("Name ", 0..366)]),
+                    vec![row(vec![
+                        column(1, 1, true, text("Id", 0..366)),
+                        column(1, 1, true, text("Name ", 0..366))
+                    ])],
                     vec![
-                        row(vec![text("1", 0..366), text("Chris", 0..366)]),
-                        row(vec![text("2", 0..366), text("Dennis", 0..366)]),
+                        row(vec![
+                            column(1, 1, false, text("1", 0..366)),
+                            column(1, 1, false, text("Chris", 0..366))
+                        ]),
+                        row(vec![
+                            column(1, 1, false, text("2", 0..366)),
+                            column(1, 1, false, text("Dennis", 0..366))
+                        ]),
                     ],
                 ))],
             },
@@ -1622,10 +1694,16 @@ mod tests {
             ParsedMarkdown {
                 children: vec![ParsedMarkdownElement::Table(table(
                     0..240,
-                    row(vec![]),
+                    vec![],
                     vec![
-                        row(vec![text("1", 0..240), text("Chris", 0..240)]),
-                        row(vec![text("2", 0..240), text("Dennis", 0..240)]),
+                        row(vec![
+                            column(1, 1, false, text("1", 0..240)),
+                            column(1, 1, false, text("Chris", 0..240))
+                        ]),
+                        row(vec![
+                            column(1, 1, false, text("2", 0..240)),
+                            column(1, 1, false, text("Dennis", 0..240))
+                        ]),
                     ],
                 ))],
             },
@@ -1651,7 +1729,10 @@ mod tests {
             ParsedMarkdown {
                 children: vec![ParsedMarkdownElement::Table(table(
                     0..150,
-                    row(vec![text("Id", 0..150), text("Name", 0..150)]),
+                    vec![row(vec![
+                        column(1, 1, true, text("Id", 0..150)),
+                        column(1, 1, true, text("Name", 0..150))
+                    ])],
                     vec![],
                 ))],
             },
@@ -1833,7 +1914,10 @@ Some other content
 
         let expected_table = table(
             0..48,
-            row(vec![text("Header 1", 1..11), text("Header 2", 12..22)]),
+            vec![row(vec![
+                column(1, 1, true, text("Header 1", 1..11)),
+                column(1, 1, true, text("Header 2", 12..22)),
+            ])],
             vec![],
         );
 
@@ -1853,10 +1937,19 @@ Some other content
 
         let expected_table = table(
             0..95,
-            row(vec![text("Header 1", 1..11), text("Header 2", 12..22)]),
+            vec![row(vec![
+                column(1, 1, true, text("Header 1", 1..11)),
+                column(1, 1, true, text("Header 2", 12..22)),
+            ])],
             vec![
-                row(vec![text("Cell 1", 49..59), text("Cell 2", 60..70)]),
-                row(vec![text("Cell 3", 73..83), text("Cell 4", 84..94)]),
+                row(vec![
+                    column(1, 1, false, text("Cell 1", 49..59)),
+                    column(1, 1, false, text("Cell 2", 60..70)),
+                ]),
+                row(vec![
+                    column(1, 1, false, text("Cell 3", 73..83)),
+                    column(1, 1, false, text("Cell 4", 84..94)),
+                ]),
             ],
         );
 
@@ -2313,7 +2406,7 @@ fn main() {
 
     fn table(
         source_range: Range<usize>,
-        header: ParsedMarkdownTableRow,
+        header: Vec<ParsedMarkdownTableRow>,
         body: Vec<ParsedMarkdownTableRow>,
     ) -> ParsedMarkdownTable {
         ParsedMarkdownTable {
@@ -2324,8 +2417,22 @@ fn main() {
         }
     }
 
-    fn row(children: Vec<MarkdownParagraph>) -> ParsedMarkdownTableRow {
-        ParsedMarkdownTableRow { children }
+    fn row(columns: Vec<ParsedMarkdownTableColumn>) -> ParsedMarkdownTableRow {
+        ParsedMarkdownTableRow { columns }
+    }
+
+    fn column(
+        col_span: usize,
+        row_span: usize,
+        is_header: bool,
+        children: MarkdownParagraph,
+    ) -> ParsedMarkdownTableColumn {
+        ParsedMarkdownTableColumn {
+            col_span,
+            row_span,
+            is_header,
+            children,
+        }
     }
 
     impl PartialEq for ParsedMarkdownTable {

crates/markdown_preview/src/markdown_preview_view.rs 🔗

@@ -278,8 +278,12 @@ impl MarkdownPreviewView {
                         this.parse_markdown_from_active_editor(true, window, cx);
                     }
                     EditorEvent::SelectionsChanged { .. } => {
-                        let selection_range = editor
-                            .update(cx, |editor, cx| editor.selections.last::<usize>(cx).range());
+                        let selection_range = editor.update(cx, |editor, cx| {
+                            editor
+                                .selections
+                                .last::<usize>(&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();

crates/markdown_preview/src/markdown_renderer.rs 🔗

@@ -8,8 +8,8 @@ use fs::normalize_path;
 use gpui::{
     AbsoluteLength, AnyElement, App, AppContext as _, ClipboardItem, Context, DefiniteLength, Div,
     Element, ElementId, Entity, HighlightStyle, Hsla, ImageSource, InteractiveText, IntoElement,
-    Keystroke, Length, Modifiers, ParentElement, Render, Resource, SharedString, Styled,
-    StyledText, TextStyle, WeakEntity, Window, div, img, rems,
+    Keystroke, Modifiers, ParentElement, Render, Resource, SharedString, Styled, StyledText,
+    TextStyle, WeakEntity, Window, div, img, rems,
 };
 use settings::Settings;
 use std::{
@@ -22,7 +22,7 @@ use ui::{
     ButtonCommon, Clickable, Color, FluentBuilder, IconButton, IconName, IconSize,
     InteractiveElement, Label, LabelCommon, LabelSize, LinkPreview, Pixels, Rems,
     StatefulInteractiveElement, StyledExt, StyledImage, ToggleState, Tooltip, VisibleOnHover,
-    h_flex, relative, tooltip_container, v_flex,
+    h_flex, tooltip_container, v_flex,
 };
 use workspace::{OpenOptions, OpenVisible, Workspace};
 
@@ -51,7 +51,8 @@ pub struct RenderContext {
     buffer_text_style: TextStyle,
     text_style: TextStyle,
     border_color: Hsla,
-    element_background_color: Hsla,
+    title_bar_background_color: Hsla,
+    panel_background_color: Hsla,
     text_color: Hsla,
     link_color: Hsla,
     window_rem_size: Pixels,
@@ -87,7 +88,8 @@ impl RenderContext {
             text_style: window.text_style(),
             syntax_theme: theme.syntax().clone(),
             border_color: theme.colors().border,
-            element_background_color: theme.colors().element_background,
+            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(),
@@ -467,128 +469,100 @@ impl gpui::RenderOnce for MarkdownCheckbox {
     }
 }
 
-fn paragraph_len(paragraphs: &MarkdownParagraph) -> usize {
-    paragraphs
-        .iter()
-        .map(|paragraph| match paragraph {
-            MarkdownParagraphChunk::Text(text) => text.contents.len(),
-            // TODO: Scale column width based on image size
-            MarkdownParagraphChunk::Image(_) => 1,
-        })
-        .sum()
+fn calculate_table_columns_count(rows: &Vec<ParsedMarkdownTableRow>) -> 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::<usize>(),
+        );
+    }
+    actual_column_count
 }
 
 fn render_markdown_table(parsed: &ParsedMarkdownTable, cx: &mut RenderContext) -> AnyElement {
-    let mut max_lengths: Vec<usize> = vec![0; parsed.header.children.len()];
+    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);
 
-    for (index, cell) in parsed.header.children.iter().enumerate() {
-        let length = paragraph_len(cell);
-        max_lengths[index] = length;
-    }
+    let total_rows = parsed.header.len() + parsed.body.len();
 
-    for row in &parsed.body {
-        for (index, cell) in row.children.iter().enumerate() {
-            let length = paragraph_len(cell);
+    // Track which grid cells are occupied by spanning cells
+    let mut grid_occupied = vec![vec![false; max_column_count]; total_rows];
 
-            if index >= max_lengths.len() {
-                max_lengths.resize(index + 1, length);
-            }
+    let mut cells = Vec::with_capacity(total_rows * max_column_count);
 
-            if length > max_lengths[index] {
-                max_lengths[index] = length;
-            }
-        }
-    }
+    for (row_idx, row) in parsed.header.iter().chain(parsed.body.iter()).enumerate() {
+        let mut col_idx = 0;
 
-    let total_max_length: usize = max_lengths.iter().sum();
-    let max_column_widths: Vec<f32> = max_lengths
-        .iter()
-        .map(|&length| length as f32 / total_max_length as f32)
-        .collect();
-
-    let header = render_markdown_table_row(
-        &parsed.header,
-        &parsed.column_alignments,
-        &max_column_widths,
-        true,
-        cx,
-    );
-
-    let body: Vec<AnyElement> = parsed
-        .body
-        .iter()
-        .map(|row| {
-            render_markdown_table_row(
-                row,
-                &parsed.column_alignments,
-                &max_column_widths,
-                false,
-                cx,
-            )
-        })
-        .collect();
+        for (cell_idx, cell) in row.columns.iter().enumerate() {
+            // 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;
+            }
 
-    cx.with_common_p(v_flex())
-        .w_full()
-        .child(header)
-        .children(body)
-        .into_any()
-}
+            if col_idx >= max_column_count {
+                break;
+            }
 
-fn render_markdown_table_row(
-    parsed: &ParsedMarkdownTableRow,
-    alignments: &Vec<ParsedMarkdownTableAlignment>,
-    max_column_widths: &Vec<f32>,
-    is_header: bool,
-    cx: &mut RenderContext,
-) -> AnyElement {
-    let mut items = Vec::with_capacity(parsed.children.len());
-    let count = parsed.children.len();
+            let alignment = parsed
+                .column_alignments
+                .get(cell_idx)
+                .copied()
+                .unwrap_or_else(|| {
+                    if cell.is_header {
+                        ParsedMarkdownTableAlignment::Center
+                    } else {
+                        ParsedMarkdownTableAlignment::None
+                    }
+                });
 
-    for (index, cell) in parsed.children.iter().enumerate() {
-        let alignment = alignments
-            .get(index)
-            .copied()
-            .unwrap_or(ParsedMarkdownTableAlignment::None);
+            let container = match alignment {
+                ParsedMarkdownTableAlignment::Left | ParsedMarkdownTableAlignment::None => div(),
+                ParsedMarkdownTableAlignment::Center => v_flex().items_center(),
+                ParsedMarkdownTableAlignment::Right => v_flex().items_end(),
+            };
 
-        let contents = render_markdown_text(cell, cx);
+            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()
+                .size_full()
+                .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));
 
-        let container = match alignment {
-            ParsedMarkdownTableAlignment::Left | ParsedMarkdownTableAlignment::None => div(),
-            ParsedMarkdownTableAlignment::Center => v_flex().items_center(),
-            ParsedMarkdownTableAlignment::Right => v_flex().items_end(),
-        };
+            cells.push(cell_element);
 
-        let max_width = max_column_widths.get(index).unwrap_or(&0.0);
-        let mut cell = container
-            .w(Length::Definite(relative(*max_width)))
-            .h_full()
-            .children(contents)
-            .px_2()
-            .py_1()
-            .border_color(cx.border_color)
-            .border_l_1();
-
-        if count == index + 1 {
-            cell = cell.border_r_1();
-        }
+            // 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;
+                    }
+                }
+            }
 
-        if is_header {
-            cell = cell.bg(cx.element_background_color)
+            col_idx += cell.col_span;
         }
-
-        items.push(cell);
-    }
-
-    let mut row = h_flex().border_color(cx.border_color);
-
-    if is_header {
-        row = row.border_y_1();
-    } else {
-        row = row.border_b_1();
     }
 
-    row.children(items).into_any_element()
+    cx.with_common_p(div())
+        .grid()
+        .size_full()
+        .grid_cols(max_column_count as u16)
+        .border_1()
+        .border_color(cx.border_color)
+        .children(cells)
+        .into_any()
 }
 
 fn render_markdown_block_quote(
@@ -692,7 +666,7 @@ fn render_markdown_text(parsed_new: &MarkdownParagraph, cx: &mut RenderContext)
                 let highlights = gpui::combine_highlights(
                     parsed.highlights.iter().filter_map(|(range, highlight)| {
                         highlight
-                            .to_highlight_style(&syntax_theme, link_color)
+                            .to_highlight_style(&syntax_theme)
                             .map(|style| (range.clone(), style))
                     }),
                     parsed.regions.iter().zip(&parsed.region_ranges).filter_map(
@@ -705,6 +679,14 @@ fn render_markdown_text(parsed_new: &MarkdownParagraph, cx: &mut RenderContext)
                                         ..Default::default()
                                     },
                                 ))
+                            } else if region.link.is_some() {
+                                Some((
+                                    range.clone(),
+                                    HighlightStyle {
+                                        color: Some(link_color),
+                                        ..Default::default()
+                                    },
+                                ))
                             } else {
                                 None
                             }
@@ -891,3 +873,143 @@ impl Render for InteractiveMarkdownElementTooltip {
         })
     }
 }
+
+#[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(),
+            region_ranges: Default::default(),
+            regions: Default::default(),
+        })
+    }
+
+    fn column(
+        col_span: usize,
+        row_span: usize,
+        children: Vec<MarkdownParagraphChunk>,
+    ) -> ParsedMarkdownTableColumn {
+        ParsedMarkdownTableColumn {
+            col_span,
+            row_span,
+            is_header: false,
+            children,
+        }
+    }
+
+    fn column_with_row_span(
+        col_span: usize,
+        row_span: usize,
+        children: Vec<MarkdownParagraphChunk>,
+    ) -> ParsedMarkdownTableColumn {
+        ParsedMarkdownTableColumn {
+            col_span,
+            row_span,
+            is_header: false,
+            children,
+        }
+    }
+
+    #[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")]),
+                ])
+            ])
+        );
+    }
+}

crates/media/Cargo.toml 🔗

@@ -15,7 +15,6 @@ doctest = false
 
 [dependencies]
 anyhow.workspace = true
-workspace-hack.workspace = true
 
 [target.'cfg(target_os = "macos")'.dependencies]
 core-foundation.workspace = true

crates/menu/Cargo.toml 🔗

@@ -14,4 +14,3 @@ doctest = false
 
 [dependencies]
 gpui.workspace = true
-workspace-hack.workspace = true

crates/migrator/Cargo.toml 🔗

@@ -20,7 +20,6 @@ log.workspace = true
 streaming-iterator.workspace = true
 tree-sitter-json.workspace = true
 tree-sitter.workspace = true
-workspace-hack.workspace = true
 serde_json_lenient.workspace = true
 serde_json.workspace = true
 settings.workspace = true

crates/migrator/src/migrations.rs 🔗

@@ -123,3 +123,9 @@ pub(crate) mod m_2025_10_16 {
 
     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;
+}

crates/migrator/src/migrations/m_2025_10_16/settings.rs 🔗

@@ -37,6 +37,9 @@ fn restore_code_actions_on_format_inner(value: &mut Value, path: &[&str]) -> Res
     } 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 {
@@ -58,7 +61,7 @@ fn restore_code_actions_on_format_inner(value: &mut Value, path: &[&str]) -> Res
             .map(|code_action| (code_action, Value::Bool(true))),
     );
 
-    obj.remove("formatter");
+    obj.insert("formatter".to_string(), Value::Array(vec![]));
     obj.insert(
         "code_actions_on_format".into(),
         Value::Object(code_actions_map),

crates/migrator/src/migrations/m_2025_10_17/settings.rs 🔗

@@ -0,0 +1,23 @@
+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()),
+        _ => anyhow::bail!("Expected include_ignored to be a boolean or null"),
+    };
+    Ok(())
+}

crates/migrator/src/migrator.rs 🔗

@@ -212,6 +212,7 @@ pub fn migrate_settings(text: &str) -> Result<Option<String>> {
             &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),
     ];
     run_migrations(text, migrations)
 }
@@ -2015,9 +2016,9 @@ mod tests {
                 &r#"{
                     "code_actions_on_format": {
                         "foo": true
-                    }
-                }
-                "#
+                    },
+                    "formatter": []
+                }"#
                 .unindent(),
             ),
         );
@@ -2052,6 +2053,7 @@ mod tests {
             .unindent(),
             Some(
                 &r#"{
+                    "formatter": [],
                     "code_actions_on_format": {
                         "foo": true,
                         "bar": true,
@@ -2079,6 +2081,7 @@ mod tests {
             .unindent(),
             Some(
                 &r#"{
+                    "formatter": [],
                     "code_actions_on_format": {
                         "foo": true,
                         "qux": true,
@@ -2089,5 +2092,91 @@ mod tests {
                 .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(),
+            ),
+        );
     }
 }

crates/mistral/Cargo.toml 🔗

@@ -23,4 +23,3 @@ schemars = { workspace = true, optional = true }
 serde.workspace = true
 serde_json.workspace = true
 strum.workspace = true
-workspace-hack.workspace = true

crates/multi_buffer/Cargo.toml 🔗

@@ -43,7 +43,6 @@ text.workspace = true
 theme.workspace = true
 tree-sitter.workspace = true
 util.workspace = true
-workspace-hack.workspace = true
 
 [dev-dependencies]
 buffer_diff = { workspace = true, features = ["test-support"] }

crates/multi_buffer/src/anchor.rs 🔗

@@ -1,4 +1,4 @@
-use super::{ExcerptId, MultiBufferSnapshot, ToOffset, ToOffsetUtf16, ToPoint};
+use super::{ExcerptId, MultiBufferSnapshot, ToOffset, ToPoint};
 use language::{OffsetUtf16, Point, TextDimension};
 use std::{
     cmp::Ordering,
@@ -185,9 +185,6 @@ impl ToOffset for Anchor {
     fn to_offset(&self, snapshot: &MultiBufferSnapshot) -> usize {
         self.summary(snapshot)
     }
-}
-
-impl ToOffsetUtf16 for Anchor {
     fn to_offset_utf16(&self, snapshot: &MultiBufferSnapshot) -> OffsetUtf16 {
         self.summary(snapshot)
     }
@@ -197,6 +194,9 @@ 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 {

crates/multi_buffer/src/multi_buffer.rs 🔗

@@ -1,7 +1,11 @@
 mod anchor;
 #[cfg(test)]
 mod multi_buffer_tests;
+mod path_key;
 mod position;
+mod transaction;
+
+use self::transaction::History;
 
 pub use anchor::{Anchor, AnchorRangeExt, Offset};
 pub use position::{TypedOffset, TypedPoint, TypedRow};
@@ -13,7 +17,7 @@ use buffer_diff::{
 };
 use clock::ReplicaId;
 use collections::{BTreeMap, Bound, HashMap, HashSet};
-use gpui::{App, AppContext as _, Context, Entity, EntityId, EventEmitter, Task};
+use gpui::{App, Context, Entity, EntityId, EventEmitter};
 use itertools::Itertools;
 use language::{
     AutoindentMode, Buffer, BufferChunks, BufferRow, BufferSnapshot, Capability, CharClassifier,
@@ -24,6 +28,9 @@ use language::{
     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;
@@ -40,7 +47,7 @@ use std::{
     rc::Rc,
     str,
     sync::Arc,
-    time::{Duration, Instant},
+    time::Duration,
 };
 use sum_tree::{Bias, Cursor, Dimension, Dimensions, SumTree, Summary, TreeMap};
 use text::{
@@ -49,9 +56,9 @@ use text::{
     subscription::{Subscription, Topic},
 };
 use theme::SyntaxTheme;
-use util::{post_inc, rel_path::RelPath};
+use util::post_inc;
 
-const NEWLINES: &[u8] = &[b'\n'; u8::MAX as usize];
+pub use self::path_key::PathKey;
 
 #[derive(Debug, Default, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
 pub struct ExcerptId(u32);
@@ -64,17 +71,22 @@ pub struct MultiBuffer {
     /// Use [`MultiBuffer::snapshot`] to get a up-to-date snapshot.
     snapshot: RefCell<MultiBufferSnapshot>,
     /// Contains the state of the buffers being edited
-    buffers: RefCell<HashMap<BufferId, BufferState>>,
-    // only used by consumers using `set_excerpts_for_buffer`
+    buffers: HashMap<BufferId, BufferState>,
+    /// Mapping from path keys to their excerpts.
     excerpts_by_path: BTreeMap<PathKey, Vec<ExcerptId>>,
+    /// Mapping from excerpt IDs to their path key.
     paths_by_excerpt: HashMap<ExcerptId, PathKey>,
+    /// Mapping from buffer IDs to their diff states
     diffs: HashMap<BufferId, DiffState>,
-    // all_diff_hunks_expanded: bool,
     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<String>,
+    /// The writing capability of the multi-buffer.
     capability: Capability,
     buffer_changed_since_sync: Rc<Cell<bool>>,
 }
@@ -158,40 +170,6 @@ impl MultiBufferDiffHunk {
     }
 }
 
-#[derive(PartialEq, Eq, Ord, PartialOrd, Clone, Hash, Debug)]
-pub struct PathKey {
-    // Used by the derived PartialOrd & Ord
-    sort_prefix: Option<u64>,
-    path: Arc<RelPath>,
-}
-
-impl PathKey {
-    pub fn with_sort_prefix(sort_prefix: u64, path: Arc<RelPath>) -> Self {
-        Self {
-            sort_prefix: Some(sort_prefix),
-            path,
-        }
-    }
-
-    pub fn for_buffer(buffer: &Entity<Buffer>, 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(),
-            }
-        }
-    }
-
-    #[cfg(any(test, feature = "test-support"))]
-    pub fn path(&self) -> &Arc<RelPath> {
-        &self.path
-    }
-}
-
 pub type MultiBufferPoint = Point;
 type ExcerptOffset = TypedOffset<Excerpt>;
 type ExcerptPoint = TypedPoint<Excerpt>;
@@ -213,44 +191,20 @@ impl std::ops::Add<usize> for MultiBufferRow {
     }
 }
 
-#[derive(Clone)]
-struct History {
-    next_transaction_id: TransactionId,
-    undo_stack: Vec<Transaction>,
-    redo_stack: Vec<Transaction>,
-    transaction_depth: usize,
-    group_interval: Duration,
-}
-
-#[derive(Clone)]
-struct Transaction {
-    id: TransactionId,
-    buffer_transactions: HashMap<BufferId, text::TransactionId>,
-    first_edit_at: Instant,
-    last_edit_at: Instant,
-    suppress_grouping: bool,
-}
-
 pub trait ToOffset: 'static + fmt::Debug {
     fn to_offset(&self, snapshot: &MultiBufferSnapshot) -> usize;
-}
-
-pub trait ToOffsetUtf16: 'static + fmt::Debug {
     fn to_offset_utf16(&self, snapshot: &MultiBufferSnapshot) -> OffsetUtf16;
 }
 
 pub trait ToPoint: 'static + fmt::Debug {
     fn to_point(&self, snapshot: &MultiBufferSnapshot) -> Point;
-}
-
-pub trait ToPointUtf16: 'static + fmt::Debug {
     fn to_point_utf16(&self, snapshot: &MultiBufferSnapshot) -> PointUtf16;
 }
 
 struct BufferState {
     buffer: Entity<Buffer>,
-    last_version: clock::Global,
-    last_non_text_state_update_count: usize,
+    last_version: RefCell<clock::Global>,
+    last_non_text_state_update_count: Cell<usize>,
     excerpts: Vec<Locator>,
     _subscriptions: [gpui::Subscription; 2],
 }
@@ -281,19 +235,20 @@ impl DiffState {
 /// The contents of a [`MultiBuffer`] at a single point in time.
 #[derive(Clone, Default)]
 pub struct MultiBufferSnapshot {
-    singleton: bool,
     excerpts: SumTree<Excerpt>,
-    excerpt_ids: SumTree<ExcerptIdMapping>,
     diffs: TreeMap<BufferId, BufferDiffSnapshot>,
     diff_transforms: SumTree<DiffTransform>,
-    replaced_excerpts: TreeMap<ExcerptId, ExcerptId>,
-    trailing_excerpt_update_count: usize,
-    all_diff_hunks_expanded: bool,
     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<ExcerptIdMapping>,
+    replaced_excerpts: TreeMap<ExcerptId, ExcerptId>,
+    trailing_excerpt_update_count: usize,
+    all_diff_hunks_expanded: bool,
     show_headers: bool,
 }
 
@@ -550,7 +505,7 @@ struct MultiBufferRegion<'a, D: TextDimension> {
 struct ExcerptChunks<'a> {
     excerpt_id: ExcerptId,
     content_chunks: BufferChunks<'a>,
-    footer_height: usize,
+    has_footer: bool,
 }
 
 #[derive(Debug)]
@@ -612,56 +567,57 @@ impl IndentGuide {
 
 impl MultiBuffer {
     pub fn new(capability: Capability) -> Self {
-        Self {
-            snapshot: RefCell::new(MultiBufferSnapshot {
+        Self::new_(
+            capability,
+            MultiBufferSnapshot {
                 show_headers: true,
                 ..MultiBufferSnapshot::default()
-            }),
-            buffers: RefCell::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 {
-                next_transaction_id: clock::Lamport::default(),
-                undo_stack: Vec::new(),
-                redo_stack: Vec::new(),
-                transaction_depth: 0,
-                group_interval: Duration::from_millis(300),
             },
-        }
+        )
     }
 
     pub fn without_headers(capability: Capability) -> Self {
+        Self::new_(capability, Default::default())
+    }
+
+    pub fn singleton(buffer: Entity<Buffer>, cx: &mut Context<Self>) -> Self {
+        let mut this = Self::new_(
+            buffer.read(cx).capability(),
+            MultiBufferSnapshot {
+                singleton: true,
+                ..MultiBufferSnapshot::default()
+            },
+        );
+        this.singleton = true;
+        this.push_excerpts(
+            buffer,
+            [ExcerptRange::new(text::Anchor::MIN..text::Anchor::MAX)],
+            cx,
+        );
+        this
+    }
+
+    #[inline]
+    pub fn new_(capability: Capability, snapshot: MultiBufferSnapshot) -> Self {
         Self {
-            snapshot: Default::default(),
+            snapshot: RefCell::new(snapshot),
             buffers: Default::default(),
-            excerpts_by_path: Default::default(),
-            paths_by_excerpt: Default::default(),
             diffs: HashMap::default(),
-            subscriptions: Default::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 {
-                next_transaction_id: Default::default(),
-                undo_stack: Default::default(),
-                redo_stack: Default::default(),
-                transaction_depth: 0,
-                group_interval: Duration::from_millis(300),
-            },
-            title: Default::default(),
+            history: History::default(),
         }
     }
 
     pub fn clone(&self, new_cx: &mut Context<Self>) -> Self {
         let mut buffers = HashMap::default();
         let buffer_changed_since_sync = Rc::new(Cell::new(false));
-        for (buffer_id, buffer_state) in self.buffers.borrow().iter() {
+        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));
             });
@@ -670,7 +626,9 @@ impl MultiBuffer {
                 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,
+                    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()),
@@ -685,7 +643,7 @@ impl MultiBuffer {
         }
         Self {
             snapshot: RefCell::new(self.snapshot.borrow().clone()),
-            buffers: RefCell::new(buffers),
+            buffers: buffers,
             excerpts_by_path: Default::default(),
             paths_by_excerpt: Default::default(),
             diffs: diff_bases,
@@ -698,6 +656,10 @@ impl MultiBuffer {
         }
     }
 
+    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
@@ -707,18 +669,6 @@ impl MultiBuffer {
         self.capability == Capability::ReadOnly
     }
 
-    pub fn singleton(buffer: Entity<Buffer>, cx: &mut Context<Self>) -> Self {
-        let mut this = Self::new(buffer.read(cx).capability());
-        this.singleton = true;
-        this.push_excerpts(
-            buffer,
-            [ExcerptRange::new(text::Anchor::MIN..text::Anchor::MAX)],
-            cx,
-        );
-        this.snapshot.borrow_mut().singleton = true;
-        this
-    }
-
     /// Returns an up-to-date snapshot of the MultiBuffer.
     pub fn snapshot(&self, cx: &App) -> MultiBufferSnapshot {
         self.sync(cx);
@@ -732,15 +682,7 @@ impl MultiBuffer {
 
     pub fn as_singleton(&self) -> Option<Entity<Buffer>> {
         if self.singleton {
-            Some(
-                self.buffers
-                    .borrow()
-                    .values()
-                    .next()
-                    .unwrap()
-                    .buffer
-                    .clone(),
-            )
+            Some(self.buffers.values().next().unwrap().buffer.clone())
         } else {
             None
         }
@@ -773,20 +715,11 @@ impl MultiBuffer {
     }
 
     pub fn is_empty(&self) -> bool {
-        self.buffers.borrow().is_empty()
-    }
-
-    pub fn symbols_containing<T: ToOffset>(
-        &self,
-        offset: T,
-        theme: Option<&SyntaxTheme>,
-        cx: &App,
-    ) -> Option<(BufferId, Vec<OutlineItem<Anchor>>)> {
-        self.read(cx).symbols_containing(offset, theme)
+        self.buffers.is_empty()
     }
 
     pub fn edit<I, S, T>(
-        &self,
+        &mut self,
         edits: I,
         autoindent_mode: Option<AutoindentMode>,
         cx: &mut Context<Self>,
@@ -795,11 +728,15 @@ impl MultiBuffer {
         S: ToOffset,
         T: Into<Arc<str>>,
     {
-        let snapshot = self.read(cx);
+        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(&snapshot)..range.end.to_offset(&snapshot);
+                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);
                 }
@@ -807,20 +744,15 @@ impl MultiBuffer {
             })
             .collect::<Vec<_>>();
 
-        return edit_internal(self, snapshot, edits, autoindent_mode, cx);
+        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: &MultiBuffer,
-            snapshot: Ref<MultiBufferSnapshot>,
+            this: &mut MultiBuffer,
             edits: Vec<(Range<usize>, Arc<str>)>,
             mut autoindent_mode: Option<AutoindentMode>,
             cx: &mut Context<MultiBuffer>,
         ) {
-            if this.read_only() || this.buffers.borrow().is_empty() {
-                return;
-            }
-
             let original_indent_columns = match &mut autoindent_mode {
                 Some(AutoindentMode::Block {
                     original_indent_columns,
@@ -828,86 +760,84 @@ impl MultiBuffer {
                 _ => Default::default(),
             };
 
-            let (buffer_edits, edited_excerpt_ids) =
-                this.convert_edits_to_buffer_edits(edits, &snapshot, &original_indent_columns);
-            drop(snapshot);
+            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.borrow()[&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<str> = Arc::default();
+                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<str> = 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 {
-                            mut range,
-                            mut new_text,
-                            mut is_insertion,
-                            original_indent_column,
-                            excerpt_id,
-                        }) = edits.next()
+                            range: next_range,
+                            is_insertion: next_is_insertion,
+                            new_text: next_new_text,
+                            excerpt_id: next_excerpt_id,
+                            ..
+                        }) = edits.peek()
                         {
-                            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 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(),
-                                ));
-                            }
+                        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()
-                            };
+                    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);
-                    })
+                    buffer.edit(deletions, deletion_autoindent_mode, cx);
+                    buffer.edit(insertions, insertion_autoindent_mode, cx);
+                })
             }
 
             cx.emit(Event::ExcerptsEdited {
@@ -918,7 +848,6 @@ impl MultiBuffer {
     }
 
     fn convert_edits_to_buffer_edits(
-        &self,
         edits: Vec<(Range<usize>, Arc<str>)>,
         snapshot: &MultiBufferSnapshot,
         original_indent_columns: &[Option<u32>],
@@ -1038,17 +967,21 @@ impl MultiBuffer {
         (buffer_edits, edited_excerpt_ids)
     }
 
-    pub fn autoindent_ranges<I, S>(&self, ranges: I, cx: &mut Context<Self>)
+    pub fn autoindent_ranges<I, S>(&mut self, ranges: I, cx: &mut Context<Self>)
     where
         I: IntoIterator<Item = Range<S>>,
         S: ToOffset,
     {
-        let snapshot = self.read(cx);
+        if self.read_only() || self.buffers.is_empty() {
+            return;
+        }
+        self.sync_mut(cx);
         let empty = Arc::<str>::from("");
         let edits = ranges
             .into_iter()
             .map(|range| {
-                let mut range = range.start.to_offset(&snapshot)..range.end.to_offset(&snapshot);
+                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);
                 }
@@ -1056,21 +989,15 @@ impl MultiBuffer {
             })
             .collect::<Vec<_>>();
 
-        return autoindent_ranges_internal(self, snapshot, edits, cx);
+        return autoindent_ranges_internal(self, edits, cx);
 
         fn autoindent_ranges_internal(
-            this: &MultiBuffer,
-            snapshot: Ref<MultiBufferSnapshot>,
+            this: &mut MultiBuffer,
             edits: Vec<(Range<usize>, Arc<str>)>,
             cx: &mut Context<MultiBuffer>,
         ) {
-            if this.read_only() || this.buffers.borrow().is_empty() {
-                return;
-            }
-
             let (buffer_edits, edited_excerpt_ids) =
-                this.convert_edits_to_buffer_edits(edits, &snapshot, &[]);
-            drop(snapshot);
+                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 {
@@ -1088,11 +1015,9 @@ impl MultiBuffer {
                     ranges.push(edit.range);
                 }
 
-                this.buffers.borrow()[&buffer_id]
-                    .buffer
-                    .update(cx, |buffer, cx| {
-                        buffer.autoindent_ranges(ranges, cx);
-                    })
+                this.buffers[&buffer_id].buffer.update(cx, |buffer, cx| {
+                    buffer.autoindent_ranges(ranges, cx);
+                })
             }
 
             cx.emit(Event::ExcerptsEdited {
@@ -1102,9 +1027,9 @@ impl MultiBuffer {
         }
     }
 
-    // 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.
+    /// 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,
@@ -1122,187 +1047,6 @@ impl MultiBuffer {
         multibuffer_point + (empty_line_start - buffer_point)
     }
 
-    pub fn start_transaction(&mut self, cx: &mut Context<Self>) -> Option<TransactionId> {
-        self.start_transaction_at(Instant::now(), cx)
-    }
-
-    pub fn start_transaction_at(
-        &mut self,
-        now: Instant,
-        cx: &mut Context<Self>,
-    ) -> Option<TransactionId> {
-        if let Some(buffer) = self.as_singleton() {
-            return buffer.update(cx, |buffer, _| buffer.start_transaction_at(now));
-        }
-
-        for BufferState { buffer, .. } in self.buffers.borrow().values() {
-            buffer.update(cx, |buffer, _| buffer.start_transaction_at(now));
-        }
-        self.history.start_transaction(now)
-    }
-
-    pub fn last_transaction_id(&self, cx: &App) -> Option<TransactionId> {
-        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<Self>) -> Option<TransactionId> {
-        self.end_transaction_at(Instant::now(), cx)
-    }
-
-    pub fn end_transaction_at(
-        &mut self,
-        now: Instant,
-        cx: &mut Context<Self>,
-    ) -> Option<TransactionId> {
-        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.borrow().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<D>(
-        &self,
-        transaction_id: TransactionId,
-        cx: &App,
-    ) -> Vec<Range<D>>
-    where
-        D: TextDimension + Ord + Sub<D, Output = D>,
-    {
-        let Some(transaction) = self.history.transaction(transaction_id) else {
-            return Vec::new();
-        };
-
-        let mut ranges = Vec::new();
-        let snapshot = self.read(cx);
-        let buffers = self.buffers.borrow();
-        let mut cursor = snapshot.excerpts.cursor::<ExcerptSummary>(());
-
-        for (buffer_id, buffer_transaction) in &transaction.buffer_transactions {
-            let Some(buffer_state) = buffers.get(buffer_id) else {
-                continue;
-            };
-
-            let buffer = buffer_state.buffer.read(cx);
-            for range in buffer.edited_ranges_for_transaction_id::<D>(*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::<D>(buffer);
-                        let excerpt_buffer_end = excerpt.range.context.end.summary::<D>(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_text_summary(&cursor.start().text);
-
-                            let mut start = excerpt_start;
-                            start.add_assign(&(range.start - excerpt_buffer_start));
-                            let mut end = excerpt_start;
-                            end.add_assign(&(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<Self>,
-    ) {
-        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.borrow().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>) {
-        self.history.finalize_last_transaction();
-        for BufferState { buffer, .. } in self.buffers.borrow().values() {
-            buffer.update(cx, |buffer, _| {
-                buffer.finalize_last_transaction();
-            });
-        }
-    }
-
-    pub fn push_transaction<'a, T>(&mut self, buffer_transactions: T, cx: &Context<Self>)
-    where
-        T: IntoIterator<Item = (&'a Entity<Buffer>, &'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<Self>,
-    ) {
-        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 set_active_selections(
         &self,
         selections: &[Selection<Anchor>],
@@ -1345,550 +1089,80 @@ impl MultiBuffer {
             }
         }
 
-        for (buffer_id, buffer_state) in self.buffers.borrow().iter() {
+        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.borrow()[&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<Self>) {
-        for buffer in self.buffers.borrow().values() {
-            buffer
-                .buffer
-                .update(cx, |buffer, cx| buffer.remove_active_selections(cx));
-        }
-    }
-
-    pub fn undo(&mut self, cx: &mut Context<Self>) -> Option<TransactionId> {
-        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.borrow().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<Self>) -> Option<TransactionId> {
-        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 &mut transaction.buffer_transactions {
-                if let Some(BufferState { buffer, .. }) = self.buffers.borrow().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<Self>) {
-        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.borrow().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<Self>) {
-        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.borrow_mut().get_mut(&buffer_id) {
-                    state.buffer.update(cx, |buffer, _| {
-                        buffer.forget_transaction(buffer_transaction_id);
-                    });
-                }
-            }
-        }
-    }
-
-    pub fn push_excerpts<O>(
-        &mut self,
-        buffer: Entity<Buffer>,
-        ranges: impl IntoIterator<Item = ExcerptRange<O>>,
-        cx: &mut Context<Self>,
-    ) -> Vec<ExcerptId>
-    where
-        O: text::ToOffset,
-    {
-        self.insert_excerpts_after(ExcerptId::max(), buffer, ranges, cx)
-    }
-
-    pub fn location_for_path(&self, path: &PathKey, cx: &App) -> Option<Anchor> {
-        let excerpt_id = self.excerpts_by_path.get(path)?.first()?;
-        let snapshot = self.snapshot(cx);
-        let excerpt = snapshot.excerpt(*excerpt_id)?;
-        Some(Anchor::in_buffer(
-            *excerpt_id,
-            excerpt.buffer_id,
-            excerpt.range.context.start,
-        ))
-    }
-
-    pub fn excerpt_paths(&self) -> impl Iterator<Item = &PathKey> {
-        self.excerpts_by_path.keys()
-    }
-
-    fn expand_excerpts_with_paths(
-        &mut self,
-        ids: impl IntoIterator<Item = ExcerptId>,
-        line_count: u32,
-        direction: ExpandExcerptDirection,
-        cx: &mut Context<Self>,
-    ) {
-        let grouped = ids
-            .into_iter()
-            .chunk_by(|id| self.paths_by_excerpt.get(id).cloned())
-            .into_iter()
-            .flat_map(|(k, v)| Some((k?, v.into_iter().collect::<Vec<_>>())))
-            .collect::<Vec<_>>();
-        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 expanded_ranges = excerpt_ids.iter().filter_map(|excerpt_id| {
-                let excerpt = snapshot.excerpt(*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<ExcerptRange<Point>> = 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_ids.first() else {
-                continue;
-            };
-            let Some(buffer_id) = &snapshot.buffer_id_for_excerpt(*excerpt_id) else {
-                continue;
-            };
-
-            let Some(buffer) = self
-                .buffers
-                .borrow()
-                .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.
-    pub fn set_excerpts_for_path(
-        &mut self,
-        path: PathKey,
-        buffer: Entity<Buffer>,
-        ranges: impl IntoIterator<Item = Range<Point>>,
-        context_line_count: u32,
-        cx: &mut Context<Self>,
-    ) -> (Vec<Range<Anchor>>, 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>,
-        buffer_snapshot: &BufferSnapshot,
-        excerpt_ranges: Vec<ExcerptRange<Point>>,
-        cx: &mut Context<Self>,
-    ) -> (Vec<Range<Anchor>>, 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,
-        buffer: Entity<Buffer>,
-        ranges: Vec<Range<text::Anchor>>,
-        context_line_count: u32,
-        cx: &mut Context<Self>,
-    ) -> Task<Vec<Range<Anchor>>> {
-        let buffer_snapshot = buffer.read(cx).snapshot();
-        let path_key = PathKey::for_buffer(&buffer, cx);
-        cx.spawn(async move |multi_buffer, cx| {
-            let snapshot = buffer_snapshot.clone();
-            let (excerpt_ranges, new, counts) = cx
-                .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(cx, 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()
-        })
-    }
-
-    /// 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<Buffer>,
-        ranges: Vec<ExcerptRange<Point>>,
-        buffer_snapshot: &BufferSnapshot,
-        new: Vec<ExcerptRange<Point>>,
-        counts: Vec<usize>,
-        cx: &mut Context<Self>,
-    ) -> (Vec<Range<Anchor>>, 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.remote_id(),
-                    buffer_snapshot.anchor_before(&range.primary.start)
-                        ..buffer_snapshot.anchor_after(&range.primary.end),
-                );
-                result.push(range)
-            }
-        }
-        (result, added_a_new_excerpt)
-    }
-
-    fn merge_excerpt_ranges<'a>(
-        expanded_ranges: impl IntoIterator<Item = &'a ExcerptRange<Point>> + 'a,
-    ) -> (Vec<ExcerptRange<Point>>, Vec<usize>) {
-        let mut merged_ranges: Vec<ExcerptRange<Point>> = Vec::new();
-        let mut counts: Vec<usize> = Vec::new();
-        for range in expanded_ranges {
-            if let Some(last_range) = merged_ranges.last_mut() {
-                debug_assert!(
-                    last_range.context.start <= range.context.start,
-                    "Last range: {last_range:?} 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)
-    }
-
-    fn update_path_excerpts(
-        &mut self,
-        path: PathKey,
-        buffer: Entity<Buffer>,
-        buffer_snapshot: &BufferSnapshot,
-        new: Vec<ExcerptRange<Point>>,
-        cx: &mut Context<Self>,
-    ) -> (Vec<ExcerptId>, bool) {
-        let mut insert_after = self
-            .excerpts_by_path
-            .range(..path.clone())
-            .next_back()
-            .map(|(_, value)| *value.last().unwrap())
-            .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<Point>)> = Vec::new();
-        let mut added_a_new_excerpt = false;
-        let snapshot = self.snapshot(cx);
-
-        let mut next_excerpt_id =
-            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::<Option<&Locator>>(());
-        excerpts_cursor.next();
-
-        loop {
-            let new = new_iter.peek();
-            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
-            };
-
-            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
-                        .borrow_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
-                            .borrow_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));
-                    }
-                }
-            };
+                    .buffer
+                    .update(cx, |buffer, cx| buffer.remove_active_selections(cx));
+            }
         }
 
-        self.insert_excerpts_with_ids_after(insert_after, buffer, to_insert, cx);
-        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());
-            }
-            self.excerpts_by_path
-                .insert(path, excerpt_ids.iter().dedup().cloned().collect());
+        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);
+            });
         }
+    }
 
-        (excerpt_ids, added_a_new_excerpt)
+    pub fn remove_active_selections(&self, cx: &mut Context<Self>) {
+        for buffer in self.buffers.values() {
+            buffer
+                .buffer
+                .update(cx, |buffer, cx| buffer.remove_active_selections(cx));
+        }
     }
 
-    pub fn paths(&self) -> impl Iterator<Item = PathKey> + '_ {
-        self.excerpts_by_path.keys().cloned()
+    pub fn push_excerpts<O>(
+        &mut self,
+        buffer: Entity<Buffer>,
+        ranges: impl IntoIterator<Item = ExcerptRange<O>>,
+        cx: &mut Context<Self>,
+    ) -> Vec<ExcerptId>
+    where
+        O: text::ToOffset,
+    {
+        self.insert_excerpts_after(ExcerptId::max(), buffer, ranges, cx)
     }
 
-    pub fn remove_excerpts_for_path(&mut self, path: PathKey, cx: &mut Context<Self>) {
-        if let Some(to_remove) = self.excerpts_by_path.remove(&path) {
-            self.remove_excerpts(to_remove, cx)
+    fn merge_excerpt_ranges<'a>(
+        expanded_ranges: impl IntoIterator<Item = &'a ExcerptRange<Point>> + 'a,
+    ) -> (Vec<ExcerptRange<Point>>, Vec<usize>) {
+        let mut merged_ranges: Vec<ExcerptRange<Point>> = Vec::new();
+        let mut counts: Vec<usize> = Vec::new();
+        for range in expanded_ranges {
+            if let Some(last_range) = merged_ranges.last_mut() {
+                debug_assert!(
+                    last_range.context.start <= range.context.start,
+                    "Last range: {last_range:?} 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<O>(

crates/multi_buffer/src/multi_buffer_tests.rs 🔗

@@ -7,6 +7,7 @@ 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;
@@ -78,7 +79,9 @@ fn test_remote(cx: &mut App) {
         let ops = cx
             .background_executor()
             .block(host_buffer.read(cx).serialize_ops(None, cx));
-        let mut buffer = Buffer::from_proto(1, Capability::ReadWrite, state, None).unwrap();
+        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()),
@@ -797,7 +800,13 @@ async fn test_set_anchored_excerpts_for_path(cx: &mut TestAppContext) {
     let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
     let anchor_ranges_1 = multibuffer
         .update(cx, |multibuffer, cx| {
-            multibuffer.set_anchored_excerpts_for_path(buffer_1.clone(), ranges_1, 2, 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));
@@ -814,7 +823,13 @@ async fn test_set_anchored_excerpts_for_path(cx: &mut TestAppContext) {
     );
     let anchor_ranges_2 = multibuffer
         .update(cx, |multibuffer, cx| {
-            multibuffer.set_anchored_excerpts_for_path(buffer_2.clone(), ranges_2, 2, 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));
@@ -2970,7 +2985,7 @@ fn test_history(cx: &mut App) {
     });
     let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
     multibuffer.update(cx, |this, _| {
-        this.history.group_interval = group_interval;
+        this.set_group_interval(group_interval);
     });
     multibuffer.update(cx, |multibuffer, cx| {
         multibuffer.push_excerpts(
@@ -3624,7 +3639,7 @@ fn assert_position_translation(snapshot: &MultiBufferSnapshot) {
 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(0, buffer_id, snapshot.text());
+    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::<Vec<_>>();

crates/multi_buffer/src/path_key.rs 🔗

@@ -0,0 +1,417 @@
+use std::{mem, ops::Range, sync::Arc};

+

+use collections::HashSet;

+use gpui::{App, AppContext, Context, Entity, Task};

+use itertools::Itertools;

+use language::{Buffer, BufferSnapshot};

+use rope::Point;

+use text::{Bias, OffsetRangeExt, locator::Locator};

+use util::{post_inc, rel_path::RelPath};

+

+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<u64>,

+    pub path: Arc<RelPath>,

+}

+

+impl PathKey {

+    pub fn with_sort_prefix(sort_prefix: u64, path: Arc<RelPath>) -> Self {

+        Self {

+            sort_prefix: Some(sort_prefix),

+            path,

+        }

+    }

+

+    pub fn for_buffer(buffer: &Entity<Buffer>, 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<Item = PathKey> + '_ {

+        self.excerpts_by_path.keys().cloned()

+    }

+

+    pub fn remove_excerpts_for_path(&mut self, path: PathKey, cx: &mut Context<Self>) {

+        if let Some(to_remove) = self.excerpts_by_path.remove(&path) {

+            self.remove_excerpts(to_remove, cx)

+        }

+    }

+

+    pub fn location_for_path(&self, path: &PathKey, cx: &App) -> Option<Anchor> {

+        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.buffer_id,

+            excerpt.range.context.start,

+        ))

+    }

+

+    pub fn excerpt_paths(&self) -> impl Iterator<Item = &PathKey> {

+        self.excerpts_by_path.keys()

+    }

+

+    /// Sets excerpts, returns `true` if at least one new excerpt was added.

+    pub fn set_excerpts_for_path(

+        &mut self,

+        path: PathKey,

+        buffer: Entity<Buffer>,

+        ranges: impl IntoIterator<Item = Range<Point>>,

+        context_line_count: u32,

+        cx: &mut Context<Self>,

+    ) -> (Vec<Range<Anchor>>, 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>,

+        buffer_snapshot: &BufferSnapshot,

+        excerpt_ranges: Vec<ExcerptRange<Point>>,

+        cx: &mut Context<Self>,

+    ) -> (Vec<Range<Anchor>>, 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<Buffer>,

+        ranges: Vec<Range<text::Anchor>>,

+        context_line_count: u32,

+        cx: &mut Context<Self>,

+    ) -> Task<Vec<Range<Anchor>>> {

+        let buffer_snapshot = buffer.read(cx).snapshot();

+        cx.spawn(async move |multi_buffer, cx| {

+            let snapshot = buffer_snapshot.clone();

+            let (excerpt_ranges, new, counts) = cx

+                .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(cx, 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(super) fn expand_excerpts_with_paths(

+        &mut self,

+        ids: impl IntoIterator<Item = ExcerptId>,

+        line_count: u32,

+        direction: ExpandExcerptDirection,

+        cx: &mut Context<Self>,

+    ) {

+        let grouped = ids

+            .into_iter()

+            .chunk_by(|id| self.paths_by_excerpt.get(id).cloned())

+            .into_iter()

+            .flat_map(|(k, v)| Some((k?, v.into_iter().collect::<Vec<_>>())))

+            .collect::<Vec<_>>();

+        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 expanded_ranges = excerpt_ids.iter().filter_map(|excerpt_id| {

+                let excerpt = snapshot.excerpt(*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<ExcerptRange<Point>> = 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_ids.first() 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<Buffer>,

+        ranges: Vec<ExcerptRange<Point>>,

+        buffer_snapshot: &BufferSnapshot,

+        new: Vec<ExcerptRange<Point>>,

+        counts: Vec<usize>,

+        cx: &mut Context<Self>,

+    ) -> (Vec<Range<Anchor>>, 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.remote_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>,

+        buffer_snapshot: &BufferSnapshot,

+        new: Vec<ExcerptRange<Point>>,

+        cx: &mut Context<Self>,

+    ) -> (Vec<ExcerptId>, bool) {

+        let mut insert_after = self

+            .excerpts_by_path

+            .range(..path.clone())

+            .next_back()

+            .map(|(_, value)| *value.last().unwrap())

+            .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<Point>)> = Vec::new();

+        let mut added_a_new_excerpt = false;

+        let snapshot = self.snapshot(cx);

+

+        let mut next_excerpt_id =

+            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::<Option<&Locator>>(());

+        excerpts_cursor.next();

+

+        loop {

+            let new = new_iter.peek();

+            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

+            };

+

+            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);

+        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());

+            }

+            self.excerpts_by_path

+                .insert(path, excerpt_ids.iter().dedup().cloned().collect());

+        }

+

+        (excerpt_ids, added_a_new_excerpt)

+    }

+}

crates/multi_buffer/src/transaction.rs 🔗

@@ -0,0 +1,524 @@
+use gpui::{App, Context, Entity};

+use language::{self, Buffer, TextDimension, TransactionId};

+use std::{

+    collections::HashMap,

+    ops::{Range, Sub},

+    time::{Duration, Instant},

+};

+use sum_tree::Bias;

+use text::BufferId;

+

+use crate::BufferState;

+

+use super::{Event, ExcerptSummary, MultiBuffer};

+

+#[derive(Clone)]

+pub(super) struct History {

+    next_transaction_id: TransactionId,

+    undo_stack: Vec<Transaction>,

+    redo_stack: Vec<Transaction>,

+    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<BufferId, text::TransactionId>,

+    first_edit_at: Instant,

+    last_edit_at: Instant,

+    suppress_grouping: bool,

+}

+

+impl History {

+    fn start_transaction(&mut self, now: Instant) -> Option<TransactionId> {

+        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<BufferId, text::TransactionId>,

+    ) -> 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<MultiBuffer>,

+    ) where

+        T: IntoIterator<Item = (&'a Entity<Buffer>, &'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<Transaction> {

+        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<TransactionId> {

+        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<TransactionId> {

+        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<Self>) -> Option<TransactionId> {

+        self.start_transaction_at(Instant::now(), cx)

+    }

+

+    pub fn start_transaction_at(

+        &mut self,

+        now: Instant,

+        cx: &mut Context<Self>,

+    ) -> Option<TransactionId> {

+        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<TransactionId> {

+        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<Self>) -> Option<TransactionId> {

+        self.end_transaction_at(Instant::now(), cx)

+    }

+

+    pub fn end_transaction_at(

+        &mut self,

+        now: Instant,

+        cx: &mut Context<Self>,

+    ) -> Option<TransactionId> {

+        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<D>(

+        &self,

+        transaction_id: TransactionId,

+        cx: &App,

+    ) -> Vec<Range<D>>

+    where

+        D: TextDimension + Ord + Sub<D, Output = D>,

+    {

+        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::<ExcerptSummary>(());

+

+        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::<D>(*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::<D>(buffer);

+                        let excerpt_buffer_end = excerpt.range.context.end.summary::<D>(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_text_summary(&cursor.start().text);

+

+                            let mut start = excerpt_start;

+                            start.add_assign(&(range.start - excerpt_buffer_start));

+                            let mut end = excerpt_start;

+                            end.add_assign(&(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<Self>,

+    ) {

+        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>) {

+        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<Self>)

+    where

+        T: IntoIterator<Item = (&'a Entity<Buffer>, &'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<Self>,

+    ) {

+        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<Self>) -> Option<TransactionId> {

+        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<Self>) -> Option<TransactionId> {

+        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<Self>) {

+        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<Self>) {

+        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);

+                    });

+                }

+            }

+        }

+    }

+}

crates/nc/Cargo.toml 🔗

@@ -17,4 +17,3 @@ anyhow.workspace = true
 futures.workspace = true
 net.workspace = true
 smol.workspace = true
-workspace-hack.workspace = true

crates/net/Cargo.toml 🔗

@@ -14,7 +14,6 @@ doctest = false
 
 [dependencies]
 smol.workspace = true
-workspace-hack.workspace = true
 
 [target.'cfg(target_os = "windows")'.dependencies]
 anyhow.workspace = true

crates/node_runtime/Cargo.toml 🔗

@@ -31,7 +31,6 @@ smol.workspace = true
 util.workspace = true
 watch.workspace = true
 which.workspace = true
-workspace-hack.workspace = true
 
 [target.'cfg(windows)'.dependencies]
 async-std = { version = "1.12.0", features = ["unstable"] }

crates/notifications/Cargo.toml 🔗

@@ -33,7 +33,6 @@ time.workspace = true
 ui.workspace = true
 util.workspace = true
 workspace.workspace = true
-workspace-hack.workspace = true
 zed_actions.workspace = true
 
 [dev-dependencies]

crates/notifications/src/notification_store.rs 🔗

@@ -123,14 +123,16 @@ impl NotificationStore {
             return None;
         }
         let ix = count - 1 - ix;
-        let mut cursor = self.notifications.cursor::<Count>(());
-        cursor.seek(&Count(ix), Bias::Right);
-        cursor.item()
+        let (.., item) = self
+            .notifications
+            .find::<Count, _>((), &Count(ix), Bias::Right);
+        item
     }
     pub fn notification_for_id(&self, id: u64) -> Option<&NotificationEntry> {
-        let mut cursor = self.notifications.cursor::<NotificationId>(());
-        cursor.seek(&NotificationId(id), Bias::Left);
-        if let Some(item) = cursor.item()
+        let (.., item) =
+            self.notifications
+                .find::<NotificationId, _>((), &NotificationId(id), Bias::Left);
+        if let Some(item) = item
             && item.id == id
         {
             return Some(item);

crates/ollama/Cargo.toml 🔗

@@ -23,4 +23,3 @@ schemars = { workspace = true, optional = true }
 serde.workspace = true
 serde_json.workspace = true
 settings.workspace = true
-workspace-hack.workspace = true

crates/onboarding/Cargo.toml 🔗

@@ -34,10 +34,8 @@ settings.workspace = true
 telemetry.workspace = true
 theme.workspace = true
 ui.workspace = true
-ui_input.workspace = true
 util.workspace = true
 vim_mode_setting.workspace = true
-workspace-hack.workspace = true
 workspace.workspace = true
 zed_actions.workspace = true
 zlog.workspace = true

crates/onboarding/src/onboarding.rs 🔗

@@ -17,7 +17,6 @@ use ui::{
     Divider, KeyBinding, ParentElement as _, StatefulInteractiveElement, Vector, VectorName,
     WithScrollbar as _, prelude::*, rems_from_px,
 };
-pub use ui_input::font_picker;
 use workspace::{
     AppState, Workspace, WorkspaceId,
     dock::DockPosition,
@@ -338,10 +337,9 @@ impl Render for Onboarding {
                                                 KeyBinding::for_action_in(
                                                     &Finish,
                                                     &self.focus_handle,
-                                                    window,
                                                     cx,
                                                 )
-                                                .map(|kb| kb.size(rems_from_px(12.))),
+                                                .size(rems_from_px(12.)),
                                             )
                                             .on_click(|_, window, cx| {
                                                 window.dispatch_action(Finish.boxed_clone(), cx);
@@ -385,14 +383,14 @@ impl Item for Onboarding {
         _workspace_id: Option<WorkspaceId>,
         _: &mut Window,
         cx: &mut Context<Self>,
-    ) -> Option<Entity<Self>> {
-        Some(cx.new(|cx| Onboarding {
+    ) -> Task<Option<Entity<Self>>> {
+        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::<SettingsStore>(move |_, cx| cx.notify()),
-        }))
+        })))
     }
 
     fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) {

crates/onboarding/src/welcome.rs 🔗

@@ -78,13 +78,7 @@ struct Section<const COLS: usize> {
 }
 
 impl<const COLS: usize> Section<COLS> {
-    fn render(
-        self,
-        index_offset: usize,
-        focus: &FocusHandle,
-        window: &mut Window,
-        cx: &mut App,
-    ) -> impl IntoElement {
+    fn render(self, index_offset: usize, focus: &FocusHandle, cx: &mut App) -> impl IntoElement {
         v_flex()
             .min_w_full()
             .child(
@@ -104,7 +98,7 @@ impl<const COLS: usize> Section<COLS> {
                 self.entries
                     .iter()
                     .enumerate()
-                    .map(|(index, entry)| entry.render(index_offset + index, focus, window, cx)),
+                    .map(|(index, entry)| entry.render(index_offset + index, focus, cx)),
             )
     }
 }
@@ -116,13 +110,7 @@ struct SectionEntry {
 }
 
 impl SectionEntry {
-    fn render(
-        &self,
-        button_index: usize,
-        focus: &FocusHandle,
-        window: &Window,
-        cx: &App,
-    ) -> impl IntoElement {
+    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()
@@ -141,9 +129,8 @@ impl SectionEntry {
                             )
                             .child(Label::new(self.title)),
                     )
-                    .children(
-                        KeyBinding::for_action_in(self.action, focus, window, cx)
-                            .map(|s| s.size(rems_from_px(12.))),
+                    .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))
@@ -151,7 +138,6 @@ impl SectionEntry {
 }
 
 pub struct WelcomePage {
-    first_paint: bool,
     focus_handle: FocusHandle,
 }
 
@@ -168,11 +154,7 @@ impl WelcomePage {
 }
 
 impl Render for WelcomePage {
-    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
-        if self.first_paint {
-            window.request_animation_frame();
-            self.first_paint = false;
-        }
+    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> 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();
@@ -220,13 +202,11 @@ impl Render for WelcomePage {
                                     .child(first_section.render(
                                         Default::default(),
                                         &self.focus_handle,
-                                        window,
                                         cx,
                                     ))
                                     .child(second_section.render(
                                         first_section_entries,
                                         &self.focus_handle,
-                                        window,
                                         cx,
                                     ))
                                     .child(
@@ -316,10 +296,7 @@ impl WelcomePage {
             cx.on_focus(&focus_handle, window, |_, _, cx| cx.notify())
                 .detach();
 
-            WelcomePage {
-                first_paint: true,
-                focus_handle,
-            }
+            WelcomePage { focus_handle }
         })
     }
 }

crates/open_ai/Cargo.toml 🔗

@@ -25,4 +25,3 @@ serde.workspace = true
 serde_json.workspace = true
 settings.workspace = true
 strum.workspace = true
-workspace-hack.workspace = true

crates/open_router/Cargo.toml 🔗

@@ -25,4 +25,3 @@ serde_json.workspace = true
 settings.workspace = true
 strum.workspace = true
 thiserror.workspace = true
-workspace-hack.workspace = true

crates/outline/Cargo.toml 🔗

@@ -26,7 +26,6 @@ ui.workspace = true
 util.workspace = true
 workspace.workspace = true
 zed_actions.workspace = true
-workspace-hack.workspace = true
 
 [dev-dependencies]
 editor = { workspace = true, features = ["test-support"] }

crates/outline/src/outline.rs 🔗

@@ -245,7 +245,10 @@ impl PickerDelegate for OutlineViewDelegate {
 
             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::<usize>(cx).head();
+                let cursor_offset = editor
+                    .selections
+                    .newest::<usize>(&editor.display_snapshot(cx))
+                    .head();
                 (buffer, cursor_offset)
             });
             selected_index = self
@@ -673,7 +676,7 @@ mod tests {
         let selections = editor.update(cx, |editor, cx| {
             editor
                 .selections
-                .all::<rope::Point>(cx)
+                .all::<rope::Point>(&editor.display_snapshot(cx))
                 .into_iter()
                 .map(|s| s.start..s.end)
                 .collect::<Vec<_>>()

crates/outline_panel/Cargo.toml 🔗

@@ -38,7 +38,6 @@ util.workspace = true
 workspace.workspace = true
 worktree.workspace = true
 zed_actions.workspace = true
-workspace-hack.workspace = true
 
 [dev-dependencies]
 search = { workspace = true, features = ["test-support"] }

crates/outline_panel/src/outline_panel.rs 🔗

@@ -3099,7 +3099,10 @@ impl OutlinePanel {
         cx: &mut Context<Self>,
     ) -> Option<PanelEntry> {
         let selection = editor.update(cx, |editor, cx| {
-            editor.selections.newest::<language::Point>(cx).head()
+            editor
+                .selections
+                .newest::<language::Point>(&editor.display_snapshot(cx))
+                .head()
         });
         let editor_snapshot = editor.update(cx, |editor, cx| editor.snapshot(window, cx));
         let multi_buffer = editor.read(cx).buffer();
@@ -4835,6 +4838,10 @@ impl Panel for OutlinePanel {
         "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,
@@ -6957,13 +6964,13 @@ outline: struct OutlineEntryExcerpt
 
     fn selected_row_text(editor: &Entity<Editor>, cx: &mut App) -> String {
         editor.update(cx, |editor, cx| {
-                let selections = editor.selections.all::<language::Point>(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::<String>().trim().to_owned()
+            let selections = editor.selections.all::<language::Point>(&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::<String>().trim().to_owned()
         })
     }
 

crates/outline_panel/src/outline_panel_settings.rs 🔗

@@ -62,20 +62,4 @@ impl Settings for OutlinePanelSettings {
             expand_outlines_with_depth: panel.expand_outlines_with_depth.unwrap(),
         }
     }
-
-    fn import_from_vscode(
-        vscode: &settings::VsCodeSettings,
-        current: &mut settings::SettingsContent,
-    ) {
-        if let Some(b) = vscode.read_bool("outline.icons") {
-            let outline_panel = current.outline_panel.get_or_insert_default();
-            outline_panel.file_icons = Some(b);
-            outline_panel.folder_icons = Some(b);
-        }
-
-        if let Some(b) = vscode.read_bool("git.decorations.enabled") {
-            let outline_panel = current.outline_panel.get_or_insert_default();
-            outline_panel.git_status = Some(b);
-        }
-    }
 }

crates/panel/Cargo.toml 🔗

@@ -18,4 +18,3 @@ settings.workspace = true
 theme.workspace = true
 ui.workspace = true
 workspace.workspace = true
-workspace-hack.workspace = true

crates/paths/Cargo.toml 🔗

@@ -18,4 +18,3 @@ path = "src/paths.rs"
 dirs.workspace = true
 ignore.workspace = true
 util.workspace = true
-workspace-hack.workspace = true

crates/paths/src/paths.rs 🔗

@@ -288,7 +288,7 @@ pub fn snippets_dir() -> &'static PathBuf {
 /// Returns the path to the contexts directory.
 ///
 /// This is where the saved contexts from the Assistant are stored.
-pub fn contexts_dir() -> &'static PathBuf {
+pub fn text_threads_dir() -> &'static PathBuf {
     static CONTEXTS_DIR: OnceLock<PathBuf> = OnceLock::new();
     CONTEXTS_DIR.get_or_init(|| {
         if cfg!(target_os = "macos") {

crates/picker/Cargo.toml 🔗

@@ -25,7 +25,6 @@ serde.workspace = true
 theme.workspace = true
 ui.workspace = true
 workspace.workspace = true
-workspace-hack.workspace = true
 
 [dev-dependencies]
 ctor.workspace = true

crates/picker/src/picker.rs 🔗

@@ -352,6 +352,16 @@ impl<D: PickerDelegate> Picker<D> {
         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);
     }

crates/prettier/Cargo.toml 🔗

@@ -29,7 +29,6 @@ paths.workspace = true
 serde.workspace = true
 serde_json.workspace = true
 util.workspace = true
-workspace-hack.workspace = true
 
 [dev-dependencies]
 fs = { workspace = true,  features = ["test-support"] }

crates/project/Cargo.toml 🔗

@@ -91,7 +91,6 @@ which.workspace = true
 worktree.workspace = true
 zeroize.workspace = true
 zlog.workspace = true
-workspace-hack.workspace = true
 
 [dev-dependencies]
 client = { workspace = true, features = ["test-support"] }

crates/project/src/buffer_store.rs 🔗

@@ -25,8 +25,8 @@ use rpc::{
 };
 use smol::channel::Receiver;
 use std::{io, pin::pin, sync::Arc, time::Instant};
-use text::BufferId;
-use util::{ResultExt as _, TryFutureExt, debug_panic, maybe, rel_path::RelPath};
+use text::{BufferId, ReplicaId};
+use util::{ResultExt as _, TryFutureExt, debug_panic, maybe, paths::PathStyle, rel_path::RelPath};
 use worktree::{File, PathChange, ProjectEntryId, Worktree, WorktreeId};
 
 /// A set of open buffers.
@@ -158,7 +158,7 @@ impl RemoteBufferStore {
     pub fn handle_create_buffer_for_peer(
         &mut self,
         envelope: TypedEnvelope<proto::CreateBufferForPeer>,
-        replica_id: u16,
+        replica_id: ReplicaId,
         capability: Capability,
         cx: &mut Context<BufferStore>,
     ) -> Result<Option<Entity<Buffer>>> {
@@ -623,10 +623,15 @@ impl LocalBufferStore {
             let load_file = worktree.load_file(path.as_ref(), cx);
             let reservation = cx.reserve_entity();
             let buffer_id = BufferId::from(reservation.entity_id().as_non_zero_u64());
+            let path = path.clone();
             cx.spawn(async move |_, cx| {
-                let loaded = load_file.await?;
+                let loaded = load_file.await.with_context(|| {
+                    format!("Could not open path: {}", path.display(PathStyle::local()))
+                })?;
                 let text_buffer = cx
-                    .background_spawn(async move { text::Buffer::new(0, buffer_id, loaded.text) })
+                    .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)
@@ -639,7 +644,7 @@ impl LocalBufferStore {
                 Ok(buffer) => Ok(buffer),
                 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(0, buffer_id, "");
+                    let text_buffer = text::Buffer::new(ReplicaId::LOCAL, buffer_id, "");
                     Buffer::build(
                         text_buffer,
                         Some(Arc::new(File {
@@ -917,7 +922,7 @@ impl BufferStore {
             path: file.path.clone(),
             worktree_id: file.worktree_id(cx),
         });
-        let is_remote = buffer.replica_id() != 0;
+        let is_remote = buffer.replica_id().is_remote();
         let open_buffer = OpenBuffer::Complete {
             buffer: buffer_entity.downgrade(),
         };
@@ -1317,7 +1322,7 @@ impl BufferStore {
     pub fn handle_create_buffer_for_peer(
         &mut self,
         envelope: TypedEnvelope<proto::CreateBufferForPeer>,
-        replica_id: u16,
+        replica_id: ReplicaId,
         capability: Capability,
         cx: &mut Context<Self>,
     ) -> Result<()> {

crates/project/src/debugger/dap_store.rs 🔗

@@ -264,13 +264,21 @@ impl DapStore {
                     DapBinary::Custom(binary) => Some(PathBuf::from(binary)),
                 });
                 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 cwd: Arc<Path> = worktree.read(cx).abs_path().as_ref().into();
 
                 cx.spawn(async move |this, cx| {
                     let mut binary = adapter
-                        .get_binary(&delegate, &definition, user_installed_path, user_args, cx)
+                        .get_binary(
+                            &delegate,
+                            &definition,
+                            user_installed_path,
+                            user_args,
+                            user_env,
+                            cx,
+                        )
                         .await?;
 
                     let env = this

crates/project/src/direnv.rs 🔗

@@ -1,82 +0,0 @@
-use crate::environment::EnvironmentErrorMessage;
-use std::process::ExitStatus;
-
-use {collections::HashMap, std::path::Path, util::ResultExt};
-
-#[derive(Clone)]
-pub enum DirenvError {
-    NotFound,
-    FailedRun,
-    NonZeroExit(ExitStatus, Vec<u8>),
-    InvalidJson,
-}
-
-impl From<DirenvError> for Option<EnvironmentErrorMessage> {
-    fn from(value: DirenvError) -> Self {
-        match value {
-            DirenvError::NotFound => None,
-            DirenvError::FailedRun | DirenvError::NonZeroExit(_, _) => {
-                Some(EnvironmentErrorMessage(String::from(
-                    "Failed to run direnv. See logs for more info",
-                )))
-            }
-            DirenvError::InvalidJson => Some(EnvironmentErrorMessage(String::from(
-                "Direnv returned invalid json. See logs for more info",
-            ))),
-        }
-    }
-}
-
-pub async fn load_direnv_environment(
-    env: &HashMap<String, String>,
-    dir: &Path,
-) -> Result<HashMap<String, Option<String>>, DirenvError> {
-    let Ok(direnv_path) = which::which("direnv") else {
-        return Err(DirenvError::NotFound);
-    };
-
-    let args = &["export", "json"];
-    let Some(direnv_output) = smol::process::Command::new(&direnv_path)
-        .args(args)
-        .envs(env)
-        .env("TERM", "dumb")
-        .current_dir(dir)
-        .output()
-        .await
-        .log_err()
-    else {
-        return Err(DirenvError::FailedRun);
-    };
-
-    if !direnv_output.status.success() {
-        log::error!(
-            "Loading direnv environment failed ({}), stderr: {}",
-            direnv_output.status,
-            String::from_utf8_lossy(&direnv_output.stderr)
-        );
-        return Err(DirenvError::NonZeroExit(
-            direnv_output.status,
-            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());
-    }
-
-    match serde_json::from_str(&output) {
-        Ok(env) => Ok(env),
-        Err(err) => {
-            log::error!(
-                "json parse error {}, while parsing output of `{} {}`:\n{}",
-                err,
-                direnv_path.display(),
-                args.join(" "),
-                output
-            );
-            Err(DirenvError::InvalidJson)
-        }
-    }
-}

crates/project/src/environment.rs 🔗

@@ -1,4 +1,5 @@
-use futures::{FutureExt, future::Shared};
+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};
@@ -20,7 +21,9 @@ pub struct ProjectEnvironment {
     cli_environment: Option<HashMap<String, String>>,
     local_environments: HashMap<(Shell, Arc<Path>), Shared<Task<Option<HashMap<String, String>>>>>,
     remote_environments: HashMap<(Shell, Arc<Path>), Shared<Task<Option<HashMap<String, String>>>>>,
-    environment_error_messages: VecDeque<EnvironmentErrorMessage>,
+    environment_error_messages: VecDeque<String>,
+    environment_error_messages_tx: mpsc::UnboundedSender<String>,
+    _tasks: Vec<Task<()>>,
 }
 
 pub enum ProjectEnvironmentEvent {
@@ -30,12 +33,24 @@ pub enum ProjectEnvironmentEvent {
 impl EventEmitter<ProjectEnvironmentEvent> for ProjectEnvironment {}
 
 impl ProjectEnvironment {
-    pub fn new(cli_environment: Option<HashMap<String, String>>) -> Self {
+    pub fn new(cli_environment: Option<HashMap<String, String>>, cx: &mut Context<Self>) -> 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,
+            _tasks: vec![task],
         }
     }
 
@@ -128,7 +143,37 @@ impl ProjectEnvironment {
         self.local_environments
             .entry((shell.clone(), abs_path.clone()))
             .or_insert_with(|| {
-                get_local_directory_environment_impl(shell, abs_path.clone(), cx).shared()
+                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()
     }
@@ -165,11 +210,11 @@ impl ProjectEnvironment {
             .clone()
     }
 
-    pub fn peek_environment_error(&self) -> Option<&EnvironmentErrorMessage> {
+    pub fn peek_environment_error(&self) -> Option<&String> {
         self.environment_error_messages.front()
     }
 
-    pub fn pop_environment_error(&mut self) -> Option<EnvironmentErrorMessage> {
+    pub fn pop_environment_error(&mut self) -> Option<String> {
         self.environment_error_messages.pop_front()
     }
 }
@@ -194,120 +239,72 @@ impl From<EnvironmentOrigin> for String {
     }
 }
 
-#[derive(Debug)]
-pub struct EnvironmentErrorMessage(pub String);
-
-impl std::fmt::Display for EnvironmentErrorMessage {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        write!(f, "{}", self.0)
-    }
-}
-
-impl EnvironmentErrorMessage {
-    #[allow(dead_code)]
-    fn from_str(s: &str) -> Self {
-        Self(String::from(s))
-    }
-}
-
 async fn load_directory_shell_environment(
-    shell: &Shell,
-    abs_path: &Path,
-    load_direnv: &DirenvSettings,
-) -> (
-    Option<HashMap<String, String>>,
-    Option<EnvironmentErrorMessage>,
-) {
-    match smol::fs::metadata(abs_path).await {
-        Ok(meta) => {
-            let dir = if meta.is_dir() {
-                abs_path
-            } else if let Some(parent) = abs_path.parent() {
-                parent
-            } else {
-                return (
-                    None,
-                    Some(EnvironmentErrorMessage(format!(
-                        "Failed to load shell environment in {}: not a directory",
-                        abs_path.display()
-                    ))),
-                );
-            };
-
-            load_shell_environment(shell, dir, load_direnv).await
-        }
-        Err(err) => (
-            None,
-            Some(EnvironmentErrorMessage(format!(
-                "Failed to load shell environment in {}: {}",
-                abs_path.display(),
-                err
-            ))),
-        ),
-    }
-}
-
-async fn load_shell_environment(
-    shell: &Shell,
-    dir: &Path,
-    load_direnv: &DirenvSettings,
-) -> (
-    Option<HashMap<String, String>>,
-    Option<EnvironmentErrorMessage>,
-) {
-    use crate::direnv::load_direnv_environment;
-    use util::shell_env;
-
-    if cfg!(any(test, feature = "test-support")) {
-        let fake_env = [("ZED_FAKE_TEST_ENV".into(), "true".into())]
-            .into_iter()
-            .collect();
-        (Some(fake_env), None)
-    } else if cfg!(target_os = "windows") {
-        let (shell, args) = shell.program_and_args();
-        let envs = match shell_env::capture(shell, args, dir).await {
-            Ok(envs) => envs,
-            Err(err) => {
-                util::log_err(&err);
-                return (
-                    None,
-                    Some(EnvironmentErrorMessage(format!(
-                        "Failed to load environment variables: {}",
-                        err
-                    ))),
-                );
-            }
-        };
-
+    shell: Shell,
+    abs_path: Arc<Path>,
+    load_direnv: DirenvSettings,
+    tx: mpsc::UnboundedSender<String>,
+) -> anyhow::Result<HashMap<String, String>> {
+    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()
+    };
+
+    if cfg!(target_os = "windows") {
         // Note: direnv is not available on Windows, so we skip direnv processing
         // and just return the shell environment
-        (Some(envs), None)
+        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 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);
+        }
+        Ok(envs)
     } else {
-        let dir_ = dir.to_owned();
         let (shell, args) = shell.program_and_args();
-        let mut envs = match shell_env::capture(shell, args, &dir_).await {
-            Ok(envs) => envs,
-            Err(err) => {
-                util::log_err(&err);
-                return (
-                    None,
-                    Some(EnvironmentErrorMessage::from_str(
-                        "Failed to load environment variables. See log for details",
-                    )),
-                );
-            }
-        };
+        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 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, direnv_error) = match load_direnv {
-            DirenvSettings::ShellHook => (None, None),
-            DirenvSettings::Direct => match load_direnv_environment(&envs, dir).await {
-                Ok(env) => (Some(env), None),
-                Err(err) => (None, err.into()),
-            },
+        let direnv_environment = match load_direnv {
+            DirenvSettings::ShellHook => 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 {
@@ -319,51 +316,41 @@ async fn load_shell_environment(
             }
         }
 
-        (Some(envs), direnv_error)
+        Ok(envs)
     }
 }
 
-fn get_local_directory_environment_impl(
-    shell: &Shell,
-    abs_path: Arc<Path>,
-    cx: &Context<ProjectEnvironment>,
-) -> Task<Option<HashMap<String, String>>> {
-    let load_direnv = ProjectSettings::get_global(cx).load_direnv.clone();
-
-    let shell = shell.clone();
-    cx.spawn(async move |this, cx| {
-        let (mut shell_env, error_message) = cx
-            .background_spawn({
-                let abs_path = abs_path.clone();
-                async move {
-                    load_directory_shell_environment(&shell, &abs_path, &load_direnv).await
-                }
-            })
-            .await;
-
-        if let Some(shell_env) = shell_env.as_mut() {
-            let path = shell_env
-                .get("PATH")
-                .map(|path| path.as_str())
-                .unwrap_or_default();
-            log::info!(
-                "using project environment variables shell launched in {:?}. PATH={:?}",
-                abs_path,
-                path
-            );
-
-            set_origin_marker(shell_env, EnvironmentOrigin::WorktreeShell);
-        }
+async fn load_direnv_environment(
+    env: &HashMap<String, String>,
+    dir: &Path,
+) -> anyhow::Result<HashMap<String, Option<String>>> {
+    let Some(direnv_path) = which::which("direnv").ok() else {
+        return Ok(HashMap::default());
+    };
+
+    let args = &["export", "json"];
+    let direnv_output = smol::process::Command::new(&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)
+        );
+    }
 
-        if let Some(error) = error_message {
-            this.update(cx, |this, cx| {
-                log::error!("{error}");
-                this.environment_error_messages.push_back(error);
-                cx.emit(ProjectEnvironmentEvent::ErrorsUpdated)
-            })
-            .log_err();
-        }
+    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());
+    }
 
-        shell_env
-    })
+    serde_json::from_str(&output).context("parsing direnv json")
 }

crates/project/src/git_store.rs 🔗

@@ -301,9 +301,13 @@ pub enum RepositoryState {
 
 #[derive(Clone, Debug, PartialEq, Eq)]
 pub enum RepositoryEvent {
-    Updated { full_scan: bool, new_instance: bool },
+    StatusesChanged {
+        // TODO could report which statuses changed here
+        full_scan: bool,
+    },
     MergeHeadsChanged,
-    PathsChanged,
+    BranchChanged,
+    StashEntriesChanged,
 }
 
 #[derive(Clone, Debug)]
@@ -313,7 +317,7 @@ pub struct JobsUpdated;
 pub enum GitStoreEvent {
     ActiveRepositoryChanged(Option<RepositoryId>),
     RepositoryUpdated(RepositoryId, RepositoryEvent, bool),
-    RepositoryAdded(RepositoryId),
+    RepositoryAdded,
     RepositoryRemoved(RepositoryId),
     IndexWriteError(anyhow::Error),
     JobsUpdated,
@@ -1218,7 +1222,7 @@ impl GitStore {
                 self._subscriptions
                     .push(cx.subscribe(&repo, Self::on_jobs_updated));
                 self.repositories.insert(id, repo);
-                cx.emit(GitStoreEvent::RepositoryAdded(id));
+                cx.emit(GitStoreEvent::RepositoryAdded);
                 self.active_repo_id.get_or_insert_with(|| {
                     cx.emit(GitStoreEvent::ActiveRepositoryChanged(Some(id)));
                     id
@@ -1485,11 +1489,10 @@ impl GitStore {
             let id = RepositoryId::from_proto(update.id);
             let client = this.upstream_client().context("no upstream client")?;
 
-            let mut is_new = false;
+            let mut repo_subscription = None;
             let repo = this.repositories.entry(id).or_insert_with(|| {
-                is_new = true;
                 let git_store = cx.weak_entity();
-                cx.new(|cx| {
+                let repo = cx.new(|cx| {
                     Repository::remote(
                         id,
                         Path::new(&update.abs_path).into(),
@@ -1499,16 +1502,16 @@ impl GitStore {
                         git_store,
                         cx,
                     )
-                })
+                });
+                repo_subscription = Some(cx.subscribe(&repo, Self::on_repository_event));
+                cx.emit(GitStoreEvent::RepositoryAdded);
+                repo
             });
-            if is_new {
-                this._subscriptions
-                    .push(cx.subscribe(repo, Self::on_repository_event))
-            }
+            this._subscriptions.extend(repo_subscription);
 
             repo.update(cx, {
                 let update = update.clone();
-                |repo, cx| repo.apply_remote_update(update, is_new, cx)
+                |repo, cx| repo.apply_remote_update(update, cx)
             })?;
 
             this.active_repo_id.get_or_insert_with(|| {
@@ -2910,6 +2913,13 @@ impl RepositorySnapshot {
         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,
@@ -3349,10 +3359,7 @@ impl Repository {
     pub fn repo_path_to_project_path(&self, path: &RepoPath, cx: &App) -> Option<ProjectPath> {
         let git_store = self.git_store.upgrade()?;
         let worktree_store = git_store.read(cx).worktree_store.read(cx);
-        let abs_path = self
-            .snapshot
-            .work_directory_abs_path
-            .join(path.as_std_path());
+        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 {
@@ -3873,18 +3880,15 @@ impl Repository {
                     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;
-                            let snapshot = this.snapshot.clone();
-                            cx.emit(RepositoryEvent::Updated {
-                                full_scan: false,
-                                new_instance: false,
-                            });
-                            snapshot
+                            cx.emit(RepositoryEvent::StashEntriesChanged);
+                            this.snapshot.clone()
                         })?;
                         if let Some(updates_tx) = updates_tx {
                             updates_tx
@@ -4026,7 +4030,7 @@ impl Repository {
 
         let this = cx.weak_entity();
         self.send_job(
-            Some(format!("git push {} {} {}", args, branch, remote).into()),
+            Some(format!("git push {} {} {}", args, remote, branch).into()),
             move |git_repo, mut cx| async move {
                 match git_repo {
                     RepositoryState::Local {
@@ -4044,18 +4048,15 @@ impl Repository {
                                 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;
-                                let snapshot = this.snapshot.clone();
-                                cx.emit(RepositoryEvent::Updated {
-                                    full_scan: false,
-                                    new_instance: false,
-                                });
-                                snapshot
+                                cx.emit(RepositoryEvent::BranchChanged);
+                                this.snapshot.clone()
                             })?;
                             if let Some(updates_tx) = updates_tx {
                                 updates_tx
@@ -4454,7 +4455,6 @@ impl Repository {
     pub(crate) fn apply_remote_update(
         &mut self,
         update: proto::UpdateRepository,
-        is_new: bool,
         cx: &mut Context<Self>,
     ) -> Result<()> {
         let conflicted_paths = TreeSet::from_ordered_entries(
@@ -4463,21 +4463,30 @@ impl Repository {
                 .into_iter()
                 .filter_map(|path| RepoPath::from_proto(&path).log_err()),
         );
-        self.snapshot.branch = update.branch_summary.as_ref().map(proto_to_branch);
-        self.snapshot.head_commit = update
+        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);
-        self.snapshot.stash_entries = GitStash {
+        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;
 
         let edits = update
             .removed_statuses
@@ -4496,14 +4505,13 @@ impl Repository {
                     }),
             )
             .collect::<Vec<_>>();
+        if !edits.is_empty() {
+            cx.emit(RepositoryEvent::StatusesChanged { full_scan: true });
+        }
         self.snapshot.statuses_by_path.edit(edits, ());
         if update.is_last_update {
             self.snapshot.scan_id = update.scan_id;
         }
-        cx.emit(RepositoryEvent::Updated {
-            full_scan: true,
-            new_instance: is_new,
-        });
         Ok(())
     }
 
@@ -4826,23 +4834,19 @@ impl Repository {
                     .await;
 
                 this.update(&mut cx, |this, cx| {
-                    let needs_update = !changed_path_statuses.is_empty()
-                        || this.snapshot.stash_entries != stash_entries;
-                    this.snapshot.stash_entries = stash_entries;
+                    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 { full_scan: false });
                         this.snapshot
                             .statuses_by_path
                             .edit(changed_path_statuses, ());
                         this.snapshot.scan_id += 1;
                     }
 
-                    if needs_update {
-                        cx.emit(RepositoryEvent::Updated {
-                            full_scan: false,
-                            new_instance: false,
-                        });
-                    }
-
                     if let Some(updates_tx) = updates_tx {
                         updates_tx
                             .unbounded_send(DownstreamUpdate::UpdateRepository(
@@ -4850,7 +4854,6 @@ impl Repository {
                             ))
                             .ok();
                     }
-                    cx.emit(RepositoryEvent::PathsChanged);
                 })
             },
         );
@@ -5113,28 +5116,24 @@ async fn compute_snapshot(
         MergeDetails::load(&backend, &statuses_by_path, &prev_snapshot).await?;
     log::debug!("new merge details (changed={merge_heads_changed:?}): {merge_details:?}");
 
-    if merge_heads_changed
-        || branch != prev_snapshot.branch
-        || statuses_by_path != prev_snapshot.statuses_by_path
-    {
-        events.push(RepositoryEvent::Updated {
-            full_scan: true,
-            new_instance: false,
-        });
-    }
-
-    // Cache merge conflict paths so they don't change from staging/unstaging,
-    // until the merge heads change (at commit time, etc.).
     if merge_heads_changed {
         events.push(RepositoryEvent::MergeHeadsChanged);
     }
 
+    if statuses_by_path != prev_snapshot.statuses_by_path {
+        events.push(RepositoryEvent::StatusesChanged { full_scan: true })
+    }
+
     // 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);
+    }
+
     // Used by edit prediction data collection
     let remote_origin_url = backend.remote_url("origin");
     let remote_upstream_url = backend.remote_url("upstream");

crates/project/src/git_store/conflict_set.rs 🔗

@@ -72,13 +72,15 @@ impl ConflictSetSnapshot {
             (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)),
+            (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)),
+            (Some(first), Some(second)) => Some(*first.range.end.max(&second.range.end, buffer)),
         };
         ConflictSetUpdate {
             buffer_range: start.zip(end).map(|(start, end)| start..end),
@@ -269,7 +271,7 @@ mod tests {
     use language::language_settings::AllLanguageSettings;
     use serde_json::json;
     use settings::Settings as _;
-    use text::{Buffer, BufferId, Point, ToOffset as _};
+    use text::{Buffer, BufferId, Point, ReplicaId, ToOffset as _};
     use unindent::Unindent as _;
     use util::{path, rel_path::rel_path};
     use worktree::WorktreeSettings;
@@ -297,7 +299,7 @@ mod tests {
         .unindent();
 
         let buffer_id = BufferId::new(1).unwrap();
-        let buffer = Buffer::new(0, buffer_id, test_content);
+        let buffer = Buffer::new(ReplicaId::LOCAL, buffer_id, test_content);
         let snapshot = buffer.snapshot();
 
         let conflict_snapshot = ConflictSet::parse(&snapshot);
@@ -372,7 +374,7 @@ mod tests {
         .unindent();
 
         let buffer_id = BufferId::new(1).unwrap();
-        let buffer = Buffer::new(0, buffer_id, test_content);
+        let buffer = Buffer::new(ReplicaId::LOCAL, buffer_id, test_content);
         let snapshot = buffer.snapshot();
 
         let conflict_snapshot = ConflictSet::parse(&snapshot);
@@ -403,7 +405,7 @@ mod tests {
             >>>>>>> "#
             .unindent();
         let buffer_id = BufferId::new(1).unwrap();
-        let buffer = Buffer::new(0, buffer_id, test_content);
+        let buffer = Buffer::new(ReplicaId::LOCAL, buffer_id, test_content);
         let snapshot = buffer.snapshot();
 
         let conflict_snapshot = ConflictSet::parse(&snapshot);
@@ -445,7 +447,7 @@ mod tests {
         .unindent();
 
         let buffer_id = BufferId::new(1).unwrap();
-        let buffer = Buffer::new(0, buffer_id, test_content.clone());
+        let buffer = Buffer::new(ReplicaId::LOCAL, buffer_id, test_content.clone());
         let snapshot = buffer.snapshot();
 
         let conflict_snapshot = ConflictSet::parse(&snapshot);

crates/project/src/image_store.rs 🔗

@@ -687,6 +687,7 @@ fn create_gpui_image(content: Vec<u8>) -> anyhow::Result<Arc<gpui::Image>> {
             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,

crates/workspace/src/invalid_buffer_view.rs → crates/project/src/invalid_item_view.rs 🔗

@@ -11,7 +11,8 @@ use zed_actions::workspace::OpenWithSystem;
 use crate::Item;
 
 /// A view to display when a certain buffer fails to open.
-pub struct InvalidBufferView {
+#[derive(Debug)]
+pub struct InvalidItemView {
     /// Which path was attempted to open.
     pub abs_path: Arc<Path>,
     /// An error message, happened when opening the buffer.
@@ -20,7 +21,7 @@ pub struct InvalidBufferView {
     focus_handle: FocusHandle,
 }
 
-impl InvalidBufferView {
+impl InvalidItemView {
     pub fn new(
         abs_path: &Path,
         is_local: bool,
@@ -37,7 +38,7 @@ impl InvalidBufferView {
     }
 }
 
-impl Item for InvalidBufferView {
+impl Item for InvalidItemView {
     type Event = ();
 
     fn tab_content_text(&self, mut detail: usize, _: &App) -> SharedString {
@@ -66,15 +67,15 @@ impl Item for InvalidBufferView {
     }
 }
 
-impl EventEmitter<()> for InvalidBufferView {}
+impl EventEmitter<()> for InvalidItemView {}
 
-impl Focusable for InvalidBufferView {
+impl Focusable for InvalidItemView {
     fn focus_handle(&self, _: &App) -> FocusHandle {
         self.focus_handle.clone()
     }
 }
 
-impl Render for InvalidBufferView {
+impl Render for InvalidItemView {
     fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl gpui::IntoElement {
         let abs_path = self.abs_path.clone();
         v_flex()

crates/project/src/lsp_command.rs 🔗

@@ -234,7 +234,7 @@ pub(crate) struct OnTypeFormatting {
     pub push_to_history: bool,
 }
 
-#[derive(Debug)]
+#[derive(Clone, Debug)]
 pub(crate) struct InlayHints {
     pub range: Range<Anchor>,
 }
@@ -1834,13 +1834,20 @@ impl LspCommand for GetSignatureHelp {
         message: Option<lsp::SignatureHelp>,
         lsp_store: Entity<LspStore>,
         _: Entity<Buffer>,
-        _: LanguageServerId,
+        id: LanguageServerId,
         cx: AsyncApp,
     ) -> Result<Self::Response> {
         let Some(message) = message else {
             return Ok(None);
         };
-        cx.update(|cx| SignatureHelp::new(message, Some(lsp_store.read(cx).languages.clone()), cx))
+        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 {
@@ -1900,7 +1907,12 @@ impl LspCommand for GetSignatureHelp {
                 .signature_help
                 .map(proto_to_lsp_signature)
                 .and_then(|signature| {
-                    SignatureHelp::new(signature, Some(lsp_store.read(cx).languages.clone()), cx)
+                    SignatureHelp::new(
+                        signature,
+                        Some(lsp_store.read(cx).languages.clone()),
+                        None,
+                        cx,
+                    )
                 })
         })
     }

crates/project/src/lsp_command/signature_help.rs 🔗

@@ -2,8 +2,10 @@ 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 {
@@ -31,6 +33,7 @@ impl SignatureHelp {
     pub fn new(
         help: lsp::SignatureHelp,
         language_registry: Option<Arc<LanguageRegistry>>,
+        lang_server_id: Option<LanguageServerId>,
         cx: &mut App,
     ) -> Option<Self> {
         if help.signatures.is_empty() {
@@ -39,6 +42,7 @@ impl SignatureHelp {
         let active_signature = help.active_signature.unwrap_or(0) as usize;
         let mut signatures = Vec::<SignatureHelpData>::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))
@@ -49,39 +53,53 @@ impl SignatureHelp {
             if let Some(parameters) = &signature.parameters {
                 for (index, parameter) in parameters.iter().enumerate() {
                     let label_range = match &parameter.label {
-                        lsp::ParameterLabel::LabelOffsets(parameter_label_offsets) => {
-                            let range = *parameter_label_offsets.get(0)? as usize
-                                ..*parameter_label_offsets.get(1)? as usize;
-                            if index == active_parameter {
-                                highlights.push((
-                                    range.clone(),
-                                    HighlightStyle {
-                                        font_weight: Some(FontWeight::EXTRA_BOLD),
-                                        ..HighlightStyle::default()
-                                    },
-                                ));
-                            }
-                            Some(range)
+                        &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) {
-                                let range = start..start + parameter_label.len();
-                                if index == active_parameter {
-                                    highlights.push((
-                                        range.clone(),
-                                        HighlightStyle {
-                                            font_weight: Some(FontWeight::EXTRA_BOLD),
-                                            ..HighlightStyle::default()
-                                        },
-                                    ));
-                                }
-                                Some(range)
+                                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()
@@ -94,7 +112,6 @@ impl SignatureHelp {
                 }
             }
 
-            let label = SharedString::from(signature.label.clone());
             let documentation = signature
                 .documentation
                 .as_ref()
@@ -290,7 +307,7 @@ mod tests {
             active_signature: Some(0),
             active_parameter: Some(0),
         };
-        let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, cx));
+        let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, None, cx));
         assert!(maybe_markdown.is_some());
 
         let markdown = maybe_markdown.unwrap();
@@ -336,7 +353,7 @@ mod tests {
             active_signature: Some(0),
             active_parameter: Some(1),
         };
-        let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, cx));
+        let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, None, cx));
         assert!(maybe_markdown.is_some());
 
         let markdown = maybe_markdown.unwrap();
@@ -396,7 +413,7 @@ mod tests {
             active_signature: Some(0),
             active_parameter: Some(0),
         };
-        let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, cx));
+        let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, None, cx));
         assert!(maybe_markdown.is_some());
 
         let markdown = maybe_markdown.unwrap();
@@ -449,7 +466,7 @@ mod tests {
             active_signature: Some(1),
             active_parameter: Some(0),
         };
-        let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, cx));
+        let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, None, cx));
         assert!(maybe_markdown.is_some());
 
         let markdown = maybe_markdown.unwrap();
@@ -502,7 +519,7 @@ mod tests {
             active_signature: Some(1),
             active_parameter: Some(1),
         };
-        let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, cx));
+        let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, None, cx));
         assert!(maybe_markdown.is_some());
 
         let markdown = maybe_markdown.unwrap();
@@ -555,7 +572,7 @@ mod tests {
             active_signature: Some(1),
             active_parameter: None,
         };
-        let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, cx));
+        let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, None, cx));
         assert!(maybe_markdown.is_some());
 
         let markdown = maybe_markdown.unwrap();
@@ -623,7 +640,7 @@ mod tests {
             active_signature: Some(2),
             active_parameter: Some(1),
         };
-        let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, cx));
+        let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, None, cx));
         assert!(maybe_markdown.is_some());
 
         let markdown = maybe_markdown.unwrap();
@@ -645,7 +662,7 @@ mod tests {
             active_signature: None,
             active_parameter: None,
         };
-        let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, cx));
+        let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, None, cx));
         assert!(maybe_markdown.is_none());
     }
 
@@ -670,7 +687,7 @@ mod tests {
             active_signature: Some(0),
             active_parameter: Some(0),
         };
-        let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, cx));
+        let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, None, cx));
         assert!(maybe_markdown.is_some());
 
         let markdown = maybe_markdown.unwrap();
@@ -708,7 +725,8 @@ mod tests {
             active_signature: Some(0),
             active_parameter: Some(0),
         };
-        let maybe_signature_help = cx.update(|cx| SignatureHelp::new(signature_help, None, cx));
+        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();
@@ -736,4 +754,40 @@ mod tests {
         // 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())]
+            )
+        );
+    }
 }

crates/project/src/lsp_store.rs 🔗

@@ -14,17 +14,22 @@ 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, LocationLink, LspAction,
-    LspPullDiagnostics, ManifestProvidersStore, Project, ProjectItem, ProjectPath,
+    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,
+        inlay_hint_cache::BufferChunk,
         log_store::{GlobalLogStore, LanguageServerKind},
     },
     manifest_tree::{
@@ -56,7 +61,7 @@ use gpui::{
 use http_client::HttpClient;
 use itertools::Itertools as _;
 use language::{
-    Bias, BinaryStatus, Buffer, BufferSnapshot, CachedLspAdapter, CodeLabel, Diagnostic,
+    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,
@@ -84,7 +89,7 @@ use parking_lot::Mutex;
 use postage::{mpsc, sink::Sink, stream::Stream, watch};
 use rand::prelude::*;
 use rpc::{
-    AnyProtoClient,
+    AnyProtoClient, ErrorCode, ErrorExt as _,
     proto::{LspRequestId, LspRequestMessage as _},
 };
 use serde::Serialize;
@@ -105,11 +110,14 @@ use std::{
     path::{self, Path, PathBuf},
     pin::pin,
     rc::Rc,
-    sync::Arc,
+    sync::{
+        Arc,
+        atomic::{self, AtomicUsize},
+    },
     time::{Duration, Instant},
 };
 use sum_tree::Dimensions;
-use text::{Anchor, BufferId, LineEnding, OffsetRangeExt, ToPoint as _};
+use text::{Anchor, BufferId, LineEnding, OffsetRangeExt, Point, ToPoint as _};
 
 use util::{
     ConnectionResult, ResultExt as _, debug_panic, defer, maybe, merge_json_value_into,
@@ -120,6 +128,7 @@ use util::{
 
 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::{
@@ -564,8 +573,7 @@ impl LocalLspStore {
     }
 
     fn setup_lsp_messages(
-        this: WeakEntity<LspStore>,
-
+        lsp_store: WeakEntity<LspStore>,
         language_server: &LanguageServer,
         delegate: Arc<dyn LspAdapterDelegate>,
         adapter: Arc<CachedLspAdapter>,
@@ -575,7 +583,7 @@ impl LocalLspStore {
         language_server
             .on_notification::<lsp::notification::PublishDiagnostics, _>({
                 let adapter = adapter.clone();
-                let this = this.clone();
+                let this = lsp_store.clone();
                 move |mut params, cx| {
                     let adapter = adapter.clone();
                     if let Some(this) = this.upgrade() {
@@ -619,8 +627,7 @@ impl LocalLspStore {
             .on_request::<lsp::request::WorkspaceConfiguration, _, _>({
                 let adapter = adapter.adapter.clone();
                 let delegate = delegate.clone();
-                let this = this.clone();
-
+                let this = lsp_store.clone();
                 move |params, cx| {
                     let adapter = adapter.clone();
                     let delegate = delegate.clone();
@@ -665,7 +672,7 @@ impl LocalLspStore {
 
         language_server
             .on_request::<lsp::request::WorkspaceFoldersRequest, _, _>({
-                let this = this.clone();
+                let this = lsp_store.clone();
                 move |_, cx| {
                     let this = this.clone();
                     let cx = cx.clone();
@@ -693,7 +700,7 @@ impl LocalLspStore {
         // to these requests when initializing.
         language_server
             .on_request::<lsp::request::WorkDoneProgressCreate, _, _>({
-                let this = this.clone();
+                let this = lsp_store.clone();
                 move |params, cx| {
                     let this = this.clone();
                     let mut cx = cx.clone();
@@ -714,7 +721,7 @@ impl LocalLspStore {
 
         language_server
             .on_request::<lsp::request::RegisterCapability, _, _>({
-                let lsp_store = this.clone();
+                let lsp_store = lsp_store.clone();
                 move |params, cx| {
                     let lsp_store = lsp_store.clone();
                     let mut cx = cx.clone();
@@ -743,7 +750,7 @@ impl LocalLspStore {
 
         language_server
             .on_request::<lsp::request::UnregisterCapability, _, _>({
-                let lsp_store = this.clone();
+                let lsp_store = lsp_store.clone();
                 move |params, cx| {
                     let lsp_store = lsp_store.clone();
                     let mut cx = cx.clone();
@@ -772,7 +779,7 @@ impl LocalLspStore {
 
         language_server
             .on_request::<lsp::request::ApplyWorkspaceEdit, _, _>({
-                let this = this.clone();
+                let this = lsp_store.clone();
                 move |params, cx| {
                     let mut cx = cx.clone();
                     let this = this.clone();
@@ -791,18 +798,22 @@ impl LocalLspStore {
 
         language_server
             .on_request::<lsp::request::InlayHintRefreshRequest, _, _>({
-                let this = this.clone();
+                let lsp_store = lsp_store.clone();
                 move |(), cx| {
-                    let this = this.clone();
+                    let this = lsp_store.clone();
                     let mut cx = cx.clone();
                     async move {
-                        this.update(&mut cx, |this, cx| {
-                            cx.emit(LspStoreEvent::RefreshInlayHints);
-                            this.downstream_client.as_ref().map(|(client, project_id)| {
-                                client.send(proto::RefreshInlayHints {
-                                    project_id: *project_id,
+                        this.update(&mut cx, |lsp_store, cx| {
+                            cx.emit(LspStoreEvent::RefreshInlayHints(server_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(),
+                                    })
                                 })
-                            })
                         })?
                         .transpose()?;
                         Ok(())
@@ -813,7 +824,7 @@ impl LocalLspStore {
 
         language_server
             .on_request::<lsp::request::CodeLensRefresh, _, _>({
-                let this = this.clone();
+                let this = lsp_store.clone();
                 move |(), cx| {
                     let this = this.clone();
                     let mut cx = cx.clone();
@@ -835,7 +846,7 @@ impl LocalLspStore {
 
         language_server
             .on_request::<lsp::request::WorkspaceDiagnosticRefresh, _, _>({
-                let this = this.clone();
+                let this = lsp_store.clone();
                 move |(), cx| {
                     let this = this.clone();
                     let mut cx = cx.clone();
@@ -861,7 +872,7 @@ impl LocalLspStore {
 
         language_server
             .on_request::<lsp::request::ShowMessageRequest, _, _>({
-                let this = this.clone();
+                let this = lsp_store.clone();
                 let name = name.to_string();
                 move |params, cx| {
                     let this = this.clone();
@@ -899,7 +910,7 @@ impl LocalLspStore {
             .detach();
         language_server
             .on_notification::<lsp::notification::ShowMessage, _>({
-                let this = this.clone();
+                let this = lsp_store.clone();
                 let name = name.to_string();
                 move |params, cx| {
                     let this = this.clone();
@@ -931,7 +942,7 @@ impl LocalLspStore {
 
         language_server
             .on_notification::<lsp::notification::Progress, _>({
-                let this = this.clone();
+                let this = lsp_store.clone();
                 move |params, cx| {
                     if let Some(this) = this.upgrade() {
                         this.update(cx, |this, cx| {
@@ -950,7 +961,7 @@ impl LocalLspStore {
 
         language_server
             .on_notification::<lsp::notification::LogMessage, _>({
-                let this = this.clone();
+                let this = lsp_store.clone();
                 move |params, cx| {
                     if let Some(this) = this.upgrade() {
                         this.update(cx, |_, cx| {
@@ -968,7 +979,7 @@ impl LocalLspStore {
 
         language_server
             .on_notification::<lsp::notification::LogTrace, _>({
-                let this = this.clone();
+                let this = lsp_store.clone();
                 move |params, cx| {
                     let mut cx = cx.clone();
                     if let Some(this) = this.upgrade() {
@@ -987,9 +998,10 @@ impl LocalLspStore {
             })
             .detach();
 
-        json_language_server_ext::register_requests(this.clone(), language_server);
-        rust_analyzer_ext::register_notifications(this.clone(), language_server);
-        clangd_ext::register_notifications(this, language_server, adapter);
+        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(
@@ -3023,9 +3035,8 @@ impl LocalLspStore {
                                     Some(buffer_to_edit.read(cx).saved_version().clone())
                                 };
 
-                                let most_recent_edit = version.and_then(|version| {
-                                    version.iter().max_by_key(|timestamp| timestamp.value)
-                                });
+                                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 {
@@ -3497,9 +3508,55 @@ pub struct LspStore {
     diagnostic_summaries:
         HashMap<WorktreeId, HashMap<Arc<RelPath>, HashMap<LanguageServerId, DiagnosticSummary>>>,
     pub lsp_server_capabilities: HashMap<LanguageServerId, lsp::ServerCapabilities>,
-    lsp_document_colors: HashMap<BufferId, DocumentColorData>,
-    lsp_code_lens: HashMap<BufferId, CodeLensData>,
-    running_lsp_requests: HashMap<TypeId, (Global, HashMap<LspRequestId, Task<()>>)>,
+    lsp_data: HashMap<BufferId, BufferLspData>,
+    next_hint_id: Arc<AtomicUsize>,
+}
+
+#[derive(Debug)]
+pub struct BufferLspData {
+    buffer_version: Global,
+    document_colors: Option<DocumentColorData>,
+    code_lens: Option<CodeLensData>,
+    inlay_hints: BufferInlayHints,
+    lsp_requests: HashMap<LspKey, HashMap<LspRequestId, Task<()>>>,
+    chunk_lsp_requests: HashMap<LspKey, HashMap<BufferChunk, LspRequestId>>,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+struct LspKey {
+    request_type: TypeId,
+    server_queried: Option<LanguageServerId>,
+}
+
+impl BufferLspData {
+    fn new(buffer: &Entity<Buffer>, 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)]
@@ -3513,7 +3570,6 @@ type CodeLensTask = Shared<Task<std::result::Result<Option<Vec<CodeAction>>, Arc
 
 #[derive(Debug, Default)]
 struct DocumentColorData {
-    colors_for_version: Global,
     colors: HashMap<LanguageServerId, HashSet<DocumentColor>>,
     cache_version: usize,
     colors_update: Option<(Global, DocumentColorTask)>,
@@ -3521,7 +3577,6 @@ struct DocumentColorData {
 
 #[derive(Debug, Default)]
 struct CodeLensData {
-    lens_for_version: Global,
     lens: HashMap<LanguageServerId, Vec<CodeAction>>,
     update: Option<(Global, CodeLensTask)>,
 }
@@ -3542,7 +3597,7 @@ pub enum LspStoreEvent {
         new_language: Option<Arc<Language>>,
     },
     Notification(String),
-    RefreshInlayHints,
+    RefreshInlayHints(LanguageServerId),
     RefreshCodeLens,
     DiagnosticsUpdated {
         server_id: LanguageServerId,
@@ -3614,7 +3669,6 @@ impl LspStore {
         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_inlay_hints);
         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);
@@ -3764,9 +3818,8 @@ impl LspStore {
             nonce: StdRng::from_os_rng().random(),
             diagnostic_summaries: HashMap::default(),
             lsp_server_capabilities: HashMap::default(),
-            lsp_document_colors: HashMap::default(),
-            lsp_code_lens: HashMap::default(),
-            running_lsp_requests: 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),
@@ -3825,9 +3878,8 @@ impl LspStore {
             nonce: StdRng::from_os_rng().random(),
             diagnostic_summaries: HashMap::default(),
             lsp_server_capabilities: HashMap::default(),
-            lsp_document_colors: HashMap::default(),
-            lsp_code_lens: HashMap::default(),
-            running_lsp_requests: HashMap::default(),
+            next_hint_id: Arc::default(),
+            lsp_data: HashMap::default(),
             active_entry: None,
 
             _maintain_workspace_config,
@@ -4024,8 +4076,7 @@ impl LspStore {
                         *refcount
                     };
                     if refcount == 0 {
-                        lsp_store.lsp_document_colors.remove(&buffer_id);
-                        lsp_store.lsp_code_lens.remove(&buffer_id);
+                        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);
@@ -4292,7 +4343,7 @@ impl LspStore {
         &self,
         buffer: &Entity<Buffer>,
         request: &R,
-        cx: &Context<Self>,
+        cx: &App,
     ) -> bool
     where
         R: LspCommand,
@@ -4313,7 +4364,7 @@ impl LspStore {
         &self,
         buffer: &Entity<Buffer>,
         check: F,
-        cx: &Context<Self>,
+        cx: &App,
     ) -> bool
     where
         F: Fn(&lsp::ServerCapabilities) -> bool,
@@ -4799,7 +4850,65 @@ impl LspStore {
         }
     }
 
-    pub fn resolve_inlay_hint(
+    pub fn resolved_hint(
+        &mut self,
+        buffer_id: BufferId,
+        id: InlayId,
+        cx: &mut Context<Self>,
+    ) -> Option<ResolvedHint> {
+        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<Buffer>,
@@ -5148,6 +5257,7 @@ impl LspStore {
             }
             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)),
@@ -5213,6 +5323,7 @@ impl LspStore {
             }
             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)),
@@ -5278,6 +5389,7 @@ impl LspStore {
             }
             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)),
@@ -5343,6 +5455,7 @@ impl LspStore {
             }
             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)),
@@ -5409,6 +5522,7 @@ impl LspStore {
 
             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)),
@@ -5476,6 +5590,7 @@ impl LspStore {
             }
             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)),
@@ -5537,32 +5652,38 @@ impl LspStore {
     ) -> 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(cached_data) = self.lsp_code_lens.get(&buffer_id)
-            && !version_queried_for.changed_since(&cached_data.lens_for_version)
-        {
-            let has_different_servers = self.as_local().is_some_and(|local| {
-                local
-                    .buffers_opened_in_servers
-                    .get(&buffer_id)
-                    .cloned()
-                    .unwrap_or_default()
-                    != cached_data.lens.keys().copied().collect()
-            });
-            if !has_different_servers {
-                return Task::ready(Ok(Some(
-                    cached_data.lens.values().flatten().cloned().collect(),
-                )))
-                .shared();
+        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 lsp_data = self.lsp_code_lens.entry(buffer_id).or_default();
-        if let Some((updating_for, running_update)) = &lsp_data.update
-            && !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
@@ -5581,7 +5702,13 @@ impl LspStore {
                     Err(e) => {
                         lsp_store
                             .update(cx, |lsp_store, _| {
-                                lsp_store.lsp_code_lens.entry(buffer_id).or_default().update = None;
+                                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);
@@ -5590,25 +5717,26 @@ impl LspStore {
 
                 lsp_store
                     .update(cx, |lsp_store, _| {
-                        let lsp_data = lsp_store.lsp_code_lens.entry(buffer_id).or_default();
+                        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.lens_for_version == query_version_queried_for {
-                                lsp_data.lens.extend(fetched_lens);
+                            if lsp_data.buffer_version == query_version_queried_for {
+                                code_lens.lens.extend(fetched_lens);
                             } else if !lsp_data
-                                .lens_for_version
+                                .buffer_version
                                 .changed_since(&query_version_queried_for)
                             {
-                                lsp_data.lens_for_version = query_version_queried_for;
-                                lsp_data.lens = fetched_lens;
+                                lsp_data.buffer_version = query_version_queried_for;
+                                code_lens.lens = fetched_lens;
                             }
                         }
-                        lsp_data.update = None;
-                        Some(lsp_data.lens.values().flatten().cloned().collect())
+                        code_lens.update = None;
+                        Some(code_lens.lens.values().flatten().cloned().collect())
                     })
                     .map_err(Arc::new)
             })
             .shared();
-        lsp_data.update = Some((version_queried_for, new_task.clone()));
+        lens_lsp_data.update = Some((version_queried_for, new_task.clone()));
         new_task
     }
 
@@ -5624,6 +5752,7 @@ impl LspStore {
             }
             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)),
@@ -6326,6 +6455,7 @@ impl LspStore {
             }
             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)),
@@ -6368,58 +6498,305 @@ impl LspStore {
         }
     }
 
+    pub fn applicable_inlay_chunks(
+        &mut self,
+        buffer: &Entity<Buffer>,
+        ranges: &[Range<text::Anchor>],
+        cx: &mut Context<Self>,
+    ) -> Vec<Range<BufferRow>> {
+        self.latest_lsp_data(buffer, cx)
+            .inlay_hints
+            .applicable_chunks(ranges)
+            .map(|chunk| chunk.start..chunk.end)
+            .collect()
+    }
+
+    pub fn invalidate_inlay_hints<'a>(
+        &'a mut self,
+        for_buffers: impl IntoIterator<Item = &'a BufferId> + '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<Buffer>,
-        range: Range<Anchor>,
+        ranges: Vec<Range<text::Anchor>>,
+        known_chunks: Option<(clock::Global, HashSet<Range<BufferRow>>)>,
         cx: &mut Context<Self>,
-    ) -> Task<anyhow::Result<Vec<InlayHint>>> {
-        let range_start = range.start;
-        let range_end = range.end;
-        let buffer_id = buffer.read(cx).remote_id().into();
-        let request = InlayHints { range };
+    ) -> HashMap<Range<BufferRow>, Task<Result<CacheInlayHints>>> {
+        let buffer_snapshot = buffer.read(cx).snapshot();
+        let for_server = if let InvalidationStrategy::RefreshRequested(server_id) = invalidate {
+            Some(server_id)
+        } else {
+            None
+        };
+        let invalidate_cache = invalidate.should_invalidate();
+        let next_hint_id = self.next_hint_id.clone();
+        let lsp_data = self.latest_lsp_data(&buffer, cx);
+        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();
 
-        if let Some((client, project_id)) = self.upstream_client() {
-            if !self.is_capable_for_proto_request(&buffer, &request, cx) {
-                return Task::ready(Ok(Vec::new()));
+        let mut hint_fetch_tasks = Vec::new();
+        let mut cached_inlay_hints = HashMap::default();
+        let mut ranges_to_query = Vec::new();
+        let applicable_chunks = existing_inlay_hints
+            .applicable_chunks(ranges.as_slice())
+            .filter(|chunk| !known_chunks.contains(&(chunk.start..chunk.end)))
+            .collect::<Vec<_>>();
+        if applicable_chunks.is_empty() {
+            return HashMap::default();
+        }
+
+        let last_chunk_number = applicable_chunks.len() - 1;
+
+        for (i, row_chunk) in applicable_chunks.into_iter().enumerate() {
+            match (
+                existing_inlay_hints
+                    .cached_hints(&row_chunk)
+                    .filter(|_| !invalidate_cache)
+                    .cloned(),
+                existing_inlay_hints
+                    .fetched_hints(&row_chunk)
+                    .as_ref()
+                    .filter(|_| !invalidate_cache)
+                    .cloned(),
+            ) {
+                (None, None) => {
+                    let end = if last_chunk_number == i {
+                        Point::new(row_chunk.end, buffer_snapshot.line_len(row_chunk.end))
+                    } else {
+                        Point::new(row_chunk.end, 0)
+                    };
+                    ranges_to_query.push((
+                        row_chunk,
+                        buffer_snapshot.anchor_before(Point::new(row_chunk.start, 0))
+                            ..buffer_snapshot.anchor_after(end),
+                    ));
+                }
+                (None, Some(fetched_hints)) => {
+                    hint_fetch_tasks.push((row_chunk, fetched_hints.clone()))
+                }
+                (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
+                                .entry(row_chunk.start..row_chunk.end)
+                                .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.clone()));
+                    for (server_id, cached_hints) in cached_hints {
+                        if for_server.is_none_or(|for_server| for_server == server_id) {
+                            cached_inlay_hints
+                                .entry(row_chunk.start..row_chunk.end)
+                                .or_insert_with(HashMap::default)
+                                .entry(server_id)
+                                .or_insert_with(Vec::new)
+                                .extend(cached_hints);
+                        }
+                    }
+                }
             }
-            let proto_request = proto::InlayHints {
-                project_id,
-                buffer_id,
-                start: Some(serialize_anchor(&range_start)),
-                end: Some(serialize_anchor(&range_end)),
-                version: serialize_version(&buffer.read(cx).version()),
-            };
-            cx.spawn(async move |project, cx| {
-                let response = client
-                    .request(proto_request)
-                    .await
-                    .context("inlay hints proto request")?;
-                LspCommand::response_from_proto(
-                    request,
-                    response,
-                    project.upgrade().context("No project")?,
-                    buffer.clone(),
-                    cx.clone(),
+        }
+
+        let cached_chunk_data = cached_inlay_hints
+            .into_iter()
+            .map(|(row_chunk, hints)| (row_chunk, Task::ready(Ok(hints))))
+            .collect();
+        if hint_fetch_tasks.is_empty() && ranges_to_query.is_empty() {
+            cached_chunk_data
+        } else {
+            if invalidate_cache {
+                lsp_data.inlay_hints.clear();
+            }
+
+            for (chunk, range_to_query) in ranges_to_query {
+                let next_hint_id = next_hint_id.clone();
+                let buffer = buffer.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
+                                        .changed_since(&buffer.read(cx).version());
+                                    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::<Vec<_>>();
+                                            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));
+            }
+
+            let mut combined_data = cached_chunk_data;
+            combined_data.extend(hint_fetch_tasks.into_iter().map(|(chunk, hints_fetch)| {
+                (
+                    chunk.start..chunk.end,
+                    cx.spawn(async move |_, _| {
+                        hints_fetch.await.map_err(|e| {
+                            if e.error_code() != ErrorCode::Internal {
+                                anyhow!(e.error_code())
+                            } else {
+                                anyhow!("{e:#}")
+                            }
+                        })
+                    }),
                 )
-                .await
-                .context("inlay hints proto response conversion")
+            }));
+            combined_data
+        }
+    }
+
+    fn fetch_inlay_hints(
+        &mut self,
+        for_server: Option<LanguageServerId>,
+        buffer: &Entity<Buffer>,
+        range: Range<Anchor>,
+        cx: &mut Context<Self>,
+    ) -> Task<Result<HashMap<LanguageServerId, Vec<InlayHint>>>> {
+        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 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
+                        }
+                    })
+                    .collect::<HashMap<_, _>>();
+                anyhow::ensure!(
+                    !has_errors || !inlay_hints.is_empty(),
+                    "Failed to fetch inlay hints"
+                );
+                Ok(inlay_hints)
             })
         } else {
-            let lsp_request_task = self.request_lsp(
-                buffer.clone(),
-                LanguageServerToQuery::FirstCapable,
-                request,
-                cx,
-            );
-            cx.spawn(async move |_, cx| {
-                buffer
-                    .update(cx, |buffer, _| {
-                        buffer.wait_for_edits(vec![range_start.timestamp, range_end.timestamp])
-                    })?
+            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)),
+                            Err(e) => log::error!(
+                                "Error handling response for inlay hints request: {e:#}"
+                            ),
+                        }
+                        responses
+                    })
+                }
+                None => self.request_multiple_lsp_locally(buffer, None::<usize>, request, cx),
+            };
+            let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
+            cx.background_spawn(async move {
+                Ok(inlay_hints_task
                     .await
-                    .context("waiting for inlay hint request range edits")?;
-                lsp_request_task.await.context("inlay hints LSP request")
+                    .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_le()
+                        });
+                        (server_id, new_hints)
+                    })
+                    .collect())
             })
         }
     }
@@ -6530,39 +6907,55 @@ impl LspStore {
         let version_queried_for = buffer.read(cx).version();
         let buffer_id = buffer.read(cx).remote_id();
 
-        if let Some(cached_data) = self.lsp_document_colors.get(&buffer_id)
-            && !version_queried_for.changed_since(&cached_data.colors_for_version)
-        {
-            let has_different_servers = self.as_local().is_some_and(|local| {
-                local
-                    .buffers_opened_in_servers
-                    .get(&buffer_id)
-                    .cloned()
-                    .unwrap_or_default()
-                    != cached_data.colors.keys().copied().collect()
-            });
-            if !has_different_servers {
-                if Some(cached_data.cache_version) == known_cache_version {
-                    return None;
-                } else {
-                    return Some(
-                        Task::ready(Ok(DocumentColors {
-                            colors: cached_data.colors.values().flatten().cloned().collect(),
-                            cache_version: Some(cached_data.cache_version),
-                        }))
-                        .shared(),
-                    );
+        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 lsp_data = self.lsp_document_colors.entry(buffer_id).or_default();
-        if let Some((updating_for, running_update)) = &lsp_data.colors_update
+        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 query_version_queried_for = version_queried_for.clone();
+        let buffer_version_queried_for = version_queried_for.clone();
         let new_task = cx
             .spawn(async move |lsp_store, cx| {
                 cx.background_executor()

crates/project/src/lsp_store/inlay_hint_cache.rs 🔗

@@ -0,0 +1,221 @@
+use std::{collections::hash_map, ops::Range, sync::Arc};
+
+use collections::HashMap;
+use futures::future::Shared;
+use gpui::{App, Entity, Task};
+use language::{Buffer, BufferRow, BufferSnapshot};
+use lsp::LanguageServerId;
+use text::OffsetRangeExt;
+
+use crate::{InlayHint, InlayId};
+
+pub type CacheInlayHints = HashMap<LanguageServerId, Vec<(InlayId, InlayHint)>>;
+pub type CacheInlayHintsTask = Shared<Task<Result<CacheInlayHints, Arc<anyhow::Error>>>>;
+
+/// 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 <a href="https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_inlayHint_refresh">request</a>.
+    /// 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(LanguageServerId),
+    /// 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 {
+    snapshot: BufferSnapshot,
+    buffer_chunks: Vec<BufferChunk>,
+    hints_by_chunks: Vec<Option<CacheInlayHints>>,
+    fetches_by_chunks: Vec<Option<CacheInlayHintsTask>>,
+    hints_by_id: HashMap<InlayId, HintForId>,
+    pub(super) hint_resolves: HashMap<InlayId, Shared<Task<()>>>,
+}
+
+#[derive(Debug, Clone, Copy)]
+struct HintForId {
+    chunk_id: usize,
+    server_id: LanguageServerId,
+    position: usize,
+}
+
+/// An range of rows, exclusive as [`lsp::Range`] and
+/// <https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#range>
+/// denote.
+///
+/// Represents an area in a text editor, adjacent to other ones.
+/// Together, chunks form entire document at a particular version [`clock::Global`].
+/// Each chunk is queried for inlays as `(start_row, 0)..(end_exclusive, 0)` via
+/// <https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#inlayHintParams>
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub struct BufferChunk {
+    id: usize,
+    pub start: BufferRow,
+    pub end: BufferRow,
+}
+
+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.buffer_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<Buffer>, cx: &mut App) -> Self {
+        let buffer = buffer.read(cx);
+        let snapshot = buffer.snapshot();
+        let buffer_point_range = (0..buffer.len()).to_point(&snapshot);
+        let last_row = buffer_point_range.end.row;
+        let buffer_chunks = (buffer_point_range.start.row..=last_row)
+            .step_by(MAX_ROWS_IN_A_CHUNK as usize)
+            .enumerate()
+            .map(|(id, chunk_start)| BufferChunk {
+                id,
+                start: chunk_start,
+                end: (chunk_start + MAX_ROWS_IN_A_CHUNK).min(last_row),
+            })
+            .collect::<Vec<_>>();
+
+        Self {
+            hints_by_chunks: vec![None; buffer_chunks.len()],
+            fetches_by_chunks: vec![None; buffer_chunks.len()],
+            hints_by_id: HashMap::default(),
+            hint_resolves: HashMap::default(),
+            snapshot,
+            buffer_chunks,
+        }
+    }
+
+    pub fn applicable_chunks(
+        &self,
+        ranges: &[Range<text::Anchor>],
+    ) -> impl Iterator<Item = BufferChunk> {
+        let row_ranges = ranges
+            .iter()
+            .map(|range| range.to_point(&self.snapshot))
+            .map(|point_range| point_range.start.row..=point_range.end.row)
+            .collect::<Vec<_>>();
+        self.buffer_chunks
+            .iter()
+            .filter(move |chunk| -> bool {
+                // Be lenient and yield multiple chunks if they "touch" the exclusive part of the range.
+                // This will result in LSP hints [re-]queried for more ranges, but also more hints already visible when scrolling around.
+                let chunk_range = chunk.start..=chunk.end;
+                row_ranges.iter().any(|row_range| {
+                    chunk_range.contains(&row_range.start())
+                        || chunk_range.contains(&row_range.end())
+                })
+            })
+            .copied()
+    }
+
+    pub fn cached_hints(&mut self, chunk: &BufferChunk) -> Option<&CacheInlayHints> {
+        self.hints_by_chunks[chunk.id].as_ref()
+    }
+
+    pub fn fetched_hints(&mut self, chunk: &BufferChunk) -> &mut Option<CacheInlayHintsTask> {
+        &mut self.fetches_by_chunks[chunk.id]
+    }
+
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn all_cached_hints(&self) -> Vec<InlayHint> {
+        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<CacheInlayHintsTask> {
+        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.buffer_chunks.len()];
+        self.fetches_by_chunks = vec![None; self.buffer_chunks.len()];
+        self.hints_by_id.clear();
+        self.hint_resolves.clear();
+    }
+
+    pub fn insert_new_hints(
+        &mut self,
+        chunk: BufferChunk,
+        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)
+    }
+}

crates/project/src/lsp_store/vue_language_server_ext.rs 🔗

@@ -0,0 +1,124 @@
+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<LspStore>, 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::<VueServerRequest, _>({
+                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::<Vec<_>>();
+                                let _ = vue_server
+                                    .notify::<TypescriptServerResponse>(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::request::ExecuteCommand>(
+                                    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::<TypescriptServerResponse>(vec![(request_id, response_body)])
+                            {
+                                log::warn!(
+                                    "Failed to notify vue-language-server of tsserver response: {err:?}"
+                                );
+                            }
+                        })
+                        .detach();
+                    }
+                }
+            })
+            .detach();
+    }
+}

crates/project/src/project.rs 🔗

@@ -15,6 +15,7 @@ pub mod project_settings;
 pub mod search;
 mod task_inventory;
 pub mod task_store;
+pub mod telemetry_snapshot;
 pub mod terminals;
 pub mod toolchain_store;
 pub mod worktree_store;
@@ -22,7 +23,6 @@ pub mod worktree_store;
 #[cfg(test)]
 mod project_tests;
 
-mod direnv;
 mod environment;
 use buffer_diff::BufferDiff;
 use context_server_store::ContextServerStore;
@@ -146,9 +146,9 @@ pub use task_inventory::{
 
 pub use buffer_store::ProjectTransaction;
 pub use lsp_store::{
-    DiagnosticSummary, LanguageServerLogType, LanguageServerProgress, LanguageServerPromptRequest,
-    LanguageServerStatus, LanguageServerToQuery, LspStore, LspStoreEvent,
-    SERVER_PROGRESS_THROTTLE_TIMEOUT,
+    DiagnosticSummary, InvalidationStrategy, LanguageServerLogType, LanguageServerProgress,
+    LanguageServerPromptRequest, LanguageServerStatus, LanguageServerToQuery, LspStore,
+    LspStoreEvent, SERVER_PROGRESS_THROTTLE_TIMEOUT,
 };
 pub use toolchain_store::{ToolchainStore, Toolchains};
 const MAX_PROJECT_SEARCH_HISTORY_SIZE: usize = 500;
@@ -339,7 +339,7 @@ pub enum Event {
     HostReshared,
     Reshared,
     Rejoined,
-    RefreshInlayHints,
+    RefreshInlayHints(LanguageServerId),
     RefreshCodeLens,
     RevealInProjectPanel(ProjectEntryId),
     SnippetEdit(BufferId, Vec<(lsp::Range, Snippet)>),
@@ -403,6 +403,26 @@ pub enum PrepareRenameResponse {
     InvalidPosition,
 }
 
+#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub enum InlayId {
+    EditPrediction(usize),
+    DebuggerValue(usize),
+    // LSP
+    Hint(usize),
+    Color(usize),
+}
+
+impl InlayId {
+    pub fn id(&self) -> usize {
+        match self {
+            Self::EditPrediction(id) => *id,
+            Self::DebuggerValue(id) => *id,
+            Self::Hint(id) => *id,
+            Self::Color(id) => *id,
+        }
+    }
+}
+
 #[derive(Debug, Clone, PartialEq, Eq)]
 pub struct InlayHint {
     pub position: language::Anchor,
@@ -989,12 +1009,6 @@ impl settings::Settings for DisableAiSettings {
             disable_ai: content.disable_ai.unwrap().0,
         }
     }
-
-    fn import_from_vscode(
-        _vscode: &settings::VsCodeSettings,
-        _current: &mut settings::SettingsContent,
-    ) {
-    }
 }
 
 impl Project {
@@ -1060,7 +1074,7 @@ impl Project {
             let context_server_store =
                 cx.new(|cx| ContextServerStore::new(worktree_store.clone(), weak_self, cx));
 
-            let environment = cx.new(|_| ProjectEnvironment::new(env));
+            let environment = cx.new(|cx| ProjectEnvironment::new(env, cx));
             let manifest_tree = ManifestTree::new(worktree_store.clone(), cx);
             let toolchain_store = cx.new(|cx| {
                 ToolchainStore::local(
@@ -1068,6 +1082,7 @@ impl Project {
                     worktree_store.clone(),
                     environment.clone(),
                     manifest_tree.clone(),
+                    fs.clone(),
                     cx,
                 )
             });
@@ -1294,7 +1309,7 @@ impl Project {
             cx.subscribe(&settings_observer, Self::on_settings_observer_event)
                 .detach();
 
-            let environment = cx.new(|_| ProjectEnvironment::new(None));
+            let environment = cx.new(|cx| ProjectEnvironment::new(None, cx));
 
             let lsp_store = cx.new(|cx| {
                 LspStore::new_remote(
@@ -1507,7 +1522,7 @@ impl Project {
             ImageStore::remote(worktree_store.clone(), client.clone().into(), remote_id, cx)
         })?;
 
-        let environment = cx.new(|_| ProjectEnvironment::new(None))?;
+        let environment = cx.new(|cx| ProjectEnvironment::new(None, cx))?;
 
         let breakpoint_store =
             cx.new(|_| BreakpointStore::remote(remote_id, client.clone().into()))?;
@@ -1570,7 +1585,7 @@ impl Project {
         })?;
 
         let agent_server_store = cx.new(|cx| AgentServerStore::collab(cx))?;
-        let replica_id = response.payload.replica_id as ReplicaId;
+        let replica_id = ReplicaId::new(response.payload.replica_id as u16);
 
         let project = cx.new(|cx| {
             let snippets = SnippetProvider::new(fs.clone(), BTreeSet::from_iter([]), cx);
@@ -1833,10 +1848,12 @@ impl Project {
         project
     }
 
+    #[inline]
     pub fn dap_store(&self) -> Entity<DapStore> {
         self.dap_store.clone()
     }
 
+    #[inline]
     pub fn breakpoint_store(&self) -> Entity<BreakpointStore> {
         self.breakpoint_store.clone()
     }
@@ -1850,50 +1867,62 @@ impl Project {
         Some((session, active_position.clone()))
     }
 
+    #[inline]
     pub fn lsp_store(&self) -> Entity<LspStore> {
         self.lsp_store.clone()
     }
 
+    #[inline]
     pub fn worktree_store(&self) -> Entity<WorktreeStore> {
         self.worktree_store.clone()
     }
 
+    #[inline]
     pub fn context_server_store(&self) -> Entity<ContextServerStore> {
         self.context_server_store.clone()
     }
 
+    #[inline]
     pub fn buffer_for_id(&self, remote_id: BufferId, cx: &App) -> Option<Entity<Buffer>> {
         self.buffer_store.read(cx).get(remote_id)
     }
 
+    #[inline]
     pub fn languages(&self) -> &Arc<LanguageRegistry> {
         &self.languages
     }
 
+    #[inline]
     pub fn client(&self) -> Arc<Client> {
         self.collab_client.clone()
     }
 
+    #[inline]
     pub fn remote_client(&self) -> Option<Entity<RemoteClient>> {
         self.remote_client.clone()
     }
 
+    #[inline]
     pub fn user_store(&self) -> Entity<UserStore> {
         self.user_store.clone()
     }
 
+    #[inline]
     pub fn node_runtime(&self) -> Option<&NodeRuntime> {
         self.node.as_ref()
     }
 
+    #[inline]
     pub fn opened_buffers(&self, cx: &App) -> Vec<Entity<Buffer>> {
         self.buffer_store.read(cx).buffers().collect()
     }
 
+    #[inline]
     pub fn environment(&self) -> &Entity<ProjectEnvironment> {
         &self.environment
     }
 
+    #[inline]
     pub fn cli_environment(&self, cx: &App) -> Option<HashMap<String, String>> {
         self.environment.read(cx).get_cli_environment()
     }
@@ -1924,13 +1953,12 @@ impl Project {
         })
     }
 
-    pub fn peek_environment_error<'a>(
-        &'a self,
-        cx: &'a App,
-    ) -> Option<&'a EnvironmentErrorMessage> {
+    #[inline]
+    pub fn peek_environment_error<'a>(&'a self, cx: &'a App) -> Option<&'a String> {
         self.environment.read(cx).peek_environment_error()
     }
 
+    #[inline]
     pub fn pop_environment_error(&mut self, cx: &mut Context<Self>) {
         self.environment.update(cx, |environment, _| {
             environment.pop_environment_error();
@@ -1938,6 +1966,7 @@ impl Project {
     }
 
     #[cfg(any(test, feature = "test-support"))]
+    #[inline]
     pub fn has_open_buffer(&self, path: impl Into<ProjectPath>, cx: &App) -> bool {
         self.buffer_store
             .read(cx)
@@ -1945,10 +1974,12 @@ impl Project {
             .is_some()
     }
 
+    #[inline]
     pub fn fs(&self) -> &Arc<dyn Fs> {
         &self.fs
     }
 
+    #[inline]
     pub fn remote_id(&self) -> Option<u64> {
         match self.client_state {
             ProjectClientState::Local => None,
@@ -1957,6 +1988,7 @@ impl Project {
         }
     }
 
+    #[inline]
     pub fn supports_terminal(&self, _cx: &App) -> bool {
         if self.is_local() {
             return true;
@@ -1968,39 +2000,45 @@ impl Project {
         false
     }
 
+    #[inline]
     pub fn remote_connection_state(&self, cx: &App) -> Option<remote::ConnectionState> {
         self.remote_client
             .as_ref()
             .map(|remote| remote.read(cx).connection_state())
     }
 
+    #[inline]
     pub fn remote_connection_options(&self, cx: &App) -> Option<RemoteConnectionOptions> {
         self.remote_client
             .as_ref()
             .map(|remote| remote.read(cx).connection_options())
     }
 
+    #[inline]
     pub fn replica_id(&self) -> ReplicaId {
         match self.client_state {
             ProjectClientState::Remote { replica_id, .. } => replica_id,
             _ => {
                 if self.remote_client.is_some() {
-                    1
+                    ReplicaId::REMOTE_SERVER
                 } else {
-                    0
+                    ReplicaId::LOCAL
                 }
             }
         }
     }
 
+    #[inline]
     pub fn task_store(&self) -> &Entity<TaskStore> {
         &self.task_store
     }
 
+    #[inline]
     pub fn snippets(&self) -> &Entity<SnippetProvider> {
         &self.snippets
     }
 
+    #[inline]
     pub fn search_history(&self, kind: SearchInputKind) -> &SearchHistory {
         match kind {
             SearchInputKind::Query => &self.search_history,
@@ -2009,6 +2047,7 @@ impl Project {
         }
     }
 
+    #[inline]
     pub fn search_history_mut(&mut self, kind: SearchInputKind) -> &mut SearchHistory {
         match kind {
             SearchInputKind::Query => &mut self.search_history,
@@ -2017,14 +2056,17 @@ impl Project {
         }
     }
 
+    #[inline]
     pub fn collaborators(&self) -> &HashMap<proto::PeerId, Collaborator> {
         &self.collaborators
     }
 
+    #[inline]
     pub fn host(&self) -> Option<&Collaborator> {
         self.collaborators.values().find(|c| c.is_host)
     }
 
+    #[inline]
     pub fn set_worktrees_reordered(&mut self, worktrees_reordered: bool, cx: &mut App) {
         self.worktree_store.update(cx, |store, _| {
             store.set_worktrees_reordered(worktrees_reordered);
@@ -2032,6 +2074,7 @@ impl Project {
     }
 
     /// Collect all worktrees, including ones that don't appear in the project panel
+    #[inline]
     pub fn worktrees<'a>(
         &self,
         cx: &'a App,
@@ -2040,6 +2083,7 @@ impl Project {
     }
 
     /// Collect all user-visible worktrees, the ones that appear in the project panel.
+    #[inline]
     pub fn visible_worktrees<'a>(
         &'a self,
         cx: &'a App,
@@ -2047,16 +2091,19 @@ impl Project {
         self.worktree_store.read(cx).visible_worktrees(cx)
     }
 
+    #[inline]
     pub fn worktree_for_root_name(&self, root_name: &str, cx: &App) -> Option<Entity<Worktree>> {
         self.visible_worktrees(cx)
             .find(|tree| tree.read(cx).root_name() == root_name)
     }
 
+    #[inline]
     pub fn worktree_root_names<'a>(&'a self, cx: &'a App) -> impl Iterator<Item = &'a str> {
         self.visible_worktrees(cx)
             .map(|tree| tree.read(cx).root_name().as_unix_str())
     }
 
+    #[inline]
     pub fn worktree_for_id(&self, id: WorktreeId, cx: &App) -> Option<Entity<Worktree>> {
         self.worktree_store.read(cx).worktree_for_id(id, cx)
     }
@@ -2071,12 +2118,14 @@ impl Project {
             .worktree_for_entry(entry_id, cx)
     }
 
+    #[inline]
     pub fn worktree_id_for_entry(&self, entry_id: ProjectEntryId, cx: &App) -> Option<WorktreeId> {
         self.worktree_for_entry(entry_id, cx)
             .map(|worktree| worktree.read(cx).id())
     }
 
     /// Checks if the entry is the root of a worktree.
+    #[inline]
     pub fn entry_is_worktree_root(&self, entry_id: ProjectEntryId, cx: &App) -> bool {
         self.worktree_for_entry(entry_id, cx)
             .map(|worktree| {
@@ -2088,6 +2137,7 @@ impl Project {
             .unwrap_or(false)
     }
 
+    #[inline]
     pub fn project_path_git_status(
         &self,
         project_path: &ProjectPath,
@@ -2098,6 +2148,7 @@ impl Project {
             .project_path_git_status(project_path, cx)
     }
 
+    #[inline]
     pub fn visibility_for_paths(
         &self,
         paths: &[PathBuf],
@@ -2149,6 +2200,7 @@ impl Project {
         })
     }
 
+    #[inline]
     pub fn copy_entry(
         &mut self,
         entry_id: ProjectEntryId,
@@ -2227,6 +2279,7 @@ impl Project {
         })
     }
 
+    #[inline]
     pub fn delete_file(
         &mut self,
         path: ProjectPath,
@@ -2237,6 +2290,7 @@ impl Project {
         self.delete_entry(entry.id, trash, cx)
     }
 
+    #[inline]
     pub fn delete_entry(
         &mut self,
         entry_id: ProjectEntryId,
@@ -2250,6 +2304,7 @@ impl Project {
         })
     }
 
+    #[inline]
     pub fn expand_entry(
         &mut self,
         worktree_id: WorktreeId,
@@ -2401,6 +2456,7 @@ impl Project {
         Ok(())
     }
 
+    #[inline]
     pub fn unshare(&mut self, cx: &mut Context<Self>) -> Result<()> {
         self.unshare_internal(cx)?;
         cx.emit(Event::RemoteIdChanged(None));
@@ -2497,10 +2553,12 @@ impl Project {
         }
     }
 
+    #[inline]
     pub fn close(&mut self, cx: &mut Context<Self>) {
         cx.emit(Event::Closed);
     }
 
+    #[inline]
     pub fn is_disconnected(&self, cx: &App) -> bool {
         match &self.client_state {
             ProjectClientState::Remote {
@@ -2514,6 +2572,7 @@ impl Project {
         }
     }
 
+    #[inline]
     fn remote_client_is_disconnected(&self, cx: &App) -> bool {
         self.remote_client
             .as_ref()
@@ -2521,6 +2580,7 @@ impl Project {
             .unwrap_or(false)
     }
 
+    #[inline]
     pub fn capability(&self) -> Capability {
         match &self.client_state {
             ProjectClientState::Remote { capability, .. } => *capability,
@@ -2528,10 +2588,12 @@ impl Project {
         }
     }
 
+    #[inline]
     pub fn is_read_only(&self, cx: &App) -> bool {
         self.is_disconnected(cx) || self.capability() == Capability::ReadOnly
     }
 
+    #[inline]
     pub fn is_local(&self) -> bool {
         match &self.client_state {
             ProjectClientState::Local | ProjectClientState::Shared { .. } => {
@@ -2541,6 +2603,8 @@ impl Project {
         }
     }
 
+    /// Whether this project is a remote server (not counting collab).
+    #[inline]
     pub fn is_via_remote_server(&self) -> bool {
         match &self.client_state {
             ProjectClientState::Local | ProjectClientState::Shared { .. } => {
@@ -2550,6 +2614,8 @@ impl Project {
         }
     }
 
+    /// Whether this project is from collab (not counting remote servers).
+    #[inline]
     pub fn is_via_collab(&self) -> bool {
         match &self.client_state {
             ProjectClientState::Local | ProjectClientState::Shared { .. } => false,
@@ -2557,6 +2623,17 @@ impl Project {
         }
     }
 
+    /// `!self.is_local()`
+    #[inline]
+    pub fn is_remote(&self) -> bool {
+        debug_assert_eq!(
+            !self.is_local(),
+            self.is_via_collab() || self.is_via_remote_server()
+        );
+        !self.is_local()
+    }
+
+    #[inline]
     pub fn create_buffer(
         &mut self,
         searchable: bool,
@@ -2567,6 +2644,7 @@ impl Project {
         })
     }
 
+    #[inline]
     pub fn create_local_buffer(
         &mut self,
         text: &str,
@@ -2574,7 +2652,7 @@ impl Project {
         project_searchable: bool,
         cx: &mut Context<Self>,
     ) -> Entity<Buffer> {
-        if self.is_via_collab() || self.is_via_remote_server() {
+        if self.is_remote() {
             panic!("called create_local_buffer on a remote project")
         }
         self.buffer_store.update(cx, |buffer_store, cx| {
@@ -3000,7 +3078,9 @@ impl Project {
                     return;
                 };
             }
-            LspStoreEvent::RefreshInlayHints => cx.emit(Event::RefreshInlayHints),
+            LspStoreEvent::RefreshInlayHints(server_id) => {
+                cx.emit(Event::RefreshInlayHints(*server_id))
+            }
             LspStoreEvent::RefreshCodeLens => cx.emit(Event::RefreshCodeLens),
             LspStoreEvent::LanguageServerPrompt(prompt) => {
                 cx.emit(Event::LanguageServerPrompt(prompt.clone()))
@@ -3920,31 +4000,6 @@ impl Project {
         })
     }
 
-    pub fn inlay_hints<T: ToOffset>(
-        &mut self,
-        buffer_handle: Entity<Buffer>,
-        range: Range<T>,
-        cx: &mut Context<Self>,
-    ) -> Task<anyhow::Result<Vec<InlayHint>>> {
-        let buffer = buffer_handle.read(cx);
-        let range = buffer.anchor_before(range.start)..buffer.anchor_before(range.end);
-        self.lsp_store.update(cx, |lsp_store, cx| {
-            lsp_store.inlay_hints(buffer_handle, range, cx)
-        })
-    }
-
-    pub fn resolve_inlay_hint(
-        &self,
-        hint: InlayHint,
-        buffer_handle: Entity<Buffer>,
-        server_id: LanguageServerId,
-        cx: &mut Context<Self>,
-    ) -> Task<anyhow::Result<InlayHint>> {
-        self.lsp_store.update(cx, |lsp_store, cx| {
-            lsp_store.resolve_inlay_hint(hint, buffer_handle, server_id, cx)
-        })
-    }
-
     pub fn search(&mut self, query: SearchQuery, cx: &mut Context<Self>) -> Receiver<SearchResult> {
         let (result_tx, result_rx) = smol::channel::unbounded();
 
@@ -5204,6 +5259,7 @@ impl Project {
             })
     }
 
+    #[cfg(any(test, feature = "test-support"))]
     pub fn has_language_servers_for(&self, buffer: &Buffer, cx: &mut App) -> bool {
         self.lsp_store.update(cx, |this, cx| {
             this.language_servers_for_local_buffer(buffer, cx)

crates/project/src/project_settings.rs 🔗

@@ -331,7 +331,7 @@ pub struct InlineBlameSettings {
     /// after a delay once the cursor stops moving.
     ///
     /// Default: 0
-    pub delay_ms: std::time::Duration,
+    pub delay_ms: settings::DelayMs,
     /// The amount of padding between the end of the source line and the start
     /// of the inline blame in units of columns.
     ///
@@ -357,8 +357,8 @@ pub struct BlameSettings {
 
 impl GitSettings {
     pub fn inline_blame_delay(&self) -> Option<Duration> {
-        if self.inline_blame.delay_ms.as_millis() > 0 {
-            Some(self.inline_blame.delay_ms)
+        if self.inline_blame.delay_ms.0 > 0 {
+            Some(Duration::from_millis(self.inline_blame.delay_ms.0))
         } else {
             None
         }
@@ -452,7 +452,7 @@ impl Settings for ProjectSettings {
                 let inline = git.inline_blame.unwrap();
                 InlineBlameSettings {
                     enabled: inline.enabled.unwrap(),
-                    delay_ms: std::time::Duration::from_millis(inline.delay_ms.unwrap()),
+                    delay_ms: inline.delay_ms.unwrap(),
                     padding: inline.padding.unwrap(),
                     min_column: inline.min_column.unwrap(),
                     show_commit_summary: inline.show_commit_summary.unwrap(),
@@ -504,11 +504,11 @@ impl Settings for ProjectSettings {
                 include_warnings: diagnostics.include_warnings.unwrap(),
                 lsp_pull_diagnostics: LspPullDiagnosticsSettings {
                     enabled: lsp_pull_diagnostics.enabled.unwrap(),
-                    debounce_ms: lsp_pull_diagnostics.debounce_ms.unwrap(),
+                    debounce_ms: lsp_pull_diagnostics.debounce_ms.unwrap().0,
                 },
                 inline: InlineDiagnosticsSettings {
                     enabled: inline_diagnostics.enabled.unwrap(),
-                    update_debounce_ms: inline_diagnostics.update_debounce_ms.unwrap(),
+                    update_debounce_ms: inline_diagnostics.update_debounce_ms.unwrap().0,
                     padding: inline_diagnostics.padding.unwrap(),
                     min_column: inline_diagnostics.min_column.unwrap(),
                     max_severity: inline_diagnostics.max_severity.map(Into::into),
@@ -522,65 +522,6 @@ impl Settings for ProjectSettings {
             },
         }
     }
-
-    fn import_from_vscode(
-        vscode: &settings::VsCodeSettings,
-        current: &mut settings::SettingsContent,
-    ) {
-        // this just sets the binary name instead of a full path so it relies on path lookup
-        // resolving to the one you want
-        let npm_path = vscode.read_enum("npm.packageManager", |s| match s {
-            v @ ("npm" | "yarn" | "bun" | "pnpm") => Some(v.to_owned()),
-            _ => None,
-        });
-        if npm_path.is_some() {
-            current.node.get_or_insert_default().npm_path = npm_path;
-        }
-
-        if let Some(b) = vscode.read_bool("git.blame.editorDecoration.enabled") {
-            current
-                .git
-                .get_or_insert_default()
-                .inline_blame
-                .get_or_insert_default()
-                .enabled = Some(b);
-        }
-
-        #[derive(Deserialize)]
-        struct VsCodeContextServerCommand {
-            command: PathBuf,
-            args: Option<Vec<String>>,
-            env: Option<HashMap<String, String>>,
-            // note: we don't support envFile and type
-        }
-        if let Some(mcp) = vscode.read_value("mcp").and_then(|v| v.as_object()) {
-            current
-                .project
-                .context_servers
-                .extend(mcp.iter().filter_map(|(k, v)| {
-                    Some((
-                        k.clone().into(),
-                        settings::ContextServerSettingsContent::Custom {
-                            enabled: true,
-                            command: serde_json::from_value::<VsCodeContextServerCommand>(
-                                v.clone(),
-                            )
-                            .ok()
-                            .map(|cmd| {
-                                settings::ContextServerCommand {
-                                    path: cmd.command,
-                                    args: cmd.args.unwrap_or_default(),
-                                    env: cmd.env,
-                                    timeout: None,
-                                }
-                            })?,
-                        },
-                    ))
-                }));
-        }
-
-        // TODO: translate lsp settings for rust-analyzer and other popular ones to old.lsp
-    }
 }
 
 pub enum SettingsObserverMode {
@@ -1215,6 +1156,7 @@ pub fn local_settings_kind_to_proto(kind: LocalSettingsKind) -> proto::LocalSett
 pub struct DapSettings {
     pub binary: DapBinary,
     pub args: Vec<String>,
+    pub env: HashMap<String, String>,
 }
 
 impl From<DapSettingsContent> for DapSettings {
@@ -1224,6 +1166,7 @@ impl From<DapSettingsContent> for DapSettings {
                 .binary
                 .map_or_else(|| DapBinary::Default, |binary| DapBinary::Custom(binary)),
             args: content.args.unwrap_or_default(),
+            env: content.env.unwrap_or_default(),
         }
     }
 }

crates/project/src/project_tests.rs 🔗

@@ -1815,7 +1815,10 @@ async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) {
     fake_server
         .start_progress(format!("{}/0", progress_token))
         .await;
-    assert_eq!(events.next().await.unwrap(), Event::RefreshInlayHints);
+    assert_eq!(
+        events.next().await.unwrap(),
+        Event::RefreshInlayHints(fake_server.server.server_id())
+    );
     assert_eq!(
         events.next().await.unwrap(),
         Event::DiskBasedDiagnosticsStarted {
@@ -1954,7 +1957,10 @@ async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppC
             Some(worktree_id)
         )
     );
-    assert_eq!(events.next().await.unwrap(), Event::RefreshInlayHints);
+    assert_eq!(
+        events.next().await.unwrap(),
+        Event::RefreshInlayHints(fake_server.server.server_id())
+    );
     fake_server.start_progress(progress_token).await;
     assert_eq!(
         events.next().await.unwrap(),
@@ -4307,7 +4313,7 @@ async fn test_rescan_and_remote_updates(cx: &mut gpui::TestAppContext) {
     let remote = cx.update(|cx| {
         Worktree::remote(
             0,
-            1,
+            ReplicaId::REMOTE_SERVER,
             metadata,
             project.read(cx).client().into(),
             project.read(cx).path_style(cx),
@@ -8846,7 +8852,7 @@ async fn test_file_status(cx: &mut gpui::TestAppContext) {
 }
 
 #[gpui::test]
-#[cfg_attr(target_os = "windows", ignore)]
+#[ignore]
 async fn test_ignored_dirs_events(cx: &mut gpui::TestAppContext) {
     init_test(cx);
     cx.executor().allow_parking();
@@ -8930,10 +8936,7 @@ async fn test_ignored_dirs_events(cx: &mut gpui::TestAppContext) {
     assert_eq!(
         repository_updates.lock().drain(..).collect::<Vec<_>>(),
         vec![
-            RepositoryEvent::Updated {
-                full_scan: true,
-                new_instance: false,
-            },
+            RepositoryEvent::StatusesChanged { full_scan: true },
             RepositoryEvent::MergeHeadsChanged,
         ],
         "Initial worktree scan should produce a repo update event"
@@ -8994,7 +8997,6 @@ async fn test_ignored_dirs_events(cx: &mut gpui::TestAppContext) {
         repository_updates
             .lock()
             .iter()
-            .filter(|update| !matches!(update, RepositoryEvent::PathsChanged))
             .cloned()
             .collect::<Vec<_>>(),
         Vec::new(),
@@ -9098,17 +9100,10 @@ async fn test_odd_events_for_ignored_dirs(
     });
 
     assert_eq!(
-        repository_updates
-            .lock()
-            .drain(..)
-            .filter(|update| !matches!(update, RepositoryEvent::PathsChanged))
-            .collect::<Vec<_>>(),
+        repository_updates.lock().drain(..).collect::<Vec<_>>(),
         vec![
-            RepositoryEvent::Updated {
-                full_scan: true,
-                new_instance: false,
-            },
             RepositoryEvent::MergeHeadsChanged,
+            RepositoryEvent::BranchChanged
         ],
         "Initial worktree scan should produce a repo update event"
     );
@@ -9136,7 +9131,6 @@ async fn test_odd_events_for_ignored_dirs(
         repository_updates
             .lock()
             .iter()
-            .filter(|update| !matches!(update, RepositoryEvent::PathsChanged))
             .cloned()
             .collect::<Vec<_>>(),
         Vec::new(),
@@ -9653,6 +9647,7 @@ fn python_lang(fs: Arc<FakeFs>) -> Arc<Language> {
             worktree_root: PathBuf,
             subroot_relative_path: Arc<RelPath>,
             _: Option<HashMap<String, String>>,
+            _: &dyn Fs,
         ) -> ToolchainList {
             // This lister will always return a path .venv directories within ancestors
             let ancestors = subroot_relative_path.ancestors().collect::<Vec<_>>();
@@ -9677,6 +9672,7 @@ fn python_lang(fs: Arc<FakeFs>) -> Arc<Language> {
             &self,
             _: PathBuf,
             _: Option<HashMap<String, String>>,
+            _: &dyn Fs,
         ) -> anyhow::Result<Toolchain> {
             Err(anyhow::anyhow!("Not implemented"))
         }
@@ -9689,7 +9685,7 @@ fn python_lang(fs: Arc<FakeFs>) -> Arc<Language> {
                 manifest_name: ManifestName::from(SharedString::new_static("pyproject.toml")),
             }
         }
-        async fn activation_script(&self, _: &Toolchain, _: ShellKind, _: &dyn Fs) -> Vec<String> {
+        fn activation_script(&self, _: &Toolchain, _: ShellKind) -> Vec<String> {
             vec![]
         }
     }

crates/project/src/telemetry_snapshot.rs 🔗

@@ -0,0 +1,125 @@
+use git::repository::DiffType;
+use gpui::{App, Entity, Task};
+use serde::{Deserialize, Serialize};
+use worktree::Worktree;
+
+use crate::{
+    Project,
+    git_store::{GitStore, RepositoryState},
+};
+
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
+pub struct TelemetrySnapshot {
+    pub worktree_snapshots: Vec<TelemetryWorktreeSnapshot>,
+}
+
+impl TelemetrySnapshot {
+    pub fn new(project: &Entity<Project>, cx: &mut App) -> Task<TelemetrySnapshot> {
+        let git_store = project.read(cx).git_store().clone();
+        let worktree_snapshots: Vec<_> = project
+            .read(cx)
+            .visible_worktrees(cx)
+            .map(|worktree| TelemetryWorktreeSnapshot::new(worktree, git_store.clone(), cx))
+            .collect();
+
+        cx.spawn(async move |_| {
+            let worktree_snapshots = futures::future::join_all(worktree_snapshots).await;
+
+            Self { worktree_snapshots }
+        })
+    }
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
+pub struct TelemetryWorktreeSnapshot {
+    pub worktree_path: String,
+    pub git_state: Option<GitState>,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
+pub struct GitState {
+    pub remote_url: Option<String>,
+    pub head_sha: Option<String>,
+    pub current_branch: Option<String>,
+    pub diff: Option<String>,
+}
+
+impl TelemetryWorktreeSnapshot {
+    fn new(
+        worktree: Entity<Worktree>,
+        git_store: Entity<GitStore>,
+        cx: &App,
+    ) -> Task<TelemetryWorktreeSnapshot> {
+        cx.spawn(async move |cx| {
+            // Get worktree path and snapshot
+            let worktree_info = cx.update(|app_cx| {
+                let worktree = worktree.read(app_cx);
+                let path = worktree.abs_path().to_string_lossy().into_owned();
+                let snapshot = worktree.snapshot();
+                (path, snapshot)
+            });
+
+            let Ok((worktree_path, _snapshot)) = worktree_info else {
+                return TelemetryWorktreeSnapshot {
+                    worktree_path: String::new(),
+                    git_state: None,
+                };
+            };
+
+            let git_state = git_store
+                .update(cx, |git_store, cx| {
+                    git_store
+                        .repositories()
+                        .values()
+                        .find(|repo| {
+                            repo.read(cx)
+                                .abs_path_to_repo_path(&worktree.read(cx).abs_path())
+                                .is_some()
+                        })
+                        .cloned()
+                })
+                .ok()
+                .flatten()
+                .map(|repo| {
+                    repo.update(cx, |repo, _| {
+                        let current_branch =
+                            repo.branch.as_ref().map(|branch| branch.name().to_owned());
+                        repo.send_job(None, |state, _| async move {
+                            let RepositoryState::Local { backend, .. } = state else {
+                                return GitState {
+                                    remote_url: None,
+                                    head_sha: None,
+                                    current_branch,
+                                    diff: None,
+                                };
+                            };
+
+                            let remote_url = backend.remote_url("origin");
+                            let head_sha = backend.head_sha().await;
+                            let diff = backend.diff(DiffType::HeadToWorktree).await.ok();
+
+                            GitState {
+                                remote_url,
+                                head_sha,
+                                current_branch,
+                                diff,
+                            }
+                        })
+                    })
+                });
+
+            let git_state = match git_state {
+                Some(git_state) => match git_state.ok() {
+                    Some(git_state) => git_state.await.ok(),
+                    None => None,
+                },
+                None => None,
+            };
+
+            TelemetryWorktreeSnapshot {
+                worktree_path,
+                git_state,
+            }
+        })
+    }
+}

crates/project/src/terminals.rs 🔗

@@ -120,7 +120,6 @@ impl Project {
             .map(|p| self.active_toolchain(p, LanguageName::new("Python"), cx))
             .collect::<Vec<_>>();
         let lang_registry = self.languages.clone();
-        let fs = self.fs.clone();
         cx.spawn(async move |project, cx| {
             let shell_kind = ShellKind::new(&shell, is_windows);
             let activation_script = maybe!(async {
@@ -133,152 +132,153 @@ impl Project {
                         .await
                         .ok();
                     let lister = language?.toolchain_lister();
-                    return Some(
-                        lister?
-                            .activation_script(&toolchain, shell_kind, fs.as_ref())
-                            .await,
-                    );
+                    return Some(lister?.activation_script(&toolchain, shell_kind));
                 }
                 None
             })
             .await
             .unwrap_or_default();
 
-            project.update(cx, move |this, cx| {
-                let format_to_run = || {
-                    if let Some(command) = &spawn_task.command {
-                        let mut command: Option<Cow<str>> = shell_kind.try_quote(command);
-                        if let Some(command) = &mut command
-                            && command.starts_with('"')
-                            && let Some(prefix) = shell_kind.command_prefix()
-                        {
-                            *command = Cow::Owned(format!("{prefix}{command}"));
-                        }
+            let builder = project
+                .update(cx, move |_, cx| {
+                    let format_to_run = || {
+                        if let Some(command) = &spawn_task.command {
+                            let mut command: Option<Cow<str>> = shell_kind.try_quote(command);
+                            if let Some(command) = &mut command
+                                && command.starts_with('"')
+                                && let Some(prefix) = shell_kind.command_prefix()
+                            {
+                                *command = Cow::Owned(format!("{prefix}{command}"));
+                            }
 
-                        let args = spawn_task
-                            .args
-                            .iter()
-                            .filter_map(|arg| shell_kind.try_quote(&arg));
+                            let args = spawn_task
+                                .args
+                                .iter()
+                                .filter_map(|arg| shell_kind.try_quote(&arg));
 
-                        command.into_iter().chain(args).join(" ")
-                    } else {
-                        // todo: this breaks for remotes to windows
-                        format!("exec {shell} -l")
-                    }
-                };
-
-                let (shell, env) = {
-                    env.extend(spawn_task.env);
-                    match remote_client {
-                        Some(remote_client) => match activation_script.clone() {
-                            activation_script if !activation_script.is_empty() => {
-                                let activation_script = activation_script.join("; ");
-                                let to_run = format_to_run();
-                                let args =
-                                    vec!["-c".to_owned(), format!("{activation_script}; {to_run}")];
-                                create_remote_shell(
-                                    Some((
-                                        &remote_client
-                                            .read(cx)
-                                            .shell()
-                                            .unwrap_or_else(get_default_system_shell),
-                                        &args,
-                                    )),
+                            command.into_iter().chain(args).join(" ")
+                        } else {
+                            // todo: this breaks for remotes to windows
+                            format!("exec {shell} -l")
+                        }
+                    };
+
+                    let (shell, env) = {
+                        env.extend(spawn_task.env);
+                        match remote_client {
+                            Some(remote_client) => match activation_script.clone() {
+                                activation_script if !activation_script.is_empty() => {
+                                    let activation_script = activation_script.join("; ");
+                                    let to_run = format_to_run();
+                                    let args = vec![
+                                        "-c".to_owned(),
+                                        format!("{activation_script}; {to_run}"),
+                                    ];
+                                    create_remote_shell(
+                                        Some((
+                                            &remote_client
+                                                .read(cx)
+                                                .shell()
+                                                .unwrap_or_else(get_default_system_shell),
+                                            &args,
+                                        )),
+                                        env,
+                                        path,
+                                        remote_client,
+                                        cx,
+                                    )?
+                                }
+                                _ => create_remote_shell(
+                                    spawn_task
+                                        .command
+                                        .as_ref()
+                                        .map(|command| (command, &spawn_task.args)),
                                     env,
                                     path,
                                     remote_client,
                                     cx,
-                                )?
-                            }
-                            _ => create_remote_shell(
-                                spawn_task
-                                    .command
-                                    .as_ref()
-                                    .map(|command| (command, &spawn_task.args)),
-                                env,
-                                path,
-                                remote_client,
-                                cx,
-                            )?,
-                        },
-                        None => match activation_script.clone() {
-                            activation_script if !activation_script.is_empty() => {
-                                let separator = shell_kind.sequential_commands_separator();
-                                let activation_script =
-                                    activation_script.join(&format!("{separator} "));
-                                let to_run = format_to_run();
-
-                                let mut arg = format!("{activation_script}{separator} {to_run}");
-                                if shell_kind == ShellKind::Cmd {
-                                    // We need to put the entire command in quotes since otherwise CMD tries to execute them
-                                    // as separate commands rather than chaining one after another.
-                                    arg = format!("\"{arg}\"");
-                                }
+                                )?,
+                            },
+                            None => match activation_script.clone() {
+                                activation_script if !activation_script.is_empty() => {
+                                    let separator = shell_kind.sequential_commands_separator();
+                                    let activation_script =
+                                        activation_script.join(&format!("{separator} "));
+                                    let to_run = format_to_run();
+
+                                    let mut arg =
+                                        format!("{activation_script}{separator} {to_run}");
+                                    if shell_kind == ShellKind::Cmd {
+                                        // We need to put the entire command in quotes since otherwise CMD tries to execute them
+                                        // as separate commands rather than chaining one after another.
+                                        arg = format!("\"{arg}\"");
+                                    }
 
-                                let args = shell_kind.args_for_shell(false, arg);
+                                    let args = shell_kind.args_for_shell(false, arg);
 
-                                (
-                                    Shell::WithArguments {
-                                        program: shell,
-                                        args,
-                                        title_override: None,
+                                    (
+                                        Shell::WithArguments {
+                                            program: shell,
+                                            args,
+                                            title_override: None,
+                                        },
+                                        env,
+                                    )
+                                }
+                                _ => (
+                                    if let Some(program) = spawn_task.command {
+                                        Shell::WithArguments {
+                                            program,
+                                            args: spawn_task.args,
+                                            title_override: None,
+                                        }
+                                    } else {
+                                        Shell::System
                                     },
                                     env,
-                                )
-                            }
-                            _ => (
-                                if let Some(program) = spawn_task.command {
-                                    Shell::WithArguments {
-                                        program,
-                                        args: spawn_task.args,
-                                        title_override: None,
-                                    }
-                                } else {
-                                    Shell::System
-                                },
-                                env,
-                            ),
-                        },
-                    }
-                };
-                TerminalBuilder::new(
-                    local_path.map(|path| path.to_path_buf()),
-                    task_state,
-                    shell,
-                    env,
-                    settings.cursor_shape,
-                    settings.alternate_scroll,
-                    settings.max_scroll_history_lines,
-                    is_via_remote,
-                    cx.entity_id().as_u64(),
-                    Some(completion_tx),
-                    cx,
-                    activation_script,
-                )
-                .map(|builder| {
-                    let terminal_handle = cx.new(|cx| builder.subscribe(cx));
-
-                    this.terminals
-                        .local_handles
-                        .push(terminal_handle.downgrade());
-
-                    let id = terminal_handle.entity_id();
-                    cx.observe_release(&terminal_handle, move |project, _terminal, cx| {
-                        let handles = &mut project.terminals.local_handles;
-
-                        if let Some(index) = handles
-                            .iter()
-                            .position(|terminal| terminal.entity_id() == id)
-                        {
-                            handles.remove(index);
-                            cx.notify();
+                                ),
+                            },
                         }
-                    })
-                    .detach();
+                    };
+                    anyhow::Ok(TerminalBuilder::new(
+                        local_path.map(|path| path.to_path_buf()),
+                        task_state,
+                        shell,
+                        env,
+                        settings.cursor_shape,
+                        settings.alternate_scroll,
+                        settings.max_scroll_history_lines,
+                        is_via_remote,
+                        cx.entity_id().as_u64(),
+                        Some(completion_tx),
+                        cx,
+                        activation_script,
+                    ))
+                })??
+                .await?;
+            project.update(cx, move |this, cx| {
+                let terminal_handle = cx.new(|cx| builder.subscribe(cx));
 
-                    terminal_handle
+                this.terminals
+                    .local_handles
+                    .push(terminal_handle.downgrade());
+
+                let id = terminal_handle.entity_id();
+                cx.observe_release(&terminal_handle, move |project, _terminal, cx| {
+                    let handles = &mut project.terminals.local_handles;
+
+                    if let Some(index) = handles
+                        .iter()
+                        .position(|terminal| terminal.entity_id() == id)
+                    {
+                        handles.remove(index);
+                        cx.notify();
+                    }
                 })
-            })?
+                .detach();
+
+                terminal_handle
+            })
         })
     }
 
@@ -342,7 +342,6 @@ impl Project {
         let shell_kind = ShellKind::new(&shell, self.path_style(cx).is_windows());
 
         let lang_registry = self.languages.clone();
-        let fs = self.fs.clone();
         cx.spawn(async move |project, cx| {
             let activation_script = maybe!(async {
                 for toolchain in toolchains {
@@ -354,63 +353,61 @@ impl Project {
                         .await
                         .ok();
                     let lister = language?.toolchain_lister();
-                    return Some(
-                        lister?
-                            .activation_script(&toolchain, shell_kind, fs.as_ref())
-                            .await,
-                    );
+                    return Some(lister?.activation_script(&toolchain, shell_kind));
                 }
                 None
             })
             .await
             .unwrap_or_default();
-            project.update(cx, move |this, cx| {
-                let (shell, env) = {
-                    match remote_client {
-                        Some(remote_client) => {
-                            create_remote_shell(None, env, path, remote_client, cx)?
-                        }
-                        None => (settings.shell, env),
-                    }
-                };
-                TerminalBuilder::new(
-                    local_path.map(|path| path.to_path_buf()),
-                    None,
-                    shell,
-                    env,
-                    settings.cursor_shape,
-                    settings.alternate_scroll,
-                    settings.max_scroll_history_lines,
-                    is_via_remote,
-                    cx.entity_id().as_u64(),
-                    None,
-                    cx,
-                    activation_script,
-                )
-                .map(|builder| {
-                    let terminal_handle = cx.new(|cx| builder.subscribe(cx));
-
-                    this.terminals
-                        .local_handles
-                        .push(terminal_handle.downgrade());
-
-                    let id = terminal_handle.entity_id();
-                    cx.observe_release(&terminal_handle, move |project, _terminal, cx| {
-                        let handles = &mut project.terminals.local_handles;
-
-                        if let Some(index) = handles
-                            .iter()
-                            .position(|terminal| terminal.entity_id() == id)
-                        {
-                            handles.remove(index);
-                            cx.notify();
+            let builder = project
+                .update(cx, move |_, cx| {
+                    let (shell, env) = {
+                        match remote_client {
+                            Some(remote_client) => {
+                                create_remote_shell(None, env, path, remote_client, cx)?
+                            }
+                            None => (settings.shell, env),
                         }
-                    })
-                    .detach();
+                    };
+                    anyhow::Ok(TerminalBuilder::new(
+                        local_path.map(|path| path.to_path_buf()),
+                        None,
+                        shell,
+                        env,
+                        settings.cursor_shape,
+                        settings.alternate_scroll,
+                        settings.max_scroll_history_lines,
+                        is_via_remote,
+                        cx.entity_id().as_u64(),
+                        None,
+                        cx,
+                        activation_script,
+                    ))
+                })??
+                .await?;
+            project.update(cx, move |this, cx| {
+                let terminal_handle = cx.new(|cx| builder.subscribe(cx));
 
-                    terminal_handle
+                this.terminals
+                    .local_handles
+                    .push(terminal_handle.downgrade());
+
+                let id = terminal_handle.entity_id();
+                cx.observe_release(&terminal_handle, move |project, _terminal, cx| {
+                    let handles = &mut project.terminals.local_handles;
+
+                    if let Some(index) = handles
+                        .iter()
+                        .position(|terminal| terminal.entity_id() == id)
+                    {
+                        handles.remove(index);
+                        cx.notify();
+                    }
                 })
-            })?
+                .detach();
+
+                terminal_handle
+            })
         })
     }
 
@@ -419,20 +416,27 @@ impl Project {
         terminal: &Entity<Terminal>,
         cx: &mut Context<'_, Project>,
         cwd: Option<PathBuf>,
-    ) -> Result<Entity<Terminal>> {
+    ) -> Task<Result<Entity<Terminal>>> {
+        // We cannot clone the task's terminal, as it will effectively re-spawn the task, which might not be desirable.
+        // For now, create a new shell instead.
+        if terminal.read(cx).task().is_some() {
+            return self.create_terminal_shell(cwd, cx);
+        }
+
         let local_path = if self.is_via_remote_server() {
             None
         } else {
             cwd
         };
 
-        terminal
-            .read(cx)
-            .clone_builder(cx, local_path)
-            .map(|builder| {
-                let terminal_handle = cx.new(|cx| builder.subscribe(cx));
+        let builder = terminal.read(cx).clone_builder(cx, local_path);
+        cx.spawn(async |project, cx| {
+            let terminal = builder.await?;
+            project.update(cx, |project, cx| {
+                let terminal_handle = cx.new(|cx| terminal.subscribe(cx));
 
-                self.terminals
+                project
+                    .terminals
                     .local_handles
                     .push(terminal_handle.downgrade());
 
@@ -452,6 +456,7 @@ impl Project {
 
                 terminal_handle
             })
+        })
     }
 
     pub fn terminal_settings<'a>(

crates/project/src/toolchain_store.rs 🔗

@@ -4,6 +4,7 @@ use anyhow::{Context as _, Result, bail};
 
 use async_trait::async_trait;
 use collections::{BTreeMap, IndexSet};
+use fs::Fs;
 use gpui::{
     App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Subscription, Task, WeakEntity,
 };
@@ -60,6 +61,7 @@ impl ToolchainStore {
         worktree_store: Entity<WorktreeStore>,
         project_environment: Entity<ProjectEnvironment>,
         manifest_tree: Entity<ManifestTree>,
+        fs: Arc<dyn Fs>,
         cx: &mut Context<Self>,
     ) -> Self {
         let entity = cx.new(|_| LocalToolchainStore {
@@ -68,6 +70,7 @@ impl ToolchainStore {
             project_environment,
             active_toolchains: Default::default(),
             manifest_tree,
+            fs,
         });
         let _sub = cx.subscribe(&entity, |_, _, e: &ToolchainStoreEvent, cx| {
             cx.emit(e.clone())
@@ -397,6 +400,7 @@ pub struct LocalToolchainStore {
     project_environment: Entity<ProjectEnvironment>,
     active_toolchains: BTreeMap<(WorktreeId, LanguageName), BTreeMap<Arc<RelPath>, Toolchain>>,
     manifest_tree: Entity<ManifestTree>,
+    fs: Arc<dyn Fs>,
 }
 
 #[async_trait(?Send)]
@@ -485,6 +489,7 @@ impl LocalToolchainStore {
         let registry = self.languages.clone();
 
         let manifest_tree = self.manifest_tree.downgrade();
+        let fs = self.fs.clone();
 
         let environment = self.project_environment.clone();
         cx.spawn(async move |this, cx| {
@@ -534,7 +539,12 @@ impl LocalToolchainStore {
             cx.background_spawn(async move {
                 Some((
                     toolchains
-                        .list(worktree_root, relative_path.path.clone(), project_env)
+                        .list(
+                            worktree_root,
+                            relative_path.path.clone(),
+                            project_env,
+                            fs.as_ref(),
+                        )
                         .await,
                     relative_path.path,
                 ))
@@ -568,6 +578,7 @@ impl LocalToolchainStore {
     ) -> Task<Result<Toolchain>> {
         let registry = self.languages.clone();
         let environment = self.project_environment.clone();
+        let fs = self.fs.clone();
         cx.spawn(async move |_, cx| {
             let language = cx
                 .background_spawn(registry.language_for_name(&language_name.0))
@@ -586,8 +597,12 @@ impl LocalToolchainStore {
                     )
                 })?
                 .await;
-            cx.background_spawn(async move { toolchain_lister.resolve(path, project_env).await })
-                .await
+            cx.background_spawn(async move {
+                toolchain_lister
+                    .resolve(path, project_env, fs.as_ref())
+                    .await
+            })
+            .await
         })
     }
 }

crates/project/src/worktree_store.rs 🔗

@@ -5,7 +5,7 @@ use std::{
     sync::{Arc, atomic::AtomicUsize},
 };
 
-use anyhow::{Context as _, Result, anyhow};
+use anyhow::{Context as _, Result, anyhow, bail};
 use collections::{HashMap, HashSet};
 use fs::{Fs, copy_recursive};
 use futures::{
@@ -551,7 +551,7 @@ impl WorktreeStore {
             let worktree = cx.update(|cx| {
                 Worktree::remote(
                     REMOTE_SERVER_PROJECT_ID,
-                    0,
+                    ReplicaId::REMOTE_SERVER,
                     proto::WorktreeMetadata {
                         id: response.worktree_id,
                         root_name,
@@ -1203,6 +1203,16 @@ impl WorktreeStore {
             RelPath::from_proto(&envelope.payload.new_path)?,
         );
         let (scan_id, entry) = this.update(&mut cx, |this, cx| {
+            let Some((_, project_id)) = this.downstream_client else {
+                bail!("no downstream client")
+            };
+            let Some(entry) = this.entry_for_id(entry_id, cx) else {
+                bail!("no such entry");
+            };
+            if entry.is_private && project_id != REMOTE_SERVER_PROJECT_ID {
+                bail!("entry is private")
+            }
+
             let new_worktree = this
                 .worktree_for_id(new_worktree_id, cx)
                 .context("no such worktree")?;
@@ -1226,6 +1236,15 @@ impl WorktreeStore {
     ) -> Result<proto::ProjectEntryResponse> {
         let entry_id = ProjectEntryId::from_proto(envelope.payload.entry_id);
         let worktree = this.update(&mut cx, |this, cx| {
+            let Some((_, project_id)) = this.downstream_client else {
+                bail!("no downstream client")
+            };
+            let Some(entry) = this.entry_for_id(entry_id, cx) else {
+                bail!("no entry")
+            };
+            if entry.is_private && project_id != REMOTE_SERVER_PROJECT_ID {
+                bail!("entry is private")
+            }
             this.worktree_for_entry(entry_id, cx)
                 .context("worktree not found")
         })??;
@@ -1246,6 +1265,18 @@ impl WorktreeStore {
             let worktree = this
                 .worktree_for_entry(entry_id, cx)
                 .context("no such worktree")?;
+
+            let Some((_, project_id)) = this.downstream_client else {
+                bail!("no downstream client")
+            };
+            let entry = worktree
+                .read(cx)
+                .entry_for_id(entry_id)
+                .ok_or_else(|| anyhow!("missing entry"))?;
+            if entry.is_private && project_id != REMOTE_SERVER_PROJECT_ID {
+                bail!("entry is private")
+            }
+
             let scan_id = worktree.read(cx).scan_id();
             anyhow::Ok((
                 scan_id,

crates/project_panel/Cargo.toml 🔗

@@ -45,7 +45,6 @@ workspace.workspace = true
 language.workspace = true
 zed_actions.workspace = true
 telemetry.workspace = true
-workspace-hack.workspace = true
 
 [dev-dependencies]
 client = { workspace = true, features = ["test-support"] }

crates/project_panel/src/project_panel.rs 🔗

@@ -64,7 +64,7 @@ use workspace::{
     DraggedSelection, OpenInTerminal, OpenOptions, OpenVisible, PreviewTabsSettings, SelectedEntry,
     SplitDirection, Workspace,
     dock::{DockPosition, Panel, PanelEvent},
-    notifications::{DetachAndPromptErr, NotifyTaskExt},
+    notifications::{DetachAndPromptErr, NotifyResultExt, NotifyTaskExt},
 };
 use worktree::CreatedEntry;
 use zed_actions::workspace::OpenWithSystem;
@@ -491,17 +491,17 @@ impl ProjectPanel {
         let project_panel = cx.new(|cx| {
             let focus_handle = cx.focus_handle();
             cx.on_focus(&focus_handle, window, Self::focus_in).detach();
-            cx.on_focus_out(&focus_handle, window, |this, _, window, cx| {
-                this.focus_out(window, cx);
-            })
-            .detach();
 
             cx.subscribe_in(
                 &git_store,
                 window,
                 |this, _, event, window, cx| match event {
-                    GitStoreEvent::RepositoryUpdated(_, RepositoryEvent::Updated { .. }, _)
-                    | GitStoreEvent::RepositoryAdded(_)
+                    GitStoreEvent::RepositoryUpdated(
+                        _,
+                        RepositoryEvent::StatusesChanged { full_scan: _ },
+                        _,
+                    )
+                    | GitStoreEvent::RepositoryAdded
                     | GitStoreEvent::RepositoryRemoved(_) => {
                         this.update_visible_entries(None, false, false, window, cx);
                         cx.notify();
@@ -615,8 +615,11 @@ impl ProjectPanel {
             .detach();
 
             let trash_action = [TypeId::of::<Trash>()];
-            let is_remote = project.read(cx).is_via_collab();
+            let is_remote = project.read(cx).is_remote();
 
+            // Make sure the trash option is never displayed anywhere on remote
+            // hosts since they may not support trashing. May want to dynamically
+            // detect this in the future.
             if is_remote {
                 CommandPaletteFilter::update_global(cx, |filter, _cx| {
                     filter.hide_action_types(&trash_action);
@@ -643,7 +646,7 @@ impl ProjectPanel {
                             .as_ref()
                             .is_some_and(|state| state.processing_filename.is_none())
                         {
-                            match project_panel.confirm_edit(window, cx) {
+                            match project_panel.confirm_edit(false, window, cx) {
                                 Some(task) => {
                                     task.detach_and_notify_err(window, cx);
                                 }
@@ -947,12 +950,6 @@ impl ProjectPanel {
         }
     }
 
-    fn focus_out(&mut self, window: &mut Window, cx: &mut Context<Self>) {
-        if !self.focus_handle.is_focused(window) {
-            self.confirm(&Confirm, window, cx);
-        }
-    }
-
     fn deploy_context_menu(
         &mut self,
         position: Point<Pixels>,
@@ -981,7 +978,7 @@ impl ProjectPanel {
             let is_foldable = auto_fold_dirs && self.is_foldable(entry, worktree);
             let is_unfoldable = auto_fold_dirs && self.is_unfoldable(entry, worktree);
             let is_read_only = project.is_read_only(cx);
-            let is_remote = project.is_via_collab();
+            let is_remote = project.is_remote();
             let is_local = project.is_local();
 
             let settings = ProjectPanelSettings::get_global(cx);
@@ -1045,13 +1042,13 @@ impl ProjectPanel {
                             .when(!should_hide_rename, |menu| {
                                 menu.action("Rename", Box::new(Rename))
                             })
-                            .when(!is_root & !is_remote, |menu| {
+                            .when(!is_root && !is_remote, |menu| {
                                 menu.action("Trash", Box::new(Trash { skip_prompt: false }))
                             })
                             .when(!is_root, |menu| {
                                 menu.action("Delete", Box::new(Delete { skip_prompt: false }))
                             })
-                            .when(!is_remote & is_root, |menu| {
+                            .when(!is_remote && is_root, |menu| {
                                 menu.separator()
                                     .action(
                                         "Add Folder to Project…",
@@ -1421,7 +1418,7 @@ impl ProjectPanel {
     }
 
     fn confirm(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context<Self>) {
-        if let Some(task) = self.confirm_edit(window, cx) {
+        if let Some(task) = self.confirm_edit(true, window, cx) {
             task.detach_and_notify_err(window, cx);
         }
     }
@@ -1553,6 +1550,7 @@ impl ProjectPanel {
 
     fn confirm_edit(
         &mut self,
+        refocus: bool,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) -> Option<Task<Result<()>>> {
@@ -1606,7 +1604,7 @@ impl ProjectPanel {
                 filename.clone()
             };
             if let Some(existing) = worktree.read(cx).entry_for_path(&new_path) {
-                if existing.id == entry.id {
+                if existing.id == entry.id && refocus {
                     window.focus(&self.focus_handle);
                 }
                 return None;
@@ -1617,7 +1615,9 @@ impl ProjectPanel {
             });
         };
 
-        window.focus(&self.focus_handle);
+        if refocus {
+            window.focus(&self.focus_handle);
+        }
         edit_state.processing_filename = Some(filename);
         cx.notify();
 
@@ -2677,12 +2677,14 @@ impl ProjectPanel {
                 for task in paste_tasks {
                     match task {
                         PasteTask::Rename(task) => {
-                            if let Some(CreatedEntry::Included(entry)) = task.await.log_err() {
+                            if let Some(CreatedEntry::Included(entry)) =
+                                task.await.notify_async_err(cx)
+                            {
                                 last_succeed = Some(entry);
                             }
                         }
                         PasteTask::Copy(task) => {
-                            if let Some(Some(entry)) = task.await.log_err() {
+                            if let Some(Some(entry)) = task.await.notify_async_err(cx) {
                                 last_succeed = Some(entry);
                             }
                         }
@@ -4680,12 +4682,11 @@ impl ProjectPanel {
                             div()
                                 .id("symlink_icon")
                                 .pr_3()
-                                .tooltip(move |window, cx| {
+                                .tooltip(move |_window, cx| {
                                     Tooltip::with_meta(
                                         path.to_string(),
                                         None,
                                         "Symbolic Link",
-                                        window,
                                         cx,
                                     )
                                 })
@@ -5456,33 +5457,13 @@ impl Render for ProjectPanel {
                         .on_action(cx.listener(Self::new_directory))
                         .on_action(cx.listener(Self::rename))
                         .on_action(cx.listener(Self::delete))
-                        .on_action(cx.listener(Self::trash))
                         .on_action(cx.listener(Self::cut))
                         .on_action(cx.listener(Self::copy))
                         .on_action(cx.listener(Self::paste))
                         .on_action(cx.listener(Self::duplicate))
-                        .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);
-                            }
-                        }))
+                        .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))
@@ -5817,7 +5798,34 @@ impl Render for ProjectPanel {
                                             );
                                         }
                                     }),
-                                ),
+                                )
+                                .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(),
                 )
@@ -5858,7 +5866,6 @@ impl Render for ProjectPanel {
                         .key_binding(KeyBinding::for_action_in(
                             &workspace::Open,
                             &focus_handle,
-                            window,
                             cx,
                         ))
                         .on_click(cx.listener(|this, _, window, cx| {
@@ -6009,6 +6016,10 @@ impl Panel for ProjectPanel {
         "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;

crates/project_panel/src/project_panel_settings.rs 🔗

@@ -2,10 +2,7 @@ use editor::EditorSettings;
 use gpui::Pixels;
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
-use settings::{
-    DockSide, ProjectPanelEntrySpacing, Settings, SettingsContent, ShowDiagnostics,
-    ShowIndentGuides,
-};
+use settings::{DockSide, ProjectPanelEntrySpacing, Settings, ShowDiagnostics, ShowIndentGuides};
 use ui::{
     px,
     scrollbars::{ScrollbarVisibility, ShowScrollbar},
@@ -86,39 +83,4 @@ impl Settings for ProjectPanelSettings {
             open_file_on_paste: project_panel.open_file_on_paste.unwrap(),
         }
     }
-
-    fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut SettingsContent) {
-        if let Some(hide_gitignore) = vscode.read_bool("explorer.excludeGitIgnore") {
-            current.project_panel.get_or_insert_default().hide_gitignore = Some(hide_gitignore);
-        }
-        if let Some(auto_reveal) = vscode.read_bool("explorer.autoReveal") {
-            current
-                .project_panel
-                .get_or_insert_default()
-                .auto_reveal_entries = Some(auto_reveal);
-        }
-        if let Some(compact_folders) = vscode.read_bool("explorer.compactFolders") {
-            current.project_panel.get_or_insert_default().auto_fold_dirs = Some(compact_folders);
-        }
-
-        if Some(false) == vscode.read_bool("git.decorations.enabled") {
-            current.project_panel.get_or_insert_default().git_status = Some(false);
-        }
-        if Some(false) == vscode.read_bool("problems.decorations.enabled") {
-            current
-                .project_panel
-                .get_or_insert_default()
-                .show_diagnostics = Some(ShowDiagnostics::Off);
-        }
-        if let (Some(false), Some(false)) = (
-            vscode.read_bool("explorer.decorations.badges"),
-            vscode.read_bool("explorer.decorations.colors"),
-        ) {
-            current.project_panel.get_or_insert_default().git_status = Some(false);
-            current
-                .project_panel
-                .get_or_insert_default()
-                .show_diagnostics = Some(ShowDiagnostics::Off);
-        }
-    }
 }

crates/project_panel/src/project_panel_tests.rs 🔗

@@ -556,7 +556,7 @@ async fn test_editing_files(cx: &mut gpui::TestAppContext) {
         panel.filename_editor.update(cx, |editor, cx| {
             editor.set_text("the-new-filename", window, cx)
         });
-        panel.confirm_edit(window, cx).unwrap()
+        panel.confirm_edit(true, window, cx).unwrap()
     });
     assert_eq!(
         visible_entries_as_strings(&panel, 0..10, cx),
@@ -616,7 +616,7 @@ async fn test_editing_files(cx: &mut gpui::TestAppContext) {
             panel.filename_editor.update(cx, |editor, cx| {
                 editor.set_text("another-filename.txt", window, cx)
             });
-            panel.confirm_edit(window, cx).unwrap()
+            panel.confirm_edit(true, window, cx).unwrap()
         })
         .await
         .unwrap();
@@ -657,7 +657,7 @@ async fn test_editing_files(cx: &mut gpui::TestAppContext) {
 
     let confirm = panel.update_in(cx, |panel, window, cx| {
         panel.filename_editor.update(cx, |editor, cx| {
-            let file_name_selections = editor.selections.all::<usize>(cx);
+            let file_name_selections = editor.selections.all::<usize>(&editor.display_snapshot(cx));
             assert_eq!(
                 file_name_selections.len(),
                 1,
@@ -676,7 +676,7 @@ async fn test_editing_files(cx: &mut gpui::TestAppContext) {
 
             editor.set_text("a-different-filename.tar.gz", window, cx)
         });
-        panel.confirm_edit(window, cx).unwrap()
+        panel.confirm_edit(true, window, cx).unwrap()
     });
     assert_eq!(
         visible_entries_as_strings(&panel, 0..10, cx),
@@ -731,7 +731,7 @@ async fn test_editing_files(cx: &mut gpui::TestAppContext) {
 
     panel.update_in(cx, |panel, window, cx| {
             panel.filename_editor.update(cx, |editor, cx| {
-                let file_name_selections = editor.selections.all::<usize>(cx);
+                let file_name_selections = editor.selections.all::<usize>(&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, 0, "Should select the file name from the start");
@@ -765,7 +765,7 @@ async fn test_editing_files(cx: &mut gpui::TestAppContext) {
         panel
             .filename_editor
             .update(cx, |editor, cx| editor.set_text("new-dir", window, cx));
-        panel.confirm_edit(window, cx).unwrap()
+        panel.confirm_edit(true, window, cx).unwrap()
     });
     panel.update_in(cx, |panel, window, cx| {
         panel.select_next(&Default::default(), window, cx)
@@ -863,11 +863,11 @@ async fn test_editing_files(cx: &mut gpui::TestAppContext) {
         panel.filename_editor.update(cx, |editor, cx| {
             editor.set_text("", window, cx);
         });
-        assert!(panel.confirm_edit(window, cx).is_none());
+        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(window, cx).is_none());
+        assert!(panel.confirm_edit(true, window, cx).is_none());
         panel.cancel(&menu::Cancel, window, cx);
         panel.update_visible_entries(None, false, false, window, cx);
     });
@@ -986,7 +986,7 @@ async fn test_adding_directories_via_file(cx: &mut gpui::TestAppContext) {
         panel.filename_editor.update(cx, |editor, cx| {
             editor.set_text("/bdir1/dir2/the-new-filename", window, cx)
         });
-        panel.confirm_edit(window, cx).unwrap()
+        panel.confirm_edit(true, window, cx).unwrap()
     });
 
     assert_eq!(
@@ -1082,7 +1082,7 @@ async fn test_adding_directory_via_file(cx: &mut gpui::TestAppContext) {
         panel
             .filename_editor
             .update(cx, |editor, cx| editor.set_text("new_dir/", window, cx));
-        panel.confirm_edit(window, cx).unwrap()
+        panel.confirm_edit(true, window, cx).unwrap()
     });
 
     assert_eq!(
@@ -1115,7 +1115,7 @@ async fn test_adding_directory_via_file(cx: &mut gpui::TestAppContext) {
         panel
             .filename_editor
             .update(cx, |editor, cx| editor.set_text("new dir 2/", window, cx));
-        panel.confirm_edit(window, cx).unwrap()
+        panel.confirm_edit(true, window, cx).unwrap()
     });
     confirm.await.unwrap();
     cx.run_until_parked();
@@ -1140,7 +1140,7 @@ async fn test_adding_directory_via_file(cx: &mut gpui::TestAppContext) {
             panel
                 .filename_editor
                 .update(cx, |editor, cx| editor.set_text("new_dir_3\\", window, cx));
-            panel.confirm_edit(window, cx).unwrap()
+            panel.confirm_edit(true, window, cx).unwrap()
         });
         confirm.await.unwrap();
         cx.run_until_parked();
@@ -1214,7 +1214,7 @@ async fn test_copy_paste(cx: &mut gpui::TestAppContext) {
 
     panel.update_in(cx, |panel, window, cx| {
         panel.filename_editor.update(cx, |editor, cx| {
-            let file_name_selections = editor.selections.all::<usize>(cx);
+            let file_name_selections = editor.selections.all::<usize>(&editor.display_snapshot(cx));
             assert_eq!(
                 file_name_selections.len(),
                 1,
@@ -1232,7 +1232,7 @@ async fn test_copy_paste(cx: &mut gpui::TestAppContext) {
                 "Should select the file name disambiguation until the extension"
             );
         });
-        assert!(panel.confirm_edit(window, cx).is_none());
+        assert!(panel.confirm_edit(true, window, cx).is_none());
     });
 
     panel.update_in(cx, |panel, window, cx| {
@@ -1253,7 +1253,7 @@ async fn test_copy_paste(cx: &mut gpui::TestAppContext) {
     );
 
     panel.update_in(cx, |panel, window, cx| {
-        assert!(panel.confirm_edit(window, cx).is_none())
+        assert!(panel.confirm_edit(true, window, cx).is_none())
     });
 }
 
@@ -1672,7 +1672,7 @@ async fn test_copy_paste_directory(cx: &mut gpui::TestAppContext) {
         panel
             .filename_editor
             .update(cx, |editor, cx| editor.set_text("c", window, cx));
-        panel.confirm_edit(window, cx).unwrap()
+        panel.confirm_edit(true, window, cx).unwrap()
     });
     assert_eq!(
         visible_entries_as_strings(&panel, 0..50, cx),
@@ -2060,7 +2060,7 @@ async fn test_create_duplicate_items(cx: &mut gpui::TestAppContext) {
             .filename_editor
             .update(cx, |editor, cx| editor.set_text("test", window, cx));
         assert!(
-            panel.confirm_edit(window, cx).is_none(),
+            panel.confirm_edit(true, window, cx).is_none(),
             "Should not allow to confirm on conflicting new directory name"
         );
     });
@@ -2116,7 +2116,7 @@ async fn test_create_duplicate_items(cx: &mut gpui::TestAppContext) {
             .filename_editor
             .update(cx, |editor, cx| editor.set_text("first.rs", window, cx));
         assert!(
-            panel.confirm_edit(window, cx).is_none(),
+            panel.confirm_edit(true, window, cx).is_none(),
             "Should not allow to confirm on conflicting new file name"
         );
     });
@@ -2174,7 +2174,7 @@ async fn test_create_duplicate_items(cx: &mut gpui::TestAppContext) {
             .filename_editor
             .update(cx, |editor, cx| editor.set_text("second.rs", window, cx));
         assert!(
-            panel.confirm_edit(window, cx).is_none(),
+            panel.confirm_edit(true, window, cx).is_none(),
             "Should not allow to confirm on conflicting file rename"
         )
     });
@@ -3041,7 +3041,7 @@ async fn test_rename_root_of_worktree(cx: &mut gpui::TestAppContext) {
         panel
             .filename_editor
             .update(cx, |editor, cx| editor.set_text("new_root1", window, cx));
-        panel.confirm_edit(window, cx).unwrap()
+        panel.confirm_edit(true, window, cx).unwrap()
     });
     confirm.await.unwrap();
     cx.run_until_parked();
@@ -4173,7 +4173,7 @@ async fn test_creating_excluded_entries(cx: &mut gpui::TestAppContext) {
             panel.filename_editor.update(cx, |editor, cx| {
                 editor.set_text(excluded_file_path, window, cx)
             });
-            panel.confirm_edit(window, cx).unwrap()
+            panel.confirm_edit(true, window, cx).unwrap()
         })
         .await
         .unwrap();
@@ -4229,7 +4229,7 @@ async fn test_creating_excluded_entries(cx: &mut gpui::TestAppContext) {
             panel.filename_editor.update(cx, |editor, cx| {
                 editor.set_text(excluded_file_path, window, cx)
             });
-            panel.confirm_edit(window, cx).unwrap()
+            panel.confirm_edit(true, window, cx).unwrap()
         })
         .await
         .unwrap();
@@ -4273,7 +4273,7 @@ async fn test_creating_excluded_entries(cx: &mut gpui::TestAppContext) {
             panel.filename_editor.update(cx, |editor, cx| {
                 editor.set_text(excluded_dir_path, window, cx)
             });
-            panel.confirm_edit(window, cx).unwrap()
+            panel.confirm_edit(true, window, cx).unwrap()
         })
         .await
         .unwrap();
@@ -5694,7 +5694,7 @@ async fn test_create_entries_without_selection(cx: &mut gpui::TestAppContext) {
             panel.filename_editor.update(cx, |editor, cx| {
                 editor.set_text("hello_from_no_selections", window, cx)
             });
-            panel.confirm_edit(window, cx).unwrap()
+            panel.confirm_edit(true, window, cx).unwrap()
         })
         .await
         .unwrap();
@@ -5792,7 +5792,7 @@ async fn test_create_entries_without_selection_hide_root(cx: &mut gpui::TestAppC
         panel.filename_editor.update(cx, |editor, cx| {
             editor.set_text("new_file_at_root.txt", window, cx)
         });
-        panel.confirm_edit(window, cx).unwrap()
+        panel.confirm_edit(true, window, cx).unwrap()
     });
     confirm.await.unwrap();
     cx.run_until_parked();
@@ -5843,7 +5843,7 @@ async fn test_create_entries_without_selection_hide_root(cx: &mut gpui::TestAppC
         panel.filename_editor.update(cx, |editor, cx| {
             editor.set_text("new_dir_at_root", window, cx)
         });
-        panel.confirm_edit(window, cx).unwrap()
+        panel.confirm_edit(true, window, cx).unwrap()
     });
     confirm.await.unwrap();
     cx.run_until_parked();

crates/project_symbols/Cargo.toml 🔗

@@ -25,7 +25,6 @@ settings.workspace = true
 theme.workspace = true
 util.workspace = true
 workspace.workspace = true
-workspace-hack.workspace = true
 
 [dev-dependencies]
 editor = { workspace = true, features = ["test-support"] }

crates/prompt_store/Cargo.toml 🔗

@@ -32,4 +32,3 @@ serde_json.workspace = true
 text.workspace = true
 util.workspace = true
 uuid.workspace = true
-workspace-hack.workspace = true

crates/proto/Cargo.toml 🔗

@@ -20,7 +20,6 @@ doctest = false
 anyhow.workspace = true
 prost.workspace = true
 serde.workspace = true
-workspace-hack.workspace = true
 
 [build-dependencies]
 prost-build.workspace = true

crates/proto/proto/lsp.proto 🔗

@@ -465,6 +465,7 @@ message ResolveInlayHintResponse {
 
 message RefreshInlayHints {
     uint64 project_id = 1;
+    uint64 server_id = 2;
 }
 
 message CodeLens {
@@ -781,6 +782,7 @@ message TextEdit {
 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;
@@ -793,6 +795,7 @@ message LspQuery {
         GetDeclaration get_declaration = 11;
         GetTypeDefinition get_type_definition = 12;
         GetImplementation get_implementation = 13;
+        InlayHints inlay_hints = 14;
     }
 }
 
@@ -815,6 +818,7 @@ message LspResponse {
         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;
 }

crates/proto/src/proto.rs 🔗

@@ -517,6 +517,7 @@ lsp_messages!(
     (GetDeclaration, GetDeclarationResponse, true),
     (GetTypeDefinition, GetTypeDefinitionResponse, true),
     (GetImplementation, GetImplementationResponse, true),
+    (InlayHints, InlayHintsResponse, false),
 );
 
 entity_messages!(
@@ -847,6 +848,7 @@ impl LspQuery {
             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 => ("<unknown>", true),
         }
     }

crates/recent_projects/Cargo.toml 🔗

@@ -42,7 +42,6 @@ ui.workspace = true
 util.workspace = true
 workspace.workspace = true
 zed_actions.workspace = true
-workspace-hack.workspace = true
 indoc.workspace = true
 
 [target.'cfg(target_os = "windows")'.dependencies]

crates/recent_projects/src/recent_projects.rs 🔗

@@ -547,11 +547,7 @@ impl PickerDelegate for RecentProjectsDelegate {
         )
     }
 
-    fn render_footer(
-        &self,
-        window: &mut Window,
-        cx: &mut Context<Picker<Self>>,
-    ) -> Option<AnyElement> {
+    fn render_footer(&self, _: &mut Window, cx: &mut Context<Picker<Self>>) -> Option<AnyElement> {
         Some(
             h_flex()
                 .w_full()
@@ -567,7 +563,6 @@ impl PickerDelegate for RecentProjectsDelegate {
                                 from_existing_connection: false,
                                 create_new_window: false,
                             },
-                            window,
                             cx,
                         ))
                         .on_click(|_, window, cx| {
@@ -583,7 +578,7 @@ impl PickerDelegate for RecentProjectsDelegate {
                 )
                 .child(
                     Button::new("local", "Open Local Folder")
-                        .key_binding(KeyBinding::for_action(&workspace::Open, window, cx))
+                        .key_binding(KeyBinding::for_action(&workspace::Open, cx))
                         .on_click(|_, window, cx| {
                             window.dispatch_action(workspace::Open.boxed_clone(), cx)
                         }),

crates/remote/Cargo.toml 🔗

@@ -41,7 +41,6 @@ thiserror.workspace = true
 urlencoding.workspace = true
 util.workspace = true
 which.workspace = true
-workspace-hack.workspace = true
 
 
 [dev-dependencies]

crates/remote/src/transport/ssh.rs 🔗

@@ -34,7 +34,7 @@ use util::{
 
 pub(crate) struct SshRemoteConnection {
     socket: SshSocket,
-    master_process: Mutex<Option<Child>>,
+    master_process: Mutex<Option<MasterProcess>>,
     remote_binary_path: Option<Arc<RelPath>>,
     ssh_platform: RemotePlatform,
     ssh_path_style: PathStyle,
@@ -80,6 +80,129 @@ struct SshSocket {
     _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<String>,
+        socket_path: &std::path::Path,
+        url: &str,
+    ) -> Result<Self> {
+        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<String>,
+        url: &str,
+    ) -> Result<Self> {
+        // 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<Child> for MasterProcess {
+    fn as_ref(&self) -> &Child {
+        &self.process
+    }
+}
+
+impl AsMut<Child> for MasterProcess {
+    fn as_mut(&mut self) -> &mut Child {
+        &mut self.process
+    }
+}
+
 macro_rules! shell_script {
     ($fmt:expr, $($name:ident = $arg:expr),+ $(,)?) => {{
         format!(
@@ -97,8 +220,8 @@ impl RemoteConnection for SshRemoteConnection {
         let Some(mut process) = self.master_process.lock().take() else {
             return Ok(());
         };
-        process.kill().ok();
-        process.status().await?;
+        process.as_mut().kill().ok();
+        process.as_mut().status().await?;
         Ok(())
     }
 
@@ -170,35 +293,44 @@ impl RemoteConnection for SshRemoteConnection {
         dest_path: RemotePathBuf,
         cx: &App,
     ) -> Task<Result<()>> {
-        let mut command = util::command::new_smol_command("scp");
-        let output = 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(),
-            )
-            .arg("-C")
-            .arg("-r")
-            .arg(&src_path)
-            .arg(format!(
-                "{}:{}",
-                self.socket.connection_options.scp_url(),
-                dest_path
-            ))
-            .output();
+        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 {
-            let output = output.await?;
+            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 {} {}\n", src_path.display(), dest_path_str);
+                    stdin.write_all(sftp_batch.as_bytes()).await?;
+                    drop(stdin);
+                }
+
+                let output = child.output().await?;
+                anyhow::ensure!(
+                    output.status.success(),
+                    "failed to upload directory via SFTP {} -> {}: {}",
+                    src_path_display,
+                    dest_path_str,
+                    String::from_utf8_lossy(&output.stderr)
+                );
+
+                return Ok(());
+            }
+
+            log::debug!("using SCP for directory upload");
+            let output = scp_command.output().await?;
 
             anyhow::ensure!(
                 output.status.success(),
-                "failed to upload directory {} -> {}: {}",
-                src_path.display(),
-                dest_path.to_string(),
+                "failed to upload directory via SCP {} -> {}: {}",
+                src_path_display,
+                dest_path_str,
                 String::from_utf8_lossy(&output.stderr)
             );
 
@@ -293,45 +425,25 @@ impl SshRemoteConnection {
         #[cfg(not(target_os = "windows"))]
         let socket_path = temp_dir.path().join("ssh.sock");
 
-        let mut master_process = {
-            #[cfg(not(target_os = "windows"))]
-            let args = [
-                "-N",
-                "-o",
-                "ControlPersist=no",
-                "-o",
-                "ControlMaster=yes",
-                "-o",
-            ];
-            // 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
-            #[cfg(target_os = "windows")]
-            let args = ["-N"];
-            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(connection_options.additional_args())
-                .args(args);
-            #[cfg(not(target_os = "windows"))]
-            master_process.arg(format!("ControlPath={}", socket_path.display()));
-            master_process.arg(&url).spawn()?
-        };
-        // Wait for this ssh process to close its stdout, indicating that authentication
-        // has completed.
-        let mut stdout = master_process.stdout.take().unwrap();
-        let mut output = Vec::new();
+        #[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.kill().ok();
+                        master_process.as_mut().kill().ok();
                         anyhow::bail!("SSH connection canceled")
                     }
                     AskPassResult::Timedout => {
@@ -339,7 +451,7 @@ impl SshRemoteConnection {
                     }
                 }
             }
-            _ = stdout.read_to_end(&mut output).fuse() => {
+            _ = master_process.wait_connected().fuse() => {
                 anyhow::Ok(())
             }
         };
@@ -348,9 +460,10 @@ impl SshRemoteConnection {
             return Err(e.context("Failed to connect to host"));
         }
 
-        if master_process.try_status()?.is_some() {
+        if master_process.as_mut().try_status()?.is_some() {
+            let mut output = Vec::new();
             output.clear();
-            let mut stderr = master_process.stderr.take().unwrap();
+            let mut stderr = master_process.as_mut().stderr.take().unwrap();
             stderr.read_to_end(&mut output).await?;
 
             let error_message = format!(
@@ -643,36 +756,92 @@ impl SshRemoteConnection {
         Ok(())
     }
 
-    async fn upload_file(&self, src_path: &Path, dest_path: &RelPath) -> Result<()> {
-        log::debug!("uploading file {:?} to {:?}", src_path, dest_path);
+    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");
-        let output = 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(),
-            )
-            .arg(src_path)
-            .arg(format!(
-                "{}:{}",
-                self.socket.connection_options.scp_url(),
-                dest_path.display(self.path_style())
-            ))
-            .output()
-            .await?;
+        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
+    }
 
-        anyhow::ensure!(
-            output.status.success(),
-            "failed to upload file {} -> {}: {}",
-            src_path.display(),
-            dest_path.display(self.path_style()),
-            String::from_utf8_lossy(&output.stderr)
+    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(),
         );
-        Ok(())
+        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 dest_path_str = dest_path.display(self.path_style());
+
+        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 {} {}\n", src_path.display(), dest_path_str);
+
+            let mut child = command.spawn()?;
+            if let Some(mut stdin) = child.stdin.take() {
+                use futures::AsyncWriteExt;
+                stdin.write_all(sftp_batch.as_bytes()).await?;
+                drop(stdin);
+            }
+
+            let output = child.output().await?;
+            anyhow::ensure!(
+                output.status.success(),
+                "failed to upload file via SFTP {} -> {}: {}",
+                src_path.display(),
+                dest_path_str,
+                String::from_utf8_lossy(&output.stderr)
+            );
+
+            Ok(())
+        } else {
+            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?;
+
+            anyhow::ensure!(
+                output.status.success(),
+                "failed to upload file via SCP {} -> {}: {}",
+                src_path.display(),
+                dest_path_str,
+                String::from_utf8_lossy(&output.stderr)
+            );
+
+            Ok(())
+        }
+    }
+
+    async fn is_sftp_available() -> bool {
+        which::which("sftp").is_ok()
     }
 }
 

crates/remote/src/transport/wsl.rs 🔗

@@ -407,7 +407,7 @@ impl RemoteConnection for WslRemoteConnection {
                     anyhow!(
                         "failed to upload directory {} -> {}: {}",
                         src_path.display(),
-                        dest_path.to_string(),
+                        dest_path,
                         e
                     )
                 })?;

crates/remote_server/Cargo.toml 🔗

@@ -75,8 +75,7 @@ minidumper.workspace = true
 
 [dev-dependencies]
 action_log.workspace = true
-assistant_tool.workspace = true
-assistant_tools.workspace = true
+agent.workspace = true
 client = { workspace = true, features = ["test-support"] }
 clock = { workspace = true, features = ["test-support"] }
 collections.workspace = true

crates/remote_server/src/headless_project.rs 🔗

@@ -94,7 +94,7 @@ impl HeadlessProject {
             store
         });
 
-        let environment = cx.new(|_| ProjectEnvironment::new(None));
+        let environment = cx.new(|cx| ProjectEnvironment::new(None, cx));
         let manifest_tree = ManifestTree::new(worktree_store.clone(), cx);
         let toolchain_store = cx.new(|cx| {
             ToolchainStore::local(
@@ -102,6 +102,7 @@ impl HeadlessProject {
                 worktree_store.clone(),
                 environment.clone(),
                 manifest_tree.clone(),
+                fs.clone(),
                 cx,
             )
         });

crates/remote_server/src/remote_editing_tests.rs 🔗

@@ -2,12 +2,11 @@
 /// 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 assistant_tool::{Tool as _, ToolResultContent};
-use assistant_tools::{ReadFileTool, ReadFileToolInput};
+use agent::{AgentTool, ReadFileTool, ReadFileToolInput, ToolCallEventStream};
 use client::{Client, UserStore};
 use clock::FakeSystemClock;
 use collections::{HashMap, HashSet};
-use language_model::{LanguageModelRequest, fake_provider::FakeLanguageModel};
+use language_model::LanguageModelToolResultContent;
 
 use extension::ExtensionHostProxy;
 use fs::{FakeFs, Fs};
@@ -1721,47 +1720,26 @@ async fn test_remote_agent_fs_tool_calls(cx: &mut TestAppContext, server_cx: &mu
         .unwrap();
 
     let action_log = cx.new(|_| action_log::ActionLog::new(project.clone()));
-    let model = Arc::new(FakeLanguageModel::default());
-    let request = Arc::new(LanguageModelRequest::default());
 
     let input = ReadFileToolInput {
         path: "project/b.txt".into(),
         start_line: None,
         end_line: None,
     };
-    let exists_result = cx.update(|cx| {
-        ReadFileTool::run(
-            Arc::new(ReadFileTool),
-            serde_json::to_value(input).unwrap(),
-            request.clone(),
-            project.clone(),
-            action_log.clone(),
-            model.clone(),
-            None,
-            cx,
-        )
-    });
-    let output = exists_result.output.await.unwrap().content;
-    assert_eq!(output, ToolResultContent::Text("B".to_string()));
+    let read_tool = Arc::new(ReadFileTool::new(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| {
-        ReadFileTool::run(
-            Arc::new(ReadFileTool),
-            serde_json::to_value(input).unwrap(),
-            request.clone(),
-            project.clone(),
-            action_log.clone(),
-            model.clone(),
-            None,
-            cx,
-        )
-    });
-    does_not_exist_result.output.await.unwrap_err();
+    let does_not_exist_result = cx.update(|cx| read_tool.run(input, event_stream, cx));
+    does_not_exist_result.await.unwrap_err();
 }
 
 #[gpui::test]

crates/remote_server/src/unix.rs 🔗

@@ -103,7 +103,9 @@ fn init_logging_server(log_file_path: PathBuf) -> Result<Receiver<Vec<u8>>> {
         buffer: Vec::new(),
     });
 
-    env_logger::Builder::from_default_env()
+    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);

crates/repl/Cargo.toml 🔗

@@ -16,7 +16,7 @@ doctest = false
 alacritty_terminal.workspace = true
 anyhow.workspace = true
 async-dispatcher.workspace = true
-async-tungstenite = { workspace = true, features = ["tokio", "tokio-rustls-manual-roots"] }
+async-tungstenite = { workspace = true, features = ["tokio", "tokio-rustls-manual-roots", "tokio-runtime"] }
 base64.workspace = true
 client.workspace = true
 collections.workspace = true
@@ -51,7 +51,6 @@ util.workspace = true
 uuid.workspace = true
 workspace.workspace = true
 picker.workspace = true
-workspace-hack.workspace = true
 
 [dev-dependencies]
 editor = { workspace = true, features = ["test-support"] }

crates/repl/src/notebook/notebook_ui.rs 🔗

@@ -326,7 +326,7 @@ impl NotebookEditor {
                                     cx,
                                 )
                                 .tooltip(move |window, cx| {
-                                    Tooltip::for_action("Execute all cells", &RunAll, window, cx)
+                                    Tooltip::for_action("Execute all cells", &RunAll, cx)
                                 })
                                 .on_click(|_, window, cx| {
                                     window.dispatch_action(Box::new(RunAll), cx);
@@ -341,12 +341,7 @@ impl NotebookEditor {
                                 )
                                 .disabled(!has_outputs)
                                 .tooltip(move |window, cx| {
-                                    Tooltip::for_action(
-                                        "Clear all outputs",
-                                        &ClearOutputs,
-                                        window,
-                                        cx,
-                                    )
+                                    Tooltip::for_action("Clear all outputs", &ClearOutputs, cx)
                                 })
                                 .on_click(|_, window, cx| {
                                     window.dispatch_action(Box::new(ClearOutputs), cx);
@@ -363,7 +358,7 @@ impl NotebookEditor {
                                     cx,
                                 )
                                 .tooltip(move |window, cx| {
-                                    Tooltip::for_action("Move cell up", &MoveCellUp, window, cx)
+                                    Tooltip::for_action("Move cell up", &MoveCellUp, cx)
                                 })
                                 .on_click(|_, window, cx| {
                                     window.dispatch_action(Box::new(MoveCellUp), cx);
@@ -377,7 +372,7 @@ impl NotebookEditor {
                                     cx,
                                 )
                                 .tooltip(move |window, cx| {
-                                    Tooltip::for_action("Move cell down", &MoveCellDown, window, cx)
+                                    Tooltip::for_action("Move cell down", &MoveCellDown, cx)
                                 })
                                 .on_click(|_, window, cx| {
                                     window.dispatch_action(Box::new(MoveCellDown), cx);
@@ -394,12 +389,7 @@ impl NotebookEditor {
                                     cx,
                                 )
                                 .tooltip(move |window, cx| {
-                                    Tooltip::for_action(
-                                        "Add markdown block",
-                                        &AddMarkdownBlock,
-                                        window,
-                                        cx,
-                                    )
+                                    Tooltip::for_action("Add markdown block", &AddMarkdownBlock, cx)
                                 })
                                 .on_click(|_, window, cx| {
                                     window.dispatch_action(Box::new(AddMarkdownBlock), cx);
@@ -413,7 +403,7 @@ impl NotebookEditor {
                                     cx,
                                 )
                                 .tooltip(move |window, cx| {
-                                    Tooltip::for_action("Add code block", &AddCodeBlock, window, cx)
+                                    Tooltip::for_action("Add code block", &AddCodeBlock, cx)
                                 })
                                 .on_click(|_, window, cx| {
                                     window.dispatch_action(Box::new(AddCodeBlock), cx);
@@ -709,11 +699,13 @@ impl Item for NotebookEditor {
         _workspace_id: Option<workspace::WorkspaceId>,
         window: &mut Window,
         cx: &mut Context<Self>,
-    ) -> Option<Entity<Self>>
+    ) -> Task<Option<Entity<Self>>>
     where
         Self: Sized,
     {
-        Some(cx.new(|cx| Self::new(self.project.clone(), self.notebook_item.clone(), window, cx)))
+        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 {

crates/repl/src/outputs/image.rs 🔗

@@ -51,6 +51,7 @@ impl ImageView {
             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:?}");
             }

crates/repl/src/repl_editor.rs 🔗

@@ -85,7 +85,11 @@ pub fn run(
 
     let editor = editor.upgrade().context("editor was dropped")?;
     let selected_range = editor
-        .update(cx, |editor, cx| editor.selections.newest_adjusted(cx))
+        .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 {
@@ -473,7 +477,9 @@ fn language_supported(language: &Arc<Language>, cx: &mut App) -> bool {
 fn get_language(editor: WeakEntity<Editor>, cx: &mut App) -> Option<Arc<Language>> {
     editor
         .update(cx, |editor, cx| {
-            let selection = editor.selections.newest::<usize>(cx);
+            let selection = editor
+                .selections
+                .newest::<usize>(&editor.display_snapshot(cx));
             let buffer = editor.buffer().read(cx).snapshot(cx);
             buffer.language_at(selection.head()).cloned()
         })

crates/repl/src/repl_sessions_ui.rs 🔗

@@ -197,7 +197,7 @@ impl Item for ReplSessionsPage {
 }
 
 impl Render for ReplSessionsPage {
-    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
         let store = ReplStore::global(cx);
 
         let (kernel_specifications, sessions) = store.update(cx, |store, _cx| {
@@ -241,7 +241,7 @@ impl Render for ReplSessionsPage {
             return ReplSessionsContainer::new("No Jupyter Kernel Sessions").child(
                 v_flex()
                     .child(Label::new(instructions))
-                    .children(KeyBinding::for_action(&Run, window, cx)),
+                    .child(KeyBinding::for_action(&Run, cx)),
             );
         }
 

crates/reqwest_client/Cargo.toml 🔗

@@ -26,7 +26,6 @@ log.workspace = true
 tokio = { workspace = true, features = ["rt", "rt-multi-thread"] }
 regex.workspace = true
 reqwest.workspace = true
-workspace-hack.workspace = true
 
 [dev-dependencies]
 gpui.workspace = true

crates/rich_text/Cargo.toml 🔗

@@ -27,4 +27,3 @@ pulldown-cmark.workspace = true
 theme.workspace = true
 ui.workspace = true
 util.workspace = true
-workspace-hack.workspace = true

crates/rope/Cargo.toml 🔗

@@ -19,7 +19,6 @@ smallvec.workspace = true
 sum_tree.workspace = true
 unicode-segmentation.workspace = true
 util.workspace = true
-workspace-hack.workspace = true
 
 [dev-dependencies]
 ctor.workspace = true

crates/rope/benches/rope_benchmark.rs 🔗

@@ -9,11 +9,21 @@ use rope::{Point, Rope};
 use sum_tree::Bias;
 use util::RandomCharIter;
 
-/// Generate a random text of the given length using the provided RNG.
+/// Returns a biased random string whose UTF-8 length is close to but no more than `len` bytes.
 ///
-/// *Note*: The length is in *characters*, not bytes.
-fn generate_random_text(rng: &mut StdRng, text_len: usize) -> String {
-    RandomCharIter::new(rng).take(text_len).collect()
+/// 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 {

crates/rope/src/chunk.rs 🔗

@@ -5,29 +5,36 @@ use sum_tree::Bias;
 use unicode_segmentation::GraphemeCursor;
 use util::debug_panic;
 
-pub(crate) const MIN_BASE: usize = if cfg!(test) { 6 } else { 64 };
-pub(crate) const MAX_BASE: usize = MIN_BASE * 2;
+#[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: u128,
+    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: u128,
+    chars_utf16: Bitmap,
     /// If bit[i] is set, then the character at index i is an ascii newline.
-    newlines: u128,
+    newlines: Bitmap,
     /// If bit[i] is set, then the character at index i is an ascii tab.
-    pub tabs: u128,
+    tabs: Bitmap,
     pub text: ArrayString<MAX_BASE>,
 }
 
 impl Chunk {
+    pub const MASK_BITS: usize = Bitmap::BITS as usize;
+
     #[inline(always)]
     pub fn new(text: &str) -> Self {
         let mut this = Chunk::default();
@@ -41,9 +48,9 @@ impl Chunk {
             let ix = self.text.len() + char_ix;
             self.chars |= 1 << ix;
             self.chars_utf16 |= 1 << ix;
-            self.chars_utf16 |= (c.len_utf16() as u128) << ix;
-            self.newlines |= ((c == '\n') as u128) << ix;
-            self.tabs |= ((c == '\t') as u128) << 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);
     }
@@ -79,17 +86,85 @@ impl Chunk {
     }
 
     #[inline(always)]
-    pub fn chars(&self) -> u128 {
+    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 {
+        #[inline]
+        pub(crate) const fn is_utf8_char_boundary(u8: u8) -> bool {
+            // This is bit magic equivalent to: b < 128 || b >= 192
+            (u8 as i8) >= -0x40
+        }
+
+        if index >= self.text.len() {
+            self.text.len()
+        } else {
+            let mut i = index;
+            while i > 0 {
+                if 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) {
+        if self.is_char_boundary(offset) {
+            return;
+        }
+        panic_char_boundary(self, offset);
+
+        #[cold]
+        #[inline(never)]
+        fn panic_char_boundary(chunk: &Chunk, offset: usize) {
+            if offset > chunk.text.len() {
+                panic!(
+                    "byte index {} is out of bounds of `{:?}` (length: {})",
+                    offset,
+                    chunk.text,
+                    chunk.text.len()
+                );
+            }
+            // find the character
+            let char_start = chunk.floor_char_boundary(offset);
+            // `char_start` must be less than len and a char boundary
+            let ch = chunk
+                .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,
+            );
+        }
+    }
 }
 
 #[derive(Clone, Copy, Debug)]
 pub struct ChunkSlice<'a> {
-    chars: u128,
-    chars_utf16: u128,
-    newlines: u128,
-    tabs: u128,
+    chars: Bitmap,
+    chars_utf16: Bitmap,
+    newlines: Bitmap,
+    tabs: Bitmap,
     text: &'a str,
 }
 
@@ -112,8 +187,8 @@ impl<'a> ChunkSlice<'a> {
     }
 
     #[inline(always)]
-    pub fn is_char_boundary(self, offset: usize) -> bool {
-        self.text.is_char_boundary(offset)
+    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)]
@@ -129,7 +204,7 @@ impl<'a> ChunkSlice<'a> {
             };
             (left, right)
         } else {
-            let mask = (1u128 << mid) - 1;
+            let mask = ((1 as Bitmap) << mid) - 1;
             let (left_text, right_text) = self.text.split_at(mid);
             let left = ChunkSlice {
                 chars: self.chars & mask,
@@ -151,17 +226,9 @@ impl<'a> ChunkSlice<'a> {
 
     #[inline(always)]
     pub fn slice(self, range: Range<usize>) -> Self {
-        let mask = if range.end == MAX_BASE {
-            u128::MAX
-        } else {
-            debug_assert!(
-                self.is_char_boundary(range.end),
-                "Invalid range end {} in {:?}",
-                range.end,
-                self
-            );
-            (1u128 << range.end) - 1
-        };
+        let mask = (1 as Bitmap)
+            .unbounded_shl(range.end as u32)
+            .wrapping_sub(1);
         if range.start == MAX_BASE {
             Self {
                 chars: 0,
@@ -171,12 +238,8 @@ impl<'a> ChunkSlice<'a> {
                 text: "",
             }
         } else {
-            debug_assert!(
-                self.is_char_boundary(range.start),
-                "Invalid range start {} in {:?}",
-                range.start,
-                self
-            );
+            self.assert_char_boundary(range.start);
+            self.assert_char_boundary(range.end);
             Self {
                 chars: (self.chars & mask) >> range.start,
                 chars_utf16: (self.chars_utf16 & mask) >> range.start,
@@ -220,7 +283,7 @@ impl<'a> ChunkSlice<'a> {
     #[inline(always)]
     pub fn lines(&self) -> Point {
         let row = self.newlines.count_ones();
-        let column = self.newlines.leading_zeros() - (u128::BITS - self.text.len() as u32);
+        let column = self.newlines.leading_zeros() - (Bitmap::BITS - self.text.len() as u32);
         Point::new(row, column)
     }
 
@@ -230,7 +293,7 @@ impl<'a> ChunkSlice<'a> {
         if self.newlines == 0 {
             self.chars.count_ones()
         } else {
-            let mask = (1u128 << self.newlines.trailing_zeros()) - 1;
+            let mask = ((1 as Bitmap) << self.newlines.trailing_zeros()) - 1;
             (self.chars & mask).count_ones()
         }
     }
@@ -241,7 +304,7 @@ impl<'a> ChunkSlice<'a> {
         if self.newlines == 0 {
             self.chars.count_ones()
         } else {
-            let mask = !(u128::MAX >> self.newlines.leading_zeros());
+            let mask = !(Bitmap::MAX >> self.newlines.leading_zeros());
             (self.chars & mask).count_ones()
         }
     }
@@ -252,7 +315,7 @@ impl<'a> ChunkSlice<'a> {
         if self.newlines == 0 {
             self.chars_utf16.count_ones()
         } else {
-            let mask = !(u128::MAX >> self.newlines.leading_zeros());
+            let mask = !(Bitmap::MAX >> self.newlines.leading_zeros());
             (self.chars_utf16 & mask).count_ones()
         }
     }
@@ -295,13 +358,9 @@ impl<'a> ChunkSlice<'a> {
 
     #[inline(always)]
     pub fn offset_to_point(&self, offset: usize) -> Point {
-        let mask = if offset == MAX_BASE {
-            u128::MAX
-        } else {
-            (1u128 << offset) - 1
-        };
+        let mask = (1 as Bitmap).unbounded_shl(offset as u32).wrapping_sub(1);
         let row = (self.newlines & mask).count_ones();
-        let newline_ix = u128::BITS - (self.newlines & mask).leading_zeros();
+        let newline_ix = Bitmap::BITS - (self.newlines & mask).leading_zeros();
         let column = (offset - newline_ix as usize) as u32;
         Point::new(row, column)
     }
@@ -330,13 +389,81 @@ impl<'a> ChunkSlice<'a> {
         }
     }
 
+    #[track_caller]
     #[inline(always)]
-    pub fn offset_to_offset_utf16(&self, offset: usize) -> OffsetUtf16 {
-        let mask = if offset == MAX_BASE {
-            u128::MAX
+    pub fn assert_char_boundary(&self, offset: usize) {
+        if self.is_char_boundary(offset) {
+            return;
+        }
+        panic_char_boundary(self, offset);
+
+        #[cold]
+        #[inline(never)]
+        fn panic_char_boundary(chunk: &ChunkSlice, offset: usize) {
+            if offset > chunk.text.len() {
+                panic!(
+                    "byte index {} is out of bounds of `{:?}` (length: {})",
+                    offset,
+                    chunk.text,
+                    chunk.text.len()
+                );
+            }
+            // find the character
+            let char_start = chunk.floor_char_boundary(offset);
+            // `char_start` must be less than len and a char boundary
+            let ch = chunk
+                .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,
+            );
+        }
+    }
+
+    pub fn floor_char_boundary(&self, index: usize) -> usize {
+        #[inline]
+        pub(crate) const fn is_utf8_char_boundary(u8: u8) -> bool {
+            // This is bit magic equivalent to: b < 128 || b >= 192
+            (u8 as i8) >= -0x40
+        }
+
+        if index >= self.text.len() {
+            self.text.len()
         } else {
-            (1u128 << offset) - 1
-        };
+            let mut i = index;
+            while i > 0 {
+                if is_utf8_char_boundary(self.text.as_bytes()[i]) {
+                    break;
+                }
+                i -= 1;
+            }
+
+            i
+        }
+    }
+
+    #[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)
     }
 
@@ -345,7 +472,11 @@ impl<'a> ChunkSlice<'a> {
         if target.0 == 0 {
             0
         } else {
-            let ix = nth_set_bit(self.chars_utf16, target.0) + 1;
+            #[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 {
@@ -360,13 +491,9 @@ impl<'a> ChunkSlice<'a> {
 
     #[inline(always)]
     pub fn offset_to_point_utf16(&self, offset: usize) -> PointUtf16 {
-        let mask = if offset == MAX_BASE {
-            u128::MAX
-        } else {
-            (1u128 << offset) - 1
-        };
+        let mask = (1 as Bitmap).unbounded_shl(offset as u32).wrapping_sub(1);
         let row = (self.newlines & mask).count_ones();
-        let newline_ix = u128::BITS - (self.newlines & mask).leading_zeros();
+        let newline_ix = Bitmap::BITS - (self.newlines & mask).leading_zeros();
         let column = if newline_ix as usize == MAX_BASE {
             0
         } else {
@@ -520,7 +647,11 @@ impl<'a> ChunkSlice<'a> {
     #[inline(always)]
     fn offset_range_for_row(&self, row: u32) -> Range<usize> {
         let row_start = if row > 0 {
-            nth_set_bit(self.newlines, row as usize) + 1
+            #[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
         };
@@ -545,8 +676,8 @@ impl<'a> ChunkSlice<'a> {
 }
 
 pub struct Tabs {
-    tabs: u128,
-    chars: u128,
+    tabs: Bitmap,
+    chars: Bitmap,
 }
 
 #[derive(Debug, PartialEq, Eq)]
@@ -647,8 +778,8 @@ mod tests {
         // Verify Chunk::chars() bitmap
         let expected_chars = char_offsets(&text)
             .into_iter()
-            .inspect(|i| assert!(*i < 128))
-            .fold(0u128, |acc, i| acc | (1 << i));
+            .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 {

crates/rope/src/rope.rs 🔗

@@ -4,7 +4,6 @@ mod point;
 mod point_utf16;
 mod unclipped;
 
-use chunk::Chunk;
 use rayon::iter::{IntoParallelIterator, ParallelIterator as _};
 use smallvec::SmallVec;
 use std::{
@@ -14,12 +13,14 @@ use std::{
 };
 use sum_tree::{Bias, Dimension, Dimensions, SumTree};
 
-pub use chunk::ChunkSlice;
+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<Chunk>,
@@ -41,15 +42,34 @@ impl Rope {
         if self.chunks.is_empty() {
             return offset == 0;
         }
-        let mut cursor = self.chunks.cursor::<usize>(());
-        cursor.seek(&offset, Bias::Left);
-        let chunk_offset = offset - cursor.start();
-        cursor
-            .item()
-            .map(|chunk| chunk.text.is_char_boundary(chunk_offset))
+        let (start, _, item) = self.chunks.find::<usize, _>((), &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) {
+        if self.chunks.is_empty() && offset == 0 {
+            return;
+        }
+        let (start, _, item) = self.chunks.find::<usize, _>((), &offset, Bias::Left);
+        match item {
+            Some(chunk) => {
+                let chunk_offset = offset - start;
+                chunk.assert_char_boundary(chunk_offset);
+            }
+            None => {
+                panic!(
+                    "byte index {} is out of bounds of rope (length: {})",
+                    offset,
+                    self.len()
+                );
+            }
+        }
+    }
+
     pub fn floor_char_boundary(&self, index: usize) -> usize {
         if index >= self.len() {
             self.len()
@@ -60,10 +80,9 @@ impl Rope {
                 (u8 as i8) >= -0x40
             }
 
-            let mut cursor = self.chunks.cursor::<usize>(());
-            cursor.seek(&index, Bias::Left);
-            let chunk_offset = index - cursor.start();
-            let lower_idx = cursor.item().map(|chunk| {
+            let (start, _, item) = self.chunks.find::<usize, _>((), &index, Bias::Left);
+            let chunk_offset = index - start;
+            let lower_idx = item.map(|chunk| {
                 let lower_bound = chunk_offset.saturating_sub(3);
                 chunk
                     .text
@@ -78,7 +97,7 @@ impl Rope {
                     })
                     .unwrap_or(chunk.text.len())
             });
-            lower_idx.map_or_else(|| self.len(), |idx| cursor.start() + idx)
+            lower_idx.map_or_else(|| self.len(), |idx| start + idx)
         }
     }
 
@@ -92,10 +111,9 @@ impl Rope {
                 (u8 as i8) >= -0x40
             }
 
-            let mut cursor = self.chunks.cursor::<usize>(());
-            cursor.seek(&index, Bias::Left);
-            let chunk_offset = index - cursor.start();
-            let upper_idx = cursor.item().map(|chunk| {
+            let (start, _, item) = self.chunks.find::<usize, _>((), &index, Bias::Left);
+            let chunk_offset = index - start;
+            let upper_idx = item.map(|chunk| {
                 let upper_bound = Ord::min(chunk_offset + 4, chunk.text.len());
                 chunk.text.as_bytes()[chunk_offset..upper_bound]
                     .iter()
@@ -103,7 +121,7 @@ impl Rope {
                     .map_or(upper_bound, |pos| pos + chunk_offset)
             });
 
-            upper_idx.map_or_else(|| self.len(), |idx| cursor.start() + idx)
+            upper_idx.map_or_else(|| self.len(), |idx| start + idx)
         }
     }
 
@@ -356,11 +374,12 @@ impl Rope {
         if offset >= self.summary().len {
             return self.summary().len_utf16;
         }
-        let mut cursor = self.chunks.cursor::<Dimensions<usize, OffsetUtf16>>(());
-        cursor.seek(&offset, Bias::Left);
-        let overshoot = offset - cursor.start().0;
-        cursor.start().1
-            + cursor.item().map_or(Default::default(), |chunk| {
+        let (start, _, item) =
+            self.chunks
+                .find::<Dimensions<usize, OffsetUtf16>, _>((), &offset, Bias::Left);
+        let overshoot = offset - start.0;
+        start.1
+            + item.map_or(Default::default(), |chunk| {
                 chunk.as_slice().offset_to_offset_utf16(overshoot)
             })
     }
@@ -369,11 +388,12 @@ impl Rope {
         if offset >= self.summary().len_utf16 {
             return self.summary().len;
         }
-        let mut cursor = self.chunks.cursor::<Dimensions<OffsetUtf16, usize>>(());
-        cursor.seek(&offset, Bias::Left);
-        let overshoot = offset - cursor.start().0;
-        cursor.start().1
-            + cursor.item().map_or(Default::default(), |chunk| {
+        let (start, _, item) =
+            self.chunks
+                .find::<Dimensions<OffsetUtf16, usize>, _>((), &offset, Bias::Left);
+        let overshoot = offset - start.0;
+        start.1
+            + item.map_or(Default::default(), |chunk| {
                 chunk.as_slice().offset_utf16_to_offset(overshoot)
             })
     }
@@ -382,11 +402,12 @@ impl Rope {
         if offset >= self.summary().len {
             return self.summary().lines;
         }
-        let mut cursor = self.chunks.cursor::<Dimensions<usize, Point>>(());
-        cursor.seek(&offset, Bias::Left);
-        let overshoot = offset - cursor.start().0;
-        cursor.start().1
-            + cursor.item().map_or(Point::zero(), |chunk| {
+        let (start, _, item) =
+            self.chunks
+                .find::<Dimensions<usize, Point>, _>((), &offset, Bias::Left);
+        let overshoot = offset - start.0;
+        start.1
+            + item.map_or(Point::zero(), |chunk| {
                 chunk.as_slice().offset_to_point(overshoot)
             })
     }
@@ -395,11 +416,12 @@ impl Rope {
         if offset >= self.summary().len {
             return self.summary().lines_utf16();
         }
-        let mut cursor = self.chunks.cursor::<Dimensions<usize, PointUtf16>>(());
-        cursor.seek(&offset, Bias::Left);
-        let overshoot = offset - cursor.start().0;
-        cursor.start().1
-            + cursor.item().map_or(PointUtf16::zero(), |chunk| {
+        let (start, _, item) =
+            self.chunks
+                .find::<Dimensions<usize, PointUtf16>, _>((), &offset, Bias::Left);
+        let overshoot = offset - start.0;
+        start.1
+            + item.map_or(PointUtf16::zero(), |chunk| {
                 chunk.as_slice().offset_to_point_utf16(overshoot)
             })
     }
@@ -408,12 +430,28 @@ impl Rope {
         if point >= self.summary().lines {
             return self.summary().lines_utf16();
         }
-        let mut cursor = self.chunks.cursor::<Dimensions<Point, PointUtf16>>(());
+        let (start, _, item) =
+            self.chunks
+                .find::<Dimensions<Point, PointUtf16>, _>((), &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::<Dimensions<PointUtf16, Point>>(());
         cursor.seek(&point, Bias::Left);
         let overshoot = point - cursor.start().0;
         cursor.start().1
-            + cursor.item().map_or(PointUtf16::zero(), |chunk| {
-                chunk.as_slice().point_to_point_utf16(overshoot)
+            + cursor.item().map_or(Point::zero(), |chunk| {
+                chunk
+                    .as_slice()
+                    .offset_to_point(chunk.as_slice().point_utf16_to_offset(overshoot, false))
             })
     }
 
@@ -421,19 +459,34 @@ impl Rope {
         if point >= self.summary().lines {
             return self.summary().len;
         }
-        let mut cursor = self.chunks.cursor::<Dimensions<Point, usize>>(());
+        let (start, _, item) =
+            self.chunks
+                .find::<Dimensions<Point, usize>, _>((), &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::<Dimensions<Point, OffsetUtf16>>(());
         cursor.seek(&point, Bias::Left);
         let overshoot = point - cursor.start().0;
         cursor.start().1
-            + cursor
-                .item()
-                .map_or(0, |chunk| chunk.as_slice().point_to_offset(overshoot))
+            + 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<PointUtf16>) -> usize {
         self.point_utf16_to_offset_impl(point.0, true)
     }
@@ -442,12 +495,30 @@ impl Rope {
         if point >= self.summary().lines_utf16() {
             return self.summary().len;
         }
-        let mut cursor = self.chunks.cursor::<Dimensions<PointUtf16, usize>>(());
+        let (start, _, item) =
+            self.chunks
+                .find::<Dimensions<PointUtf16, usize>, _>((), &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::<Dimensions<PointUtf16, OffsetUtf16>>(());
         cursor.seek(&point, Bias::Left);
         let overshoot = point - cursor.start().0;
         cursor.start().1
-            + cursor.item().map_or(0, |chunk| {
-                chunk.as_slice().point_utf16_to_offset(overshoot, clip)
+            + cursor.item().map_or(OffsetUtf16(0), |chunk| {
+                chunk
+                    .as_slice()
+                    .offset_to_offset_utf16(chunk.as_slice().point_utf16_to_offset(overshoot, clip))
             })
     }
 
@@ -455,11 +526,12 @@ impl Rope {
         if point.0 >= self.summary().lines_utf16() {
             return self.summary().lines;
         }
-        let mut cursor = self.chunks.cursor::<Dimensions<PointUtf16, Point>>(());
-        cursor.seek(&point.0, Bias::Left);
-        let overshoot = Unclipped(point.0 - cursor.start().0);
-        cursor.start().1
-            + cursor.item().map_or(Point::zero(), |chunk| {
+        let (start, _, item) =
+            self.chunks
+                .find::<Dimensions<PointUtf16, Point>, _>((), &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)
             })
     }
@@ -472,33 +544,30 @@ impl Rope {
     }
 
     pub fn clip_offset_utf16(&self, offset: OffsetUtf16, bias: Bias) -> OffsetUtf16 {
-        let mut cursor = self.chunks.cursor::<OffsetUtf16>(());
-        cursor.seek(&offset, Bias::Right);
-        if let Some(chunk) = cursor.item() {
-            let overshoot = offset - cursor.start();
-            *cursor.start() + chunk.as_slice().clip_offset_utf16(overshoot, bias)
+        let (start, _, item) = self.chunks.find::<OffsetUtf16, _>((), &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 mut cursor = self.chunks.cursor::<Point>(());
-        cursor.seek(&point, Bias::Right);
-        if let Some(chunk) = cursor.item() {
-            let overshoot = point - cursor.start();
-            *cursor.start() + chunk.as_slice().clip_point(overshoot, bias)
+        let (start, _, item) = self.chunks.find::<Point, _>((), &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<PointUtf16>, bias: Bias) -> PointUtf16 {
-        let mut cursor = self.chunks.cursor::<PointUtf16>(());
-        cursor.seek(&point.0, Bias::Right);
-        if let Some(chunk) = cursor.item() {
-            let overshoot = Unclipped(point.0 - cursor.start());
-            *cursor.start() + chunk.as_slice().clip_point_utf16(overshoot, bias)
+        let (start, _, item) = self.chunks.find::<PointUtf16, _>((), &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()
         }
@@ -657,9 +726,9 @@ 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: u128,
+    pub chars: Bitmap,
     /// Bitmap of tab locations in text. LSB ordered
-    pub tabs: u128,
+    pub tabs: Bitmap,
 }
 
 #[derive(Clone)]
@@ -831,39 +900,6 @@ impl<'a> Chunks<'a> {
         self.offset < initial_offset && self.offset == 0
     }
 
-    /// Returns bitmaps that represent character positions and tab positions
-    pub fn peek_with_bitmaps(&self) -> Option<ChunkBitmaps<'a>> {
-        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
-        };
-
-        // slice range has a bounds between 0 and 128 in non test builds
-        // We use a non wrapping sub because we want to overflow in the case where slice_range.end == 128
-        // because that represents a full chunk and the bitmask shouldn't remove anything
-        let bitmask = (1u128.unbounded_shl(slice_range.end as u32)).wrapping_sub(1);
-
-        let chars = (chunk.chars() & bitmask) >> slice_range.start;
-        let tabs = (chunk.tabs & bitmask) >> slice_range.start;
-
-        Some(ChunkBitmaps {
-            text: &chunk.text[slice_range],
-            chars,
-            tabs,
-        })
-    }
-
     pub fn peek(&self) -> Option<&'a str> {
         if !self.offset_is_valid() {
             return None;
@@ -884,7 +920,8 @@ impl<'a> Chunks<'a> {
         Some(&chunk.text[slice_range])
     }
 
-    pub fn peek_tabs(&self) -> Option<ChunkBitmaps<'a>> {
+    /// Returns bitmaps that represent character positions and tab positions
+    pub fn peek_with_bitmaps(&self) -> Option<ChunkBitmaps<'a>> {
         if !self.offset_is_valid() {
             return None;
         }
@@ -904,7 +941,7 @@ impl<'a> Chunks<'a> {
         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_tabs = chunk.tabs() >> chunk_start_offset;
         let shifted_chars = chunk.chars() >> chunk_start_offset;
 
         Some(ChunkBitmaps {

crates/rpc/Cargo.toml 🔗

@@ -36,7 +36,6 @@ strum.workspace = true
 tracing = { version = "0.1.34", features = ["log"] }
 util.workspace = true
 zstd.workspace = true
-workspace-hack.workspace = true
 
 [dev-dependencies]
 collections = { workspace = true, features = ["test-support"] }

crates/rpc/src/proto_client.rs 🔗

@@ -226,6 +226,7 @@ impl AnyProtoClient {
     pub fn request_lsp<T>(
         &self,
         project_id: u64,
+        server_id: Option<u64>,
         timeout: Duration,
         executor: BackgroundExecutor,
         request: T,
@@ -247,6 +248,7 @@ impl AnyProtoClient {
 
         let query = proto::LspQuery {
             project_id,
+            server_id,
             lsp_request_id: new_id.0,
             request: Some(request.to_proto_query()),
         };
@@ -361,6 +363,9 @@ impl AnyProtoClient {
                             Response::GetImplementationResponse(response) => {
                                 to_any_envelope(&envelope, response)
                             }
+                            Response::InlayHintsResponse(response) => {
+                                to_any_envelope(&envelope, response)
+                            }
                         };
                         Some(proto::ProtoLspResponse {
                             server_id,

crates/rules_library/Cargo.toml 🔗

@@ -30,6 +30,5 @@ theme.workspace = true
 title_bar.workspace = true
 ui.workspace = true
 util.workspace = true
-workspace-hack.workspace = true
 workspace.workspace = true
 zed_actions.workspace = true

crates/rules_library/src/rules_library.rs 🔗

@@ -3,9 +3,9 @@ use collections::{HashMap, HashSet};
 use editor::{CompletionProvider, SelectionEffects};
 use editor::{CurrentLineHighlight, Editor, EditorElement, EditorEvent, EditorStyle, actions::Tab};
 use gpui::{
-    Action, App, Bounds, Entity, EventEmitter, Focusable, PromptLevel, Subscription, Task,
-    TextStyle, TitlebarOptions, WindowBounds, WindowHandle, WindowOptions, actions, point, size,
-    transparent_black,
+    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::{
@@ -129,13 +129,13 @@ pub fn open_rules_library(
                     titlebar: Some(TitlebarOptions {
                         title: Some("Rules Library".into()),
                         appears_transparent: true,
-                        traffic_light_position: Some(point(px(9.0), px(9.0))),
+                        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(size(px(800.), px(600.))), // 4:3 Aspect Ratio
+                    window_min_size: Some(DEFAULT_ADDITIONAL_WINDOW_SIZE),
                     kind: gpui::WindowKind::Floating,
                     ..Default::default()
                 },
@@ -369,10 +369,9 @@ impl PickerDelegate for RulePickerDelegate {
                         .spacing(ListItemSpacing::Sparse)
                         .toggle_state(selected)
                         .child(
-                            h_flex()
-                                .h_5()
-                                .line_height(relative(1.))
-                                .child(Label::new(rule.title.clone().unwrap_or("Untitled".into()))),
+                            Label::new(rule.title.clone().unwrap_or("Untitled".into()))
+                                .truncate()
+                                .mr_10(),
                         )
                         .end_slot::<IconButton>(default.then(|| {
                             IconButton::new("toggle-default-rule", IconName::Paperclip)
@@ -390,12 +389,11 @@ impl PickerDelegate for RulePickerDelegate {
                                     div()
                                         .id("built-in-rule")
                                         .child(Icon::new(IconName::FileLock).color(Color::Muted))
-                                        .tooltip(move |window, cx| {
+                                        .tooltip(move |_window, cx| {
                                             Tooltip::with_meta(
                                                 "Built-in rule",
                                                 None,
                                                 BUILT_IN_TOOLTIP_TEXT,
-                                                window,
                                                 cx,
                                             )
                                         })
@@ -426,12 +424,11 @@ impl PickerDelegate for RulePickerDelegate {
                                                     "Remove from Default Rules",
                                                 ))
                                             } else {
-                                                this.tooltip(move |window, cx| {
+                                                this.tooltip(move |_window, cx| {
                                                     Tooltip::with_meta(
                                                         "Add to Default Rules",
                                                         None,
                                                         "Always included in every thread.",
-                                                        window,
                                                         cx,
                                                     )
                                                 })
@@ -455,13 +452,15 @@ impl PickerDelegate for RulePickerDelegate {
         cx: &mut Context<Picker<Self>>,
     ) -> Div {
         h_flex()
-            .bg(cx.theme().colors().editor_background)
-            .rounded_sm()
-            .overflow_hidden()
-            .flex_none()
             .py_1()
-            .px_2()
+            .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())
     }
 }
@@ -1098,11 +1097,11 @@ impl RulesLibrary {
         v_flex()
             .id("rule-list")
             .capture_action(cx.listener(Self::focus_active_rule))
-            .bg(cx.theme().colors().panel_background)
+            .px_1p5()
             .h_full()
-            .px_1()
-            .w_1_3()
+            .w_64()
             .overflow_x_hidden()
+            .bg(cx.theme().colors().panel_background)
             .child(
                 h_flex()
                     .p(DynamicSpacing::Base04.rems(cx))
@@ -1112,8 +1111,8 @@ impl RulesLibrary {
                     .justify_end()
                     .child(
                         IconButton::new("new-rule", IconName::Plus)
-                            .tooltip(move |window, cx| {
-                                Tooltip::for_action("New Rule", &NewRule, window, cx)
+                            .tooltip(move |_window, cx| {
+                                Tooltip::for_action("New Rule", &NewRule, cx)
                             })
                             .on_click(|_, window, cx| {
                                 window.dispatch_action(Box::new(NewRule), cx);
@@ -1123,16 +1122,55 @@ impl RulesLibrary {
             .child(div().flex_grow().child(self.picker.clone()))
     }
 
+    fn render_active_rule_editor(
+        &self,
+        editor: &Entity<Editor>,
+        cx: &mut Context<Self>,
+    ) -> 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<RulesLibrary>) -> gpui::Stateful<Div> {
         div()
-            .w_2_3()
-            .h_full()
             .id("rule-editor")
+            .h_full()
+            .flex_grow()
             .border_l_1()
             .border_color(cx.theme().colors().border)
             .bg(cx.theme().colors().editor_background)
-            .flex_none()
-            .min_w_64()
             .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];
@@ -1140,7 +1178,6 @@ impl RulesLibrary {
                 let model = LanguageModelRegistry::read_global(cx)
                     .default_model()
                     .map(|default| default.model);
-                let settings = ThemeSettings::get_global(cx);
 
                 Some(
                     v_flex()
@@ -1160,46 +1197,7 @@ impl RulesLibrary {
                                 .gap_2()
                                 .justify_between()
                                 .child(
-                                    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(
-                                            &rule_editor.title_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()
-                                            },
-                                        )),
+                                    self.render_active_rule_editor(&rule_editor.title_editor, cx),
                                 )
                                 .child(
                                     h_flex()
@@ -1215,7 +1213,7 @@ impl RulesLibrary {
                                                 .id("token_count")
                                                 .mr_1()
                                                 .flex_shrink_0()
-                                                .tooltip(move |window, cx| {
+                                                .tooltip(move |_window, cx| {
                                                     Tooltip::with_meta(
                                                         "Token Estimation",
                                                         None,
@@ -1226,7 +1224,6 @@ impl RulesLibrary {
                                                                 .map(|model| model.name().0)
                                                                 .unwrap_or_default()
                                                         ),
-                                                        window,
                                                         cx,
                                                     )
                                                 })
@@ -1245,23 +1242,21 @@ impl RulesLibrary {
                                                     Icon::new(IconName::FileLock)
                                                         .color(Color::Muted),
                                                 )
-                                                .tooltip(move |window, cx| {
+                                                .tooltip(move |_window, cx| {
                                                     Tooltip::with_meta(
                                                         "Built-in rule",
                                                         None,
                                                         BUILT_IN_TOOLTIP_TEXT,
-                                                        window,
                                                         cx,
                                                     )
                                                 })
                                                 .into_any()
                                         } else {
                                             IconButton::new("delete-rule", IconName::Trash)
-                                                .tooltip(move |window, cx| {
+                                                .tooltip(move |_window, cx| {
                                                     Tooltip::for_action(
                                                         "Delete Rule",
                                                         &DeleteRule,
-                                                        window,
                                                         cx,
                                                     )
                                                 })
@@ -1273,11 +1268,10 @@ impl RulesLibrary {
                                         })
                                         .child(
                                             IconButton::new("duplicate-rule", IconName::BookCopy)
-                                                .tooltip(move |window, cx| {
+                                                .tooltip(move |_window, cx| {
                                                     Tooltip::for_action(
                                                         "Duplicate Rule",
                                                         &DuplicateRule,
-                                                        window,
                                                         cx,
                                                     )
                                                 })
@@ -1305,12 +1299,11 @@ impl RulesLibrary {
                                                         "Remove from Default Rules",
                                                     ))
                                                 } else {
-                                                    this.tooltip(move |window, cx| {
+                                                    this.tooltip(move |_window, cx| {
                                                         Tooltip::with_meta(
                                                             "Add to Default Rules",
                                                             None,
                                                             "Always included in every thread.",
-                                                            window,
                                                             cx,
                                                         )
                                                     })
@@ -1417,7 +1410,7 @@ impl Render for RulesLibrary {
                                                                 .full_width()
                                                                 .key_binding(
                                                                     KeyBinding::for_action(
-                                                                        &NewRule, window, cx,
+                                                                        &NewRule, cx,
                                                                     ),
                                                                 )
                                                                 .on_click(|_, window, cx| {

crates/scheduler/Cargo.toml 🔗

@@ -22,4 +22,3 @@ chrono.workspace = true
 futures.workspace = true
 parking_lot.workspace = true
 rand.workspace = true
-workspace-hack.workspace = true

crates/schema_generator/Cargo.toml 🔗

@@ -16,4 +16,3 @@ schemars = { workspace = true, features = ["indexmap2"] }
 serde.workspace = true
 serde_json.workspace = true
 theme.workspace = true
-workspace-hack.workspace = true

crates/search/Cargo.toml 🔗

@@ -39,13 +39,15 @@ smol.workspace = true
 theme.workspace = true
 ui.workspace = true
 util.workspace = true
+util_macros.workspace = true
 workspace.workspace = true
 zed_actions.workspace = true
-workspace-hack.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
 unindent.workspace = true
 workspace = { workspace = true, features = ["test-support"] }

crates/search/src/buffer_search.rs 🔗

@@ -266,12 +266,11 @@ impl Render for BufferSearchBar {
                     .toggle_state(self.selection_search_enabled.is_some())
                     .tooltip({
                         let focus_handle = focus_handle.clone();
-                        move |window, cx| {
+                        move |_window, cx| {
                             Tooltip::for_action_in(
                                 "Toggle Search Selection",
                                 &ToggleSelection,
                                 &focus_handle,
-                                window,
                                 cx,
                             )
                         }
@@ -1524,6 +1523,7 @@ mod tests {
     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| {
@@ -1580,6 +1580,7 @@ mod tests {
         (editor.unwrap(), search_bar, cx)
     }
 
+    #[perf]
     #[gpui::test]
     async fn test_search_simple(cx: &mut TestAppContext) {
         let (editor, search_bar, cx) = init_test(cx);
@@ -1860,6 +1861,7 @@ mod tests {
             .collect::<Vec<_>>()
     }
 
+    #[perf]
     #[gpui::test]
     async fn test_search_option_handling(cx: &mut TestAppContext) {
         let (editor, search_bar, cx) = init_test(cx);
@@ -1920,6 +1922,7 @@ mod tests {
         });
     }
 
+    #[perf]
     #[gpui::test]
     async fn test_search_select_all_matches(cx: &mut TestAppContext) {
         init_globals(cx);
@@ -2128,6 +2131,7 @@ mod tests {
             .unwrap();
     }
 
+    #[perf]
     #[gpui::test]
     async fn test_search_query_with_match_whole_word(cx: &mut TestAppContext) {
         init_globals(cx);
@@ -2213,6 +2217,7 @@ mod tests {
         });
     }
 
+    #[perf]
     #[gpui::test]
     async fn test_search_query_history(cx: &mut TestAppContext) {
         let (_editor, search_bar, cx) = init_test(cx);
@@ -2362,6 +2367,7 @@ mod tests {
         });
     }
 
+    #[perf]
     #[gpui::test]
     async fn test_replace_simple(cx: &mut TestAppContext) {
         let (editor, search_bar, cx) = init_test(cx);
@@ -2529,6 +2535,7 @@ mod tests {
         );
     }
 
+    #[perf]
     #[gpui::test]
     async fn test_replace_special_characters(cx: &mut TestAppContext) {
         let (editor, search_bar, cx) = init_test(cx);
@@ -2592,6 +2599,7 @@ mod tests {
         .await;
     }
 
+    #[perf]
     #[gpui::test]
     async fn test_find_matches_in_selections_singleton_buffer_multiple_selections(
         cx: &mut TestAppContext,
@@ -2658,6 +2666,7 @@ mod tests {
         });
     }
 
+    #[perf]
     #[gpui::test]
     async fn test_find_matches_in_selections_multiple_excerpts_buffer_multiple_selections(
         cx: &mut TestAppContext,
@@ -2744,6 +2753,7 @@ mod tests {
         });
     }
 
+    #[perf]
     #[gpui::test]
     async fn test_invalid_regexp_search_after_valid(cx: &mut TestAppContext) {
         let (editor, search_bar, cx) = init_test(cx);
@@ -2779,6 +2789,7 @@ mod tests {
         });
     }
 
+    #[perf]
     #[gpui::test]
     async fn test_search_options_changes(cx: &mut TestAppContext) {
         let (_editor, search_bar, cx) = init_test(cx);

crates/search/src/project_search.rs 🔗

@@ -8,7 +8,8 @@ use crate::{
 use anyhow::Context as _;
 use collections::HashMap;
 use editor::{
-    Anchor, Editor, EditorEvent, EditorSettings, MAX_TAB_TITLE_LEN, MultiBuffer, SelectionEffects,
+    Anchor, Editor, EditorEvent, EditorSettings, MAX_TAB_TITLE_LEN, MultiBuffer, PathKey,
+    SelectionEffects,
     actions::{Backtab, SelectAll, Tab},
     items::active_match_index,
     multibuffer_context_lines,
@@ -340,6 +341,7 @@ impl ProjectSearch {
                                 .into_iter()
                                 .map(|(buffer, ranges)| {
                                     excerpts.set_anchored_excerpts_for_path(
+                                        PathKey::for_buffer(&buffer, cx),
                                         buffer,
                                         ranges,
                                         multibuffer_context_lines(cx),
@@ -389,7 +391,7 @@ pub enum ViewEvent {
 impl EventEmitter<ViewEvent> for ProjectSearchView {}
 
 impl Render for ProjectSearchView {
-    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
         if self.has_matches() {
             div()
                 .flex_1()
@@ -424,7 +426,7 @@ impl Render for ProjectSearchView {
                     None
                 }
             } else {
-                Some(self.landing_text_minor(window, cx).into_any_element())
+                Some(self.landing_text_minor(cx).into_any_element())
             };
 
             let page_content = page_content.map(|text| div().child(text));
@@ -570,12 +572,14 @@ impl Item for ProjectSearchView {
         _workspace_id: Option<WorkspaceId>,
         window: &mut Window,
         cx: &mut Context<Self>,
-    ) -> Option<Entity<Self>>
+    ) -> Task<Option<Entity<Self>>>
     where
         Self: Sized,
     {
         let model = self.entity.update(cx, |model, cx| model.clone(cx));
-        Some(cx.new(|cx| Self::new(self.workspace.clone(), model, window, cx, None)))
+        Task::ready(Some(cx.new(|cx| {
+            Self::new(self.workspace.clone(), model, window, cx, None)
+        })))
     }
 
     fn added_to_workspace(
@@ -1442,7 +1446,7 @@ impl ProjectSearchView {
         self.active_match_index.is_some()
     }
 
-    fn landing_text_minor(&self, window: &mut Window, cx: &App) -> impl IntoElement {
+    fn landing_text_minor(&self, cx: &App) -> impl IntoElement {
         let focus_handle = self.focus_handle.clone();
         v_flex()
             .gap_1()
@@ -1456,12 +1460,7 @@ impl ProjectSearchView {
                     .icon(IconName::Filter)
                     .icon_position(IconPosition::Start)
                     .icon_size(IconSize::Small)
-                    .key_binding(KeyBinding::for_action_in(
-                        &ToggleFilters,
-                        &focus_handle,
-                        window,
-                        cx,
-                    ))
+                    .key_binding(KeyBinding::for_action_in(&ToggleFilters, &focus_handle, cx))
                     .on_click(|_event, window, cx| {
                         window.dispatch_action(ToggleFilters.boxed_clone(), cx)
                     }),
@@ -1471,12 +1470,7 @@ impl ProjectSearchView {
                     .icon(IconName::Replace)
                     .icon_position(IconPosition::Start)
                     .icon_size(IconSize::Small)
-                    .key_binding(KeyBinding::for_action_in(
-                        &ToggleReplace,
-                        &focus_handle,
-                        window,
-                        cx,
-                    ))
+                    .key_binding(KeyBinding::for_action_in(&ToggleReplace, &focus_handle, cx))
                     .on_click(|_event, window, cx| {
                         window.dispatch_action(ToggleReplace.boxed_clone(), cx)
                     }),
@@ -1486,12 +1480,7 @@ impl ProjectSearchView {
                     .icon(IconName::Regex)
                     .icon_position(IconPosition::Start)
                     .icon_size(IconSize::Small)
-                    .key_binding(KeyBinding::for_action_in(
-                        &ToggleRegex,
-                        &focus_handle,
-                        window,
-                        cx,
-                    ))
+                    .key_binding(KeyBinding::for_action_in(&ToggleRegex, &focus_handle, cx))
                     .on_click(|_event, window, cx| {
                         window.dispatch_action(ToggleRegex.boxed_clone(), cx)
                     }),
@@ -1504,7 +1493,6 @@ impl ProjectSearchView {
                     .key_binding(KeyBinding::for_action_in(
                         &ToggleCaseSensitive,
                         &focus_handle,
-                        window,
                         cx,
                     ))
                     .on_click(|_event, window, cx| {
@@ -1519,7 +1507,6 @@ impl ProjectSearchView {
                     .key_binding(KeyBinding::for_action_in(
                         &ToggleWholeWord,
                         &focus_handle,
-                        window,
                         cx,
                     ))
                     .on_click(|_event, window, cx| {
@@ -2045,8 +2032,8 @@ impl Render for ProjectSearchBar {
             .child(
                 IconButton::new("project-search-filter-button", IconName::Filter)
                     .shape(IconButtonShape::Square)
-                    .tooltip(|window, cx| {
-                        Tooltip::for_action("Toggle Filters", &ToggleFilters, window, cx)
+                    .tooltip(|_window, cx| {
+                        Tooltip::for_action("Toggle Filters", &ToggleFilters, cx)
                     })
                     .on_click(cx.listener(|this, _, window, cx| {
                         this.toggle_filters(window, cx);
@@ -2059,12 +2046,11 @@ impl Render for ProjectSearchBar {
                     )
                     .tooltip({
                         let focus_handle = focus_handle.clone();
-                        move |window, cx| {
+                        move |_window, cx| {
                             Tooltip::for_action_in(
                                 "Toggle Filters",
                                 &ToggleFilters,
                                 &focus_handle,
-                                window,
                                 cx,
                             )
                         }
@@ -2353,12 +2339,15 @@ pub mod tests {
     use super::*;
     use editor::{DisplayPoint, display_map::DisplayRow};
     use gpui::{Action, TestAppContext, VisualTestContext, WindowHandle};
+    use language::{FakeLspAdapter, rust_lang};
     use project::FakeFs;
     use serde_json::json;
-    use settings::SettingsStore;
+    use settings::{InlayHintSettingsContent, SettingsStore};
     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) {
         init_test(cx);
@@ -2496,6 +2485,7 @@ pub mod tests {
             .unwrap();
     }
 
+    #[perf]
     #[gpui::test]
     async fn test_deploy_project_search_focus(cx: &mut TestAppContext) {
         init_test(cx);
@@ -2736,6 +2726,7 @@ pub mod tests {
         }).unwrap();
     }
 
+    #[perf]
     #[gpui::test]
     async fn test_filters_consider_toggle_state(cx: &mut TestAppContext) {
         init_test(cx);
@@ -2856,6 +2847,7 @@ pub mod tests {
             .unwrap();
     }
 
+    #[perf]
     #[gpui::test]
     async fn test_new_project_search_focus(cx: &mut TestAppContext) {
         init_test(cx);
@@ -3151,6 +3143,7 @@ pub mod tests {
                 });}).unwrap();
     }
 
+    #[perf]
     #[gpui::test]
     async fn test_new_project_search_in_directory(cx: &mut TestAppContext) {
         init_test(cx);
@@ -3277,6 +3270,7 @@ pub mod tests {
             .unwrap();
     }
 
+    #[perf]
     #[gpui::test]
     async fn test_search_query_history(cx: &mut TestAppContext) {
         init_test(cx);
@@ -3607,6 +3601,7 @@ pub mod tests {
             .unwrap();
     }
 
+    #[perf]
     #[gpui::test]
     async fn test_search_query_history_with_multiple_views(cx: &mut TestAppContext) {
         init_test(cx);
@@ -3684,6 +3679,7 @@ pub mod tests {
                 )
             })
             .unwrap()
+            .await
             .unwrap();
         assert_eq!(cx.update(|cx| second_pane.read(cx).items_len()), 1);
 
@@ -3830,6 +3826,7 @@ pub mod tests {
         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);
@@ -3878,6 +3875,7 @@ pub mod tests {
                 )
             })
             .unwrap()
+            .await
             .unwrap();
         assert_eq!(cx.update(|cx| second_pane.read(cx).items_len()), 1);
         assert!(
@@ -3989,6 +3987,7 @@ pub mod tests {
             .unwrap();
     }
 
+    #[perf]
     #[gpui::test]
     async fn test_scroll_search_results_to_top(cx: &mut TestAppContext) {
         init_test(cx);
@@ -4069,6 +4068,7 @@ pub mod tests {
             .expect("unable to update search view");
     }
 
+    #[perf]
     #[gpui::test]
     async fn test_buffer_search_query_reused(cx: &mut TestAppContext) {
         init_test(cx);
@@ -4209,6 +4209,101 @@ pub mod tests {
             .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 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(|fake_server| {
+                    fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
+                        move |_, _| async move {
+                            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();
+
+        // Can do the 2nd search without any panics
+        perform_search(search_view, "let ", cx);
+        cx.executor().advance_clock(Duration::from_millis(100));
+        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();
+    }
+
     fn init_test(cx: &mut TestAppContext) {
         cx.update(|cx| {
             let settings = SettingsStore::test(cx);

crates/search/src/search.rs 🔗

@@ -158,9 +158,7 @@ impl SearchOption {
         .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, window, cx)
-        })
+        .tooltip(move |_window, cx| Tooltip::for_action_in(label, action, &focus_handle, cx))
     }
 }
 

crates/search/src/search_bar.rs 🔗

@@ -32,7 +32,7 @@ pub(super) fn render_action_button(
             window.dispatch_action(action.boxed_clone(), cx)
         }
     })
-    .tooltip(move |window, cx| Tooltip::for_action_in(tooltip, action, &focus_handle, window, 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),

crates/search/src/search_status_button.rs 🔗

@@ -24,13 +24,8 @@ impl Render for SearchButton {
         button.child(
             IconButton::new("project-search-indicator", SEARCH_ICON)
                 .icon_size(IconSize::Small)
-                .tooltip(|window, cx| {
-                    Tooltip::for_action(
-                        "Project Search",
-                        &workspace::DeploySearch::default(),
-                        window,
-                        cx,
-                    )
+                .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);

crates/session/Cargo.toml 🔗

@@ -23,4 +23,3 @@ gpui.workspace = true
 uuid.workspace = true
 util.workspace = true
 serde_json.workspace = true
-workspace-hack.workspace = true

crates/settings/Cargo.toml 🔗

@@ -41,7 +41,6 @@ strum.workspace = true
 tree-sitter-json.workspace = true
 tree-sitter.workspace = true
 util.workspace = true
-workspace-hack.workspace = true
 zlog.workspace = true
 
 [dev-dependencies]

crates/settings/src/base_keymap_setting.rs 🔗

@@ -1,12 +1,9 @@
 use std::fmt::{Display, Formatter};
 
-use crate::{
-    self as settings,
-    settings_content::{BaseKeymapContent, SettingsContent},
-};
+use crate::{self as settings, settings_content::BaseKeymapContent};
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
-use settings::{Settings, VsCodeSettings};
+use settings::Settings;
 
 /// Base key bindings scheme. Base keymaps can be overridden with user keymaps.
 ///
@@ -133,8 +130,4 @@ impl Settings for BaseKeymap {
     fn from_settings(s: &crate::settings_content::SettingsContent) -> Self {
         s.base_keymap.unwrap().into()
     }
-
-    fn import_from_vscode(_vscode: &VsCodeSettings, current: &mut SettingsContent) {
-        current.base_keymap = Some(BaseKeymapContent::VSCode);
-    }
 }

crates/settings/src/serde_helper.rs 🔗

@@ -0,0 +1,135 @@
+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<S>(
+    value: &f32,
+    serializer: S,
+) -> Result<S::Ok, S::Error>
+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<f32>` 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<f32>,
+/// }
+/// ```
+pub fn serialize_optional_f32_with_two_decimal_places<S>(
+    value: &Option<f32>,
+    serializer: S,
+) -> Result<S::Ok, S::Error>
+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<f32>,
+    }
+
+    #[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);
+        }
+    }
+}

crates/settings/src/settings.rs 🔗

@@ -2,6 +2,7 @@ mod base_keymap_setting;
 mod editable_setting_control;
 mod keymap_file;
 pub mod merge_from;
+mod serde_helper;
 mod settings_content;
 mod settings_file;
 mod settings_json;
@@ -21,6 +22,7 @@ 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::{

crates/settings/src/settings_content.rs 🔗

@@ -485,6 +485,7 @@ pub struct GitPanelSettingsContent {
     /// Default width of the panel in pixels.
     ///
     /// Default: 360
+    #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")]
     pub default_width: Option<f32>,
     /// How entry statuses are displayed.
     ///
@@ -556,6 +557,7 @@ pub struct NotificationPanelSettingsContent {
     /// Default width of the panel in pixels.
     ///
     /// Default: 300
+    #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")]
     pub default_width: Option<f32>,
 }
 
@@ -573,6 +575,7 @@ pub struct PanelSettingsContent {
     /// Default width of the panel in pixels.
     ///
     /// Default: 240
+    #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")]
     pub default_width: Option<f32>,
 }
 
@@ -608,14 +611,33 @@ pub struct FileFinderSettingsContent {
     /// Whether to use gitignored files when searching.
     /// Only the file Zed had indexed will be used, not necessary all the gitignored files.
     ///
-    /// Can accept 3 values:
-    /// * `Some(true)`: Use all gitignored files
-    /// * `Some(false)`: Use only the files Zed had indexed
-    /// * `None`: Be smart and search for ignored when called from a gitignored worktree
-    ///
-    /// Default: None
-    /// todo() -> Change this type to an enum
-    pub include_ignored: Option<bool>,
+    /// Default: Smart
+    pub include_ignored: Option<IncludeIgnoredContent>,
+}
+
+#[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(
@@ -728,6 +750,7 @@ pub struct OutlinePanelSettingsContent {
     /// 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<f32>,
     /// The position of outline panel
     ///
@@ -748,6 +771,7 @@ pub struct OutlinePanelSettingsContent {
     /// 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<f32>,
     /// Whether to reveal it in the outline panel automatically,
     /// when a corresponding project entry becomes active.
@@ -985,3 +1009,248 @@ impl merge_from::MergeFrom for SaturatingBool {
         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<u64> 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)
+    }
+}
+
+/// A wrapper type that distinguishes between an explicitly set value (including null) and an unset value.
+///
+/// This is useful for configuration where you need to differentiate between:
+/// - A field that is not present in the configuration file (`Maybe::Unset`)
+/// - A field that is explicitly set to `null` (`Maybe::Set(None)`)
+/// - A field that is explicitly set to a value (`Maybe::Set(Some(value))`)
+///
+/// # Examples
+///
+/// In JSON:
+/// - `{}` (field missing) deserializes to `Maybe::Unset`
+/// - `{"field": null}` deserializes to `Maybe::Set(None)`
+/// - `{"field": "value"}` deserializes to `Maybe::Set(Some("value"))`
+///
+/// WARN: This type should not be wrapped in an option inside of settings, otherwise the default `serde_json` behavior
+/// of treating `null` and missing as the `Option::None` will be used
+#[derive(Debug, Clone, PartialEq, Eq, strum::EnumDiscriminants, Default)]
+#[strum_discriminants(derive(strum::VariantArray, strum::VariantNames, strum::FromRepr))]
+pub enum Maybe<T> {
+    /// An explicitly set value, which may be `None` (representing JSON `null`) or `Some(value)`.
+    Set(Option<T>),
+    /// A value that was not present in the configuration.
+    #[default]
+    Unset,
+}
+
+impl<T: Clone> merge_from::MergeFrom for Maybe<T> {
+    fn merge_from(&mut self, other: &Self) {
+        if self.is_unset() {
+            *self = other.clone();
+        }
+    }
+}
+
+impl<T> From<Option<Option<T>>> for Maybe<T> {
+    fn from(value: Option<Option<T>>) -> Self {
+        match value {
+            Some(value) => Maybe::Set(value),
+            None => Maybe::Unset,
+        }
+    }
+}
+
+impl<T> Maybe<T> {
+    pub fn is_set(&self) -> bool {
+        matches!(self, Maybe::Set(_))
+    }
+
+    pub fn is_unset(&self) -> bool {
+        matches!(self, Maybe::Unset)
+    }
+
+    pub fn into_inner(self) -> Option<T> {
+        match self {
+            Maybe::Set(value) => value,
+            Maybe::Unset => None,
+        }
+    }
+
+    pub fn as_ref(&self) -> Option<&Option<T>> {
+        match self {
+            Maybe::Set(value) => Some(value),
+            Maybe::Unset => None,
+        }
+    }
+}
+
+impl<T: serde::Serialize> serde::Serialize for Maybe<T> {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: serde::Serializer,
+    {
+        match self {
+            Maybe::Set(value) => value.serialize(serializer),
+            Maybe::Unset => serializer.serialize_none(),
+        }
+    }
+}
+
+impl<'de, T: serde::Deserialize<'de>> serde::Deserialize<'de> for Maybe<T> {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: serde::Deserializer<'de>,
+    {
+        Option::<T>::deserialize(deserializer).map(Maybe::Set)
+    }
+}
+
+impl<T: JsonSchema> JsonSchema for Maybe<T> {
+    fn schema_name() -> std::borrow::Cow<'static, str> {
+        format!("Nullable<{}>", T::schema_name()).into()
+    }
+
+    fn json_schema(generator: &mut schemars::generate::SchemaGenerator) -> schemars::Schema {
+        let mut schema = generator.subschema_for::<Option<T>>();
+        // Add description explaining that null is an explicit value
+        let description = if let Some(existing_desc) =
+            schema.get("description").and_then(|desc| desc.as_str())
+        {
+            format!(
+                "{}. Note: `null` is treated as an explicit value, different from omitting the field entirely.",
+                existing_desc
+            )
+        } else {
+            "This field supports explicit `null` values. Omitting the field is different from setting it to `null`.".to_string()
+        };
+
+        schema.insert("description".to_string(), description.into());
+
+        schema
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use serde_json;
+
+    #[test]
+    fn test_maybe() {
+        #[derive(Debug, PartialEq, Serialize, Deserialize)]
+        struct TestStruct {
+            #[serde(default)]
+            #[serde(skip_serializing_if = "Maybe::is_unset")]
+            field: Maybe<String>,
+        }
+
+        #[derive(Debug, PartialEq, Serialize, Deserialize)]
+        struct NumericTest {
+            #[serde(default)]
+            value: Maybe<i32>,
+        }
+
+        let json = "{}";
+        let result: TestStruct = serde_json::from_str(json).unwrap();
+        assert!(result.field.is_unset());
+        assert_eq!(result.field, Maybe::Unset);
+
+        let json = r#"{"field": null}"#;
+        let result: TestStruct = serde_json::from_str(json).unwrap();
+        assert!(result.field.is_set());
+        assert_eq!(result.field, Maybe::Set(None));
+
+        let json = r#"{"field": "hello"}"#;
+        let result: TestStruct = serde_json::from_str(json).unwrap();
+        assert!(result.field.is_set());
+        assert_eq!(result.field, Maybe::Set(Some("hello".to_string())));
+
+        let test = TestStruct {
+            field: Maybe::Unset,
+        };
+        let json = serde_json::to_string(&test).unwrap();
+        assert_eq!(json, "{}");
+
+        let test = TestStruct {
+            field: Maybe::Set(None),
+        };
+        let json = serde_json::to_string(&test).unwrap();
+        assert_eq!(json, r#"{"field":null}"#);
+
+        let test = TestStruct {
+            field: Maybe::Set(Some("world".to_string())),
+        };
+        let json = serde_json::to_string(&test).unwrap();
+        assert_eq!(json, r#"{"field":"world"}"#);
+
+        let default_maybe: Maybe<i32> = Maybe::default();
+        assert!(default_maybe.is_unset());
+
+        let unset: Maybe<String> = Maybe::Unset;
+        assert!(unset.is_unset());
+        assert!(!unset.is_set());
+
+        let set_none: Maybe<String> = Maybe::Set(None);
+        assert!(set_none.is_set());
+        assert!(!set_none.is_unset());
+
+        let set_some: Maybe<String> = Maybe::Set(Some("value".to_string()));
+        assert!(set_some.is_set());
+        assert!(!set_some.is_unset());
+
+        let original = TestStruct {
+            field: Maybe::Set(Some("test".to_string())),
+        };
+        let json = serde_json::to_string(&original).unwrap();
+        let deserialized: TestStruct = serde_json::from_str(&json).unwrap();
+        assert_eq!(original, deserialized);
+
+        let json = r#"{"value": 42}"#;
+        let result: NumericTest = serde_json::from_str(json).unwrap();
+        assert_eq!(result.value, Maybe::Set(Some(42)));
+
+        let json = r#"{"value": null}"#;
+        let result: NumericTest = serde_json::from_str(json).unwrap();
+        assert_eq!(result.value, Maybe::Set(None));
+
+        let json = "{}";
+        let result: NumericTest = serde_json::from_str(json).unwrap();
+        assert_eq!(result.value, Maybe::Unset);
+
+        // Test JsonSchema implementation
+        use schemars::schema_for;
+        let schema = schema_for!(Maybe<String>);
+        let schema_json = serde_json::to_value(&schema).unwrap();
+
+        // Verify the description mentions that null is an explicit value
+        let description = schema_json["description"].as_str().unwrap();
+        assert!(
+            description.contains("null") && description.contains("explicit"),
+            "Schema description should mention that null is an explicit value. Got: {}",
+            description
+        );
+    }
+}

crates/settings/src/settings_content/agent.rs 🔗

@@ -26,10 +26,12 @@ pub struct AgentSettingsContent {
     /// 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<f32>,
     /// 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<f32>,
     /// The default model to use when creating new chats and for other features when a specific model is not specified.
     pub default_model: Option<LanguageModelSelection>,
@@ -68,10 +70,6 @@ pub struct AgentSettingsContent {
     ///
     /// Default: false
     pub play_sound_when_agent_done: Option<bool>,
-    /// Whether to stream edits from the agent as they are received.
-    ///
-    /// Default: false
-    pub stream_edits: Option<bool>,
     /// Whether to display agent edits in single-file editors in addition to the review multibuffer pane.
     ///
     /// Default: true
@@ -236,6 +234,7 @@ pub enum CompletionMode {
 pub struct LanguageModelParameters {
     pub provider: Option<LanguageModelProviderSetting>,
     pub model: Option<SharedString>,
+    #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")]
     pub temperature: Option<f32>,
 }
 

crates/settings/src/settings_content/editor.rs 🔗

@@ -7,7 +7,9 @@ use serde::{Deserialize, Serialize};
 use serde_with::skip_serializing_none;
 use settings_macros::MergeFrom;
 
-use crate::{DiagnosticSeverityContent, ShowScrollbar};
+use crate::{
+    DelayMs, DiagnosticSeverityContent, ShowScrollbar, serialize_f32_with_two_decimal_places,
+};
 
 #[skip_serializing_none]
 #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
@@ -45,7 +47,7 @@ pub struct EditorSettingsContent {
     /// server based on the current cursor location.
     ///
     /// Default: 75
-    pub lsp_highlight_debounce: Option<u64>,
+    pub lsp_highlight_debounce: Option<DelayMs>,
     /// Whether to show the informational hover box when moving the mouse
     /// over symbols in the editor.
     ///
@@ -54,7 +56,7 @@ pub struct EditorSettingsContent {
     /// Time to wait in milliseconds before showing the informational hover box.
     ///
     /// Default: 300
-    pub hover_popover_delay: Option<u64>,
+    pub hover_popover_delay: Option<DelayMs>,
     /// Toolbar related settings
     pub toolbar: Option<ToolbarContent>,
     /// Scrollbar related settings
@@ -70,6 +72,7 @@ pub struct EditorSettingsContent {
     /// 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<f32>,
     /// Whether to scroll when clicking near the edge of the visible text area.
     ///
@@ -78,17 +81,20 @@ pub struct EditorSettingsContent {
     /// 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<f32>,
     /// 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<f32>,
     /// 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<f32>,
     /// Whether the line numbers on editors gutter are relative or not.
     ///
@@ -722,7 +728,7 @@ pub struct DragAndDropSelectionContent {
     /// 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<u64>,
+    pub delay: Option<DelayMs>,
 }
 
 /// When to show the minimap in the editor.
@@ -796,10 +802,113 @@ pub enum DisplayIn {
     derive_more::FromStr,
 )]
 #[serde(transparent)]
-pub struct MinimumContrast(pub f32);
+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<f32> 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<f32> 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<f32> 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)."
+        })
+    }
+}

crates/settings/src/settings_content/language.rs 🔗

@@ -296,12 +296,12 @@ pub struct LanguageSettingsContent {
     /// Inlay hint related settings.
     pub inlay_hints: Option<InlayHintSettingsContent>,
     /// Whether to automatically type closing characters for you. For example,
-    /// when you type (, Zed will automatically add a closing ) at the correct position.
+    /// when you type '(', Zed will automatically add a closing ')' at the correct position.
     ///
     /// Default: true
     pub use_autoclose: Option<bool>,
     /// Whether to automatically surround text with characters for you. For example,
-    /// when you select text and type (, Zed will automatically surround text with ().
+    /// when you select text and type '(', Zed will automatically surround text with ().
     ///
     /// Default: true
     pub use_auto_surround: Option<bool>,

crates/settings/src/settings_content/language_model.rs 🔗

@@ -46,6 +46,7 @@ pub struct AnthropicAvailableModel {
     /// Configuration of Anthropic's caching API.
     pub cache_configuration: Option<LanguageModelCacheConfiguration>,
     pub max_output_tokens: Option<u64>,
+    #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")]
     pub default_temperature: Option<f32>,
     #[serde(default)]
     pub extra_beta_headers: Vec<String>,
@@ -71,6 +72,7 @@ pub struct BedrockAvailableModel {
     pub max_tokens: u64,
     pub cache_configuration: Option<LanguageModelCacheConfiguration>,
     pub max_output_tokens: Option<u64>,
+    #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")]
     pub default_temperature: Option<f32>,
     pub mode: Option<ModelMode>,
 }
@@ -332,6 +334,7 @@ pub struct ZedDotDevAvailableModel {
     /// Indicates whether this custom model supports caching.
     pub cache_configuration: Option<LanguageModelCacheConfiguration>,
     /// The default temperature to use for this model.
+    #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")]
     pub default_temperature: Option<f32>,
     /// Any extra beta headers to provide when using the model.
     #[serde(default)]

crates/settings/src/settings_content/project.rs 🔗

@@ -8,7 +8,8 @@ use settings_macros::MergeFrom;
 use util::serde::default_true;
 
 use crate::{
-    AllLanguageSettingsContent, ExtendingVec, ProjectTerminalSettingsContent, SlashCommandSettings,
+    AllLanguageSettingsContent, DelayMs, ExtendingVec, Maybe, ProjectTerminalSettingsContent,
+    SlashCommandSettings,
 };
 
 #[skip_serializing_none]
@@ -55,11 +56,13 @@ pub struct ProjectSettingsContent {
 #[skip_serializing_none]
 #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct WorktreeSettingsContent {
-    /// The displayed name of this project. If not set or empty, the root directory name
+    /// The displayed name of this project. If not set or null, the root directory name
     /// will be displayed.
     ///
-    /// Default: ""
-    pub project_name: Option<String>,
+    /// Default: null
+    #[serde(default)]
+    #[serde(skip_serializing_if = "Maybe::is_unset")]
+    pub project_name: Maybe<String>,
 
     /// Completely ignore files matching globs from `file_scan_exclusions`. Overrides
     /// `file_scan_inclusions`.
@@ -154,6 +157,8 @@ pub struct DapSettingsContent {
     pub binary: Option<String>,
     #[serde(default)]
     pub args: Option<Vec<String>>,
+    #[serde(default)]
+    pub env: Option<HashMap<String, String>>,
 }
 
 #[skip_serializing_none]
@@ -308,7 +313,7 @@ pub struct InlineBlameSettings {
     /// after a delay once the cursor stops moving.
     ///
     /// Default: 0
-    pub delay_ms: Option<u64>,
+    pub delay_ms: Option<DelayMs>,
     /// The amount of padding between the end of the source line and the start
     /// of the inline blame in units of columns.
     ///
@@ -395,7 +400,7 @@ pub struct LspPullDiagnosticsSettingsContent {
     /// 0 turns the debounce off.
     ///
     /// Default: 50
-    pub debounce_ms: Option<u64>,
+    pub debounce_ms: Option<DelayMs>,
 }
 
 #[skip_serializing_none]
@@ -411,7 +416,7 @@ pub struct InlineDiagnosticsSettingsContent {
     /// last editor event.
     ///
     /// Default: 150
-    pub update_debounce_ms: Option<u64>,
+    pub update_debounce_ms: Option<DelayMs>,
     /// The amount of padding between the end of the source line and the start
     /// of the inline diagnostic in units of columns.
     ///

crates/settings/src/settings_content/terminal.rs 🔗

@@ -41,6 +41,7 @@ pub struct TerminalSettingsContent {
     ///
     /// 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<f32>,
     /// Sets the terminal's font family.
     ///
@@ -61,6 +62,7 @@ pub struct TerminalSettingsContent {
     pub line_height: Option<TerminalLineHeight>,
     pub font_features: Option<FontFeatures>,
     /// Sets the terminal's font weight in CSS weight units 0-900.
+    #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")]
     pub font_weight: Option<f32>,
     /// Default cursor shape for the terminal.
     /// Can be "bar", "block", "underline", or "hollow".
@@ -99,10 +101,12 @@ pub struct TerminalSettingsContent {
     /// 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<f32>,
     /// 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<f32>,
     /// 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.
@@ -130,11 +134,24 @@ pub struct TerminalSettingsContent {
     /// - 90: Preferred for body text
     ///
     /// Default: 45
+    #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")]
     pub minimum_contrast: Option<f32>,
 }
 
 /// Shell configuration to open the terminal with.
-#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom)]
+#[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
@@ -153,7 +170,18 @@ pub enum Shell {
     },
 }
 
-#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom)]
+#[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.  Will Fallback to the
@@ -190,7 +218,7 @@ pub enum TerminalLineHeight {
     /// particularly if they use box characters
     Standard,
     /// Use a custom line height.
-    Custom(f32),
+    Custom(#[serde(serialize_with = "crate::serialize_f32_with_two_decimal_places")] f32),
 }
 
 impl TerminalLineHeight {

crates/settings/src/settings_content/theme.rs 🔗

@@ -9,6 +9,8 @@ use std::{fmt::Display, sync::Arc};
 
 use serde_with::skip_serializing_none;
 
+use crate::serialize_f32_with_two_decimal_places;
+
 /// Settings for rendering text in UI and text buffers.
 
 #[skip_serializing_none]
@@ -16,6 +18,7 @@ use serde_with::skip_serializing_none;
 pub struct ThemeSettingsContent {
     /// The default font size for text in the UI.
     #[serde(default)]
+    #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")]
     pub ui_font_size: Option<f32>,
     /// The name of a font to use for rendering in the UI.
     #[serde(default)]
@@ -42,6 +45,7 @@ pub struct ThemeSettingsContent {
     pub buffer_font_fallbacks: Option<Vec<FontFamilyName>>,
     /// The default font size for rendering in text buffers.
     #[serde(default)]
+    #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")]
     pub buffer_font_size: Option<f32>,
     /// The weight of the editor font in CSS units from 100 to 900.
     #[serde(default)]
@@ -56,9 +60,11 @@ pub struct ThemeSettingsContent {
     pub buffer_font_features: Option<FontFeatures>,
     /// The font size for agent responses in the agent panel. Falls back to the UI font size if unset.
     #[serde(default)]
+    #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")]
     pub agent_ui_font_size: Option<f32>,
     /// The font size for user messages in the agent panel.
     #[serde(default)]
+    #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")]
     pub agent_buffer_font_size: Option<f32>,
     /// The name of the Zed theme to use.
     #[serde(default)]
@@ -104,7 +110,7 @@ pub struct ThemeSettingsContent {
     derive_more::FromStr,
 )]
 #[serde(transparent)]
-pub struct CodeFade(pub f32);
+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 {
@@ -112,6 +118,12 @@ impl Display for CodeFade {
     }
 }
 
+impl From<f32> for CodeFade {
+    fn from(x: f32) -> Self {
+        Self(x)
+    }
+}
+
 fn default_font_features() -> Option<FontFeatures> {
     Some(FontFeatures::default())
 }
@@ -154,7 +166,18 @@ pub enum ThemeSelection {
 }
 
 /// Represents the selection of an icon theme, which can be either static or dynamic.
-#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq)]
+#[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.
@@ -284,7 +307,19 @@ impl From<FontFamilyName> for String {
 }
 
 /// The buffer's line height.
-#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom, Default)]
+#[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.
@@ -860,6 +895,35 @@ pub struct ThemeColorsContent {
     /// Deprecated in favor of `version_control_conflict_marker_theirs`.
     #[deprecated]
     pub version_control_conflict_theirs_background: Option<String>,
+
+    /// Background color for Vim Normal mode indicator.
+    #[serde(rename = "vim.normal.background")]
+    pub vim_normal_background: Option<String>,
+    /// Background color for Vim Insert mode indicator.
+    #[serde(rename = "vim.insert.background")]
+    pub vim_insert_background: Option<String>,
+    /// Background color for Vim Replace mode indicator.
+    #[serde(rename = "vim.replace.background")]
+    pub vim_replace_background: Option<String>,
+    /// Background color for Vim Visual mode indicator.
+    #[serde(rename = "vim.visual.background")]
+    pub vim_visual_background: Option<String>,
+    /// Background color for Vim Visual Line mode indicator.
+    #[serde(rename = "vim.visual_line.background")]
+    pub vim_visual_line_background: Option<String>,
+    /// Background color for Vim Visual Block mode indicator.
+    #[serde(rename = "vim.visual_block.background")]
+    pub vim_visual_block_background: Option<String>,
+    /// Background color for Vim Helix Normal mode indicator.
+    #[serde(rename = "vim.helix_normal.background")]
+    pub vim_helix_normal_background: Option<String>,
+    /// Background color for Vim Helix Select mode indicator.
+    #[serde(rename = "vim.helix_select.background")]
+    pub vim_helix_select_background: Option<String>,
+
+    /// Text color for Vim mode indicator label.
+    #[serde(rename = "vim.mode.text")]
+    pub vim_mode_text: Option<String>,
 }
 
 #[skip_serializing_none]

crates/settings/src/settings_content/workspace.rs 🔗

@@ -6,7 +6,10 @@ use serde::{Deserialize, Serialize};
 use serde_with::skip_serializing_none;
 use settings_macros::MergeFrom;
 
-use crate::{DockPosition, DockSide, ScrollbarSettingsContent, ShowIndentGuides};
+use crate::{
+    CenteredPaddingSettings, DelayMs, DockPosition, DockSide, InactiveOpacity,
+    ScrollbarSettingsContent, ShowIndentGuides, serialize_optional_f32_with_two_decimal_places,
+};
 
 #[skip_serializing_none]
 #[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom)]
@@ -56,6 +59,7 @@ pub struct WorkspaceSettingsContent {
     /// 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<f32>,
     /// Whether to close the window when using 'close active item' on a workspace with no tabs
     ///
@@ -249,6 +253,7 @@ pub struct ActivePaneModifiers {
     /// The border is drawn inset.
     ///
     /// Default: `0.0`
+    #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")]
     pub border_size: Option<f32>,
     /// Opacity of inactive panels.
     /// When set to 1.0, the inactive panes have the same opacity as the active one.
@@ -256,7 +261,8 @@ pub struct ActivePaneModifiers {
     /// Values are clamped to the [0.0, 1.0] range.
     ///
     /// Default: `1.0`
-    pub inactive_opacity: Option<f32>,
+    #[schemars(range(min = 0.0, max = 1.0))]
+    pub inactive_opacity: Option<InactiveOpacity>,
 }
 
 #[derive(
@@ -377,15 +383,31 @@ pub struct StatusBarSettingsContent {
     ///
     /// Default: true
     pub cursor_position_button: Option<bool>,
+    /// Whether to show active line endings button in the status bar.
+    ///
+    /// Default: false
+    pub line_endings_button: Option<bool>,
 }
 
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom)]
+#[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: u64 },
+    AfterDelay { milliseconds: DelayMs },
     /// Autosave when focus changes.
     OnFocusChange,
     /// Autosave when the active window changes.
@@ -441,20 +463,20 @@ pub enum PaneSplitDirectionVertical {
     Right,
 }
 
-#[skip_serializing_none]
 #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Default)]
 #[serde(rename_all = "snake_case")]
+#[skip_serializing_none]
 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<f32>,
+    pub left_padding: Option<CenteredPaddingSettings>,
     // 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<f32>,
+    pub right_padding: Option<CenteredPaddingSettings>,
 }
 
 #[derive(
@@ -502,6 +524,7 @@ pub struct ProjectPanelSettingsContent {
     /// 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<f32>,
     /// The position of project panel
     ///
@@ -526,6 +549,7 @@ pub struct ProjectPanelSettingsContent {
     /// Amount of indentation (in pixels) for nested items.
     ///
     /// Default: 20
+    #[serde(serialize_with = "serialize_optional_f32_with_two_decimal_places")]
     pub indent_size: Option<f32>,
     /// Whether to reveal it in the project panel automatically,
     /// when a corresponding project entry becomes active.

crates/settings/src/settings_store.rs 🔗

@@ -70,10 +70,6 @@ pub trait Settings: 'static + Send + Sync + Sized {
     /// and you should add a default to default.json for documentation.
     fn from_settings(content: &SettingsContent) -> Self;
 
-    /// Use [the helpers in the vscode_import module](crate::vscode_import) to apply known
-    /// equivalent settings from a vscode config to our config
-    fn import_from_vscode(_vscode: &VsCodeSettings, _current: &mut SettingsContent) {}
-
     #[track_caller]
     fn register(cx: &mut App)
     where
@@ -152,9 +148,10 @@ pub struct SettingsStore {
     _setting_file_updates: Task<()>,
     setting_file_updates_tx:
         mpsc::UnboundedSender<Box<dyn FnOnce(AsyncApp) -> LocalBoxFuture<'static, Result<()>>>>,
+    file_errors: BTreeMap<SettingsFile, String>,
 }
 
-#[derive(Clone, PartialEq, Debug)]
+#[derive(Clone, PartialEq, Eq, Debug)]
 pub enum SettingsFile {
     User,
     Server,
@@ -163,6 +160,34 @@ pub enum SettingsFile {
     Project((WorktreeId, Arc<RelPath>)),
 }
 
+impl PartialOrd for SettingsFile {
+    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
+        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,
+        }
+    }
+}
+
 #[derive(Clone)]
 pub struct Editorconfig {
     pub is_root: bool,
@@ -208,11 +233,6 @@ trait AnySettingValue: 'static + Send + Sync {
     fn all_local_values(&self) -> Vec<(WorktreeId, Arc<RelPath>, &dyn Any)>;
     fn set_global_value(&mut self, value: Box<dyn Any>);
     fn set_local_value(&mut self, root_id: WorktreeId, path: Arc<RelPath>, value: Box<dyn Any>);
-    fn import_from_vscode(
-        &self,
-        vscode_settings: &VsCodeSettings,
-        settings_content: &mut SettingsContent,
-    );
 }
 
 impl SettingsStore {
@@ -237,6 +257,7 @@ impl SettingsStore {
                     (setting_file_update)(cx.clone()).await.log_err();
                 }
             }),
+            file_errors: BTreeMap::default(),
         }
     }
 
@@ -595,6 +616,24 @@ impl SettingsStore {
 
         (SettingsFile::Default, None)
     }
+
+    fn handle_potential_file_error<R>(
+        &mut self,
+        file: SettingsFile,
+        result: Result<R>,
+    ) -> Result<R> {
+        if let Err(err) = result.as_ref() {
+            let message = err.to_string();
+            self.file_errors.insert(file, message);
+        } else {
+            self.file_errors.remove(&file);
+        }
+        return result;
+    }
+
+    pub fn error_for_file(&self, file: SettingsFile) -> Option<String> {
+        self.file_errors.get(&file).cloned()
+    }
 }
 
 impl SettingsStore {
@@ -614,10 +653,8 @@ impl SettingsStore {
     }
 
     pub fn get_vscode_edits(&self, old_text: String, vscode: &VsCodeSettings) -> String {
-        self.new_text_for_update(old_text, |settings_content| {
-            for v in self.setting_values.values() {
-                v.import_from_vscode(vscode, settings_content)
-            }
+        self.new_text_for_update(old_text, |content| {
+            content.merge_from(&vscode.settings_content())
         })
     }
 
@@ -669,7 +706,10 @@ impl SettingsStore {
         let settings: UserSettingsContent = if user_settings_content.is_empty() {
             parse_json_with_comments("{}")?
         } else {
-            parse_json_with_comments(user_settings_content)?
+            self.handle_potential_file_error(
+                SettingsFile::User,
+                parse_json_with_comments(user_settings_content),
+            )?
         };
 
         self.user_settings = Some(settings);
@@ -702,7 +742,10 @@ impl SettingsStore {
         let settings: Option<SettingsContent> = if server_settings_content.is_empty() {
             None
         } else {
-            parse_json_with_comments(server_settings_content)?
+            self.handle_potential_file_error(
+                SettingsFile::Server,
+                parse_json_with_comments(server_settings_content),
+            )?
         };
 
         // Rewrite the server settings into a content type
@@ -751,20 +794,24 @@ impl SettingsStore {
                 zed_settings_changed = self
                     .local_settings
                     .remove(&(root_id, directory_path.clone()))
-                    .is_some()
+                    .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_json_with_comments::<ProjectSettingsContent>(
-                    settings_contents,
-                )
-                .map_err(|e| InvalidSettingsError::LocalSettings {
-                    path: directory_path.join(local_settings_file_relative_path()),
-                    message: e.to_string(),
-                })?;
+                let new_settings = self
+                    .handle_potential_file_error(
+                        SettingsFile::Project((root_id, directory_path.clone())),
+                        parse_json_with_comments::<ProjectSettingsContent>(settings_contents),
+                    )
+                    .map_err(|e| InvalidSettingsError::LocalSettings {
+                        path: directory_path.join(local_settings_file_relative_path()),
+                        message: e.to_string(),
+                    })?;
                 match self.local_settings.entry((root_id, directory_path.clone())) {
                     btree_map::Entry::Vacant(v) => {
                         v.insert(SettingsContent {
@@ -942,6 +989,7 @@ impl SettingsStore {
             .to_value()
     }
 
+    // todo -> this function never fails, and should not return a result
     fn recompute_values(
         &mut self,
         changed_local_path: Option<(WorktreeId, &RelPath)>,
@@ -1129,14 +1177,6 @@ impl<T: Settings> AnySettingValue for SettingValue<T> {
             Err(ix) => self.local_values.insert(ix, (root_id, path, value)),
         }
     }
-
-    fn import_from_vscode(
-        &self,
-        vscode_settings: &VsCodeSettings,
-        settings_content: &mut SettingsContent,
-    ) {
-        T::import_from_vscode(vscode_settings, settings_content);
-    }
 }
 
 #[cfg(test)]
@@ -1179,19 +1219,6 @@ mod tests {
                 git_status: content.git_status.unwrap(),
             }
         }
-
-        fn import_from_vscode(vscode: &VsCodeSettings, content: &mut SettingsContent) {
-            let mut show = None;
-
-            vscode.bool_setting("workbench.editor.decorations.colors", &mut show);
-            if let Some(show) = show {
-                content
-                    .tabs
-                    .get_or_insert_default()
-                    .git_status
-                    .replace(show);
-            }
-        }
     }
 
     #[derive(Debug, PartialEq)]
@@ -1208,18 +1235,6 @@ mod tests {
                 preferred_line_length: content.preferred_line_length.unwrap(),
             }
         }
-
-        fn import_from_vscode(vscode: &VsCodeSettings, content: &mut SettingsContent) {
-            let content = &mut content.project.all_languages.defaults;
-
-            if let Some(size) = vscode
-                .read_value("editor.tabSize")
-                .and_then(|v| v.as_u64())
-                .and_then(|n| NonZeroU32::new(n as u32))
-            {
-                content.tab_size = Some(size);
-            }
-        }
     }
 
     #[derive(Debug, PartialEq)]
@@ -1236,16 +1251,6 @@ mod tests {
                 buffer_font_fallbacks: content.buffer_font_fallbacks.unwrap(),
             }
         }
-
-        fn import_from_vscode(vscode: &VsCodeSettings, content: &mut SettingsContent) {
-            let content = &mut content.theme;
-
-            vscode.font_family_setting(
-                "editor.fontFamily",
-                &mut content.buffer_font_family,
-                &mut content.buffer_font_fallbacks,
-            );
-        }
     }
 
     #[gpui::test]
@@ -1581,6 +1586,7 @@ mod tests {
             .unindent(),
             r#" { "editor.tabSize": 37 } "#.to_owned(),
             r#"{
+              "base_keymap": "VSCode",
               "tab_size": 37
             }
             "#
@@ -1598,6 +1604,7 @@ mod tests {
             .unindent(),
             r#"{ "editor.tabSize": 42 }"#.to_owned(),
             r#"{
+                "base_keymap": "VSCode",
                 "tab_size": 42,
                 "preferred_line_length": 99,
             }
@@ -1617,6 +1624,7 @@ mod tests {
             .unindent(),
             r#"{}"#.to_owned(),
             r#"{
+                "base_keymap": "VSCode",
                 "preferred_line_length": 99,
                 "tab_size": 42
             }
@@ -1632,8 +1640,15 @@ mod tests {
             }
             "#
             .unindent(),
-            r#"{ "workbench.editor.decorations.colors": true }"#.to_owned(),
+            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
               }
@@ -1652,6 +1667,7 @@ mod tests {
             .unindent(),
             r#"{ "editor.fontFamily": "Cascadia Code, 'Consolas', Courier New" }"#.to_owned(),
             r#"{
+              "base_keymap": "VSCode",
               "buffer_font_fallbacks": [
                 "Consolas",
                 "Courier New"
@@ -2075,4 +2091,45 @@ mod tests {
         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,
+            ]
+        )
+    }
 }

crates/settings/src/vscode_import.rs 🔗

@@ -1,10 +1,15 @@
+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::{path::Path, sync::Arc};
-
-use crate::FontFamilyName;
+use std::{
+    num::{NonZeroU32, NonZeroUsize},
+    path::{Path, PathBuf},
+    sync::Arc,
+};
 
 #[derive(Clone, Copy, PartialEq, Eq, Debug)]
 pub enum VsCodeSettingsSource {
@@ -79,83 +84,53 @@ impl VsCodeSettings {
         })
     }
 
-    pub fn read_value(&self, setting: &str) -> Option<&Value> {
+    fn read_value(&self, setting: &str) -> Option<&Value> {
         self.content.get(setting)
     }
 
-    pub fn read_string(&self, setting: &str) -> Option<&str> {
+    fn read_str(&self, setting: &str) -> Option<&str> {
         self.read_value(setting).and_then(|v| v.as_str())
     }
 
-    pub fn read_bool(&self, setting: &str) -> Option<bool> {
-        self.read_value(setting).and_then(|v| v.as_bool())
-    }
-
-    pub fn string_setting(&self, key: &str, setting: &mut Option<String>) {
-        if let Some(s) = self.content.get(key).and_then(Value::as_str) {
-            *setting = Some(s.to_owned())
-        }
-    }
-
-    pub fn bool_setting(&self, key: &str, setting: &mut Option<bool>) {
-        if let Some(s) = self.content.get(key).and_then(Value::as_bool) {
-            *setting = Some(s)
-        }
+    fn read_string(&self, setting: &str) -> Option<String> {
+        self.read_value(setting)
+            .and_then(|v| v.as_str())
+            .map(|s| s.to_owned())
     }
 
-    pub fn u32_setting(&self, key: &str, setting: &mut Option<u32>) {
-        if let Some(s) = self.content.get(key).and_then(Value::as_u64) {
-            *setting = Some(s as u32)
-        }
+    fn read_bool(&self, setting: &str) -> Option<bool> {
+        self.read_value(setting).and_then(|v| v.as_bool())
     }
 
-    pub fn u64_setting(&self, key: &str, setting: &mut Option<u64>) {
-        if let Some(s) = self.content.get(key).and_then(Value::as_u64) {
-            *setting = Some(s)
-        }
+    fn read_f32(&self, setting: &str) -> Option<f32> {
+        self.read_value(setting)
+            .and_then(|v| v.as_f64())
+            .map(|v| v as f32)
     }
 
-    pub fn usize_setting(&self, key: &str, setting: &mut Option<usize>) {
-        if let Some(s) = self.content.get(key).and_then(Value::as_u64) {
-            *setting = Some(s.try_into().unwrap())
-        }
+    fn read_u64(&self, setting: &str) -> Option<u64> {
+        self.read_value(setting).and_then(|v| v.as_u64())
     }
 
-    pub fn f32_setting(&self, key: &str, setting: &mut Option<f32>) {
-        if let Some(s) = self.content.get(key).and_then(Value::as_f64) {
-            *setting = Some(s as f32)
-        }
+    fn read_usize(&self, setting: &str) -> Option<usize> {
+        self.read_value(setting)
+            .and_then(|v| v.as_u64())
+            .and_then(|v| v.try_into().ok())
     }
 
-    pub fn from_f32_setting<T: From<f32>>(&self, key: &str, setting: &mut Option<T>) {
-        if let Some(s) = self.content.get(key).and_then(Value::as_f64) {
-            *setting = Some(T::from(s as f32))
-        }
+    fn read_u32(&self, setting: &str) -> Option<u32> {
+        self.read_value(setting)
+            .and_then(|v| v.as_u64())
+            .and_then(|v| v.try_into().ok())
     }
 
-    pub fn enum_setting<T>(
-        &self,
-        key: &str,
-        setting: &mut Option<T>,
-        f: impl FnOnce(&str) -> Option<T>,
-    ) {
-        if let Some(s) = self.content.get(key).and_then(Value::as_str).and_then(f) {
-            *setting = Some(s)
-        }
-    }
-
-    pub fn read_enum<T>(&self, key: &str, f: impl FnOnce(&str) -> Option<T>) -> Option<T> {
+    fn read_enum<T>(&self, key: &str, f: impl FnOnce(&str) -> Option<T>) -> Option<T> {
         self.content.get(key).and_then(Value::as_str).and_then(f)
     }
 
-    pub fn font_family_setting(
-        &self,
-        key: &str,
-        font_family: &mut Option<FontFamilyName>,
-        font_fallbacks: &mut Option<Vec<FontFamilyName>>,
-    ) {
+    fn read_fonts(&self, key: &str) -> (Option<FontFamilyName>, Option<Vec<FontFamilyName>>) {
         let Some(css_name) = self.content.get(key).and_then(Value::as_str) else {
-            return;
+            return (None, None);
         };
 
         let mut name_buffer = String::new();
@@ -188,12 +163,725 @@ impl VsCodeSettings {
         }
 
         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<AgentSettingsContent> {
+        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"),
+            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(true),
+                _ => 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"),
+        }
+    }
+
+    fn gutter_content(&self) -> Option<GutterContent> {
+        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<ScrollbarContent> {
+        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<SearchSettingsContent> {
+        skip_default(SearchSettingsContent {
+            include_ignored: self.read_bool("search.useIgnoreFiles"),
+            ..Default::default()
+        })
+    }
+
+    fn minimap_content(&self) -> Option<MinimapContent> {
+        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()
+        })
+    }
 
-        let mut iter = fonts.into_iter();
-        *font_family = iter.next();
-        let fallbacks: Vec<_> = iter.collect();
-        if !fallbacks.is_empty() {
-            *font_fallbacks = Some(fallbacks);
+    fn git_panel_settings_content(&self) -> Option<GitPanelSettingsContent> {
+        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,
+            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()
+                }),
         }
     }
+
+    fn file_types(&self) -> Option<HashMap<Arc<str>, ExtendingVec<String>>> {
+        // vscodes file association map is inverted from ours, so we flip the mapping before merging
+        let mut associations: HashMap<Arc<str>, ExtendingVec<String>> = 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<EditPredictionSettingsContent> {
+        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<OutlinePanelSettingsContent> {
+        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<NodeBinarySettings> {
+        // 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<GitSettings> {
+        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<Arc<str>, ContextServerSettingsContent> {
+        #[derive(Deserialize)]
+        struct VsCodeContextServerCommand {
+            command: PathBuf,
+            args: Option<Vec<String>>,
+            env: Option<HashMap<String, String>>,
+            // 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::Custom {
+                        enabled: true,
+                        command: serde_json::from_value::<VsCodeContextServerCommand>(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<ItemSettingsContent> {
+        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<PreviewTabsSettingsContent> {
+        skip_default(PreviewTabsSettingsContent {
+            enabled: self.read_bool("workbench.editor.enablePreview"),
+            enable_preview_from_file_finder: self
+                .read_bool("workbench.editor.enablePreviewFromQuickOpen"),
+            enable_preview_from_code_navigation: self
+                .read_bool("workbench.editor.enablePreviewFromCodeNavigation"),
+        })
+    }
+
+    fn tab_bar_settings_content(&self) -> Option<TabBarSettingsContent> {
+        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<StatusBarSettingsContent> {
+        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<ProjectPanelSettingsContent> {
+        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,
+            open_file_on_paste: None,
+            scrollbar: None,
+            show_diagnostics: self
+                .read_bool("problems.decorations.enabled")
+                .and_then(|b| if b { Some(ShowDiagnostics::Off) } else { None }),
+            starts_open: None,
+            sticky_scroll: 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<TelemetrySettingsContent> {
+        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<TerminalSettingsContent> {
+        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,
+            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())).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,
+        }
+    }
+
+    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,
+            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<ActivePaneModifiers> {
+        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: crate::Maybe::Unset,
+            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::<Vec<_>>()
+                })
+                .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::<Vec<_>>()
+                })
+                .filter(|r| !r.is_empty()),
+            private_files: None,
+        }
+    }
+}
+
+fn skip_default<T: Default + PartialEq>(value: T) -> Option<T> {
+    if value == T::default() {
+        None
+    } else {
+        Some(value)
+    }
 }

crates/settings_macros/Cargo.toml 🔗

@@ -18,7 +18,6 @@ default = []
 [dependencies]
 quote.workspace = true
 syn.workspace = true
-workspace-hack.workspace = true
 
 [dev-dependencies]
 settings.workspace = true

crates/settings_profile_selector/Cargo.toml 🔗

@@ -18,7 +18,6 @@ gpui.workspace = true
 picker.workspace = true
 settings.workspace = true
 ui.workspace = true
-workspace-hack.workspace = true
 workspace.workspace = true
 zed_actions.workspace = true
 

crates/settings_ui/Cargo.toml 🔗

@@ -26,6 +26,7 @@ fuzzy.workspace = true
 gpui.workspace = true
 menu.workspace = true
 paths.workspace = true
+picker.workspace = true
 project.workspace = true
 schemars.workspace = true
 search.workspace = true
@@ -36,7 +37,6 @@ theme.workspace = true
 ui_input.workspace = true
 ui.workspace = true
 util.workspace = true
-workspace-hack.workspace = true
 workspace.workspace = true
 zed_actions.workspace = true
 log.workspace = true

crates/settings_ui/src/components.rs 🔗

@@ -1,95 +1,9 @@
-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 SettingsEditor {
-    initial_text: Option<String>,
-    placeholder: Option<&'static str>,
-    confirm: Option<Box<dyn Fn(Option<String>, &mut App)>>,
-    tab_index: Option<isize>,
-}
-
-impl SettingsEditor {
-    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<String>, &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 SettingsEditor {
-    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
-            }
-        });
-
-        if let Some(tab_index) = self.tab_index {
-            editor.focus_handle(cx).tab_index(tab_index);
-        }
-
-        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)
-            .child(editor)
-            .when_some(self.confirm, |this, confirm| {
-                this.on_action::<menu::Confirm>({
-                    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);
-                    }
-                })
-            })
-    }
-}
+mod font_picker;
+mod icon_theme_picker;
+mod input_field;
+mod theme_picker;
+
+pub use font_picker::font_picker;
+pub use icon_theme_picker::icon_theme_picker;
+pub use input_field::*;
+pub use theme_picker::theme_picker;

crates/settings_ui/src/components/icon_theme_picker.rs 🔗

@@ -0,0 +1,189 @@
+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<IconThemePickerDelegate>;
+
+pub struct IconThemePickerDelegate {
+    icon_themes: Vec<SharedString>,
+    filtered_themes: Vec<StringMatch>,
+    selected_index: usize,
+    current_theme: SharedString,
+    on_theme_changed: Arc<dyn Fn(SharedString, &mut App) + 'static>,
+}
+
+impl IconThemePickerDelegate {
+    fn new(
+        current_theme: SharedString,
+        on_theme_changed: impl Fn(SharedString, &mut App) + 'static,
+        cx: &mut Context<IconThemePicker>,
+    ) -> Self {
+        let theme_registry = ThemeRegistry::global(cx);
+
+        let icon_themes: Vec<SharedString> = 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<IconThemePicker>) {
+        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<str> {
+        "Search icon theme…".into()
+    }
+
+    fn update_matches(
+        &mut self,
+        query: String,
+        _window: &mut Window,
+        cx: &mut Context<IconThemePicker>,
+    ) -> Task<()> {
+        let icon_themes = self.icon_themes.clone();
+        let current_theme = self.current_theme.clone();
+
+        let matches: Vec<StringMatch> = 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<StringMatchCandidate> = 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<IconThemePicker>,
+    ) {
+        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<IconThemePicker>) {
+        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<IconThemePicker>,
+    ) -> Option<Self::ListItem> {
+        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>,
+) -> 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()))
+}

crates/settings_ui/src/components/input_field.rs 🔗

@@ -0,0 +1,96 @@
+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<String>,
+    placeholder: Option<&'static str>,
+    confirm: Option<Box<dyn Fn(Option<String>, &mut App)>>,
+    tab_index: Option<isize>,
+}
+
+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<String>, &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::<menu::Confirm>({
+                    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);
+                    }
+                })
+            })
+    }
+}

crates/settings_ui/src/components/theme_picker.rs 🔗

@@ -0,0 +1,179 @@
+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<ThemePickerDelegate>;
+
+pub struct ThemePickerDelegate {
+    themes: Vec<SharedString>,
+    filtered_themes: Vec<StringMatch>,
+    selected_index: usize,
+    current_theme: SharedString,
+    on_theme_changed: Arc<dyn Fn(SharedString, &mut App) + 'static>,
+}
+
+impl ThemePickerDelegate {
+    fn new(
+        current_theme: SharedString,
+        on_theme_changed: impl Fn(SharedString, &mut App) + 'static,
+        cx: &mut Context<ThemePicker>,
+    ) -> 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<ThemePicker>) {
+        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<str> {
+        "Search theme…".into()
+    }
+
+    fn update_matches(
+        &mut self,
+        query: String,
+        _window: &mut Window,
+        cx: &mut Context<ThemePicker>,
+    ) -> Task<()> {
+        let themes = self.themes.clone();
+        let current_theme = self.current_theme.clone();
+
+        let matches: Vec<StringMatch> = 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<StringMatchCandidate> = 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<ThemePicker>) {
+        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<ThemePicker>) {
+        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<ThemePicker>,
+    ) -> Option<Self::ListItem> {
+        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>,
+) -> 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()))
+}

crates/settings_ui/src/page_data.rs 🔗

@@ -5,10 +5,20 @@ use strum::IntoDiscriminant as _;
 use ui::{IntoElement, SharedString};
 
 use crate::{
-    DynamicItem, LOCAL, SettingField, SettingItem, SettingsFieldMetadata, SettingsPage,
+    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<SettingsPage> {
     vec![
         SettingsPage {
@@ -16,21 +26,27 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
             items: vec![
                 SettingsPageItem::SectionHeader("General Settings"),
                 SettingsPageItem::SettingItem(SettingItem {
-                    title: "Confirm Quit",
-                    description: "Confirm before quitting Zed",
-                    field: Box::new(SettingField {
-                        pick: |settings_content| settings_content.workspace.confirm_quit.as_ref(),
-                        write: |settings_content, value| {
-                            settings_content.workspace.confirm_quit = value;
-                        },
-                    }),
-                    metadata: None,
-                    files: USER,
+                    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()?.as_ref().or(DEFAULT_EMPTY_STRING)
+                            },
+                            write: |settings_content, value| {
+                                settings_content.project.worktree.project_name = settings::Maybe::Set(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",
+                    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
@@ -46,8 +62,9 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                 }),
                 SettingsPageItem::SettingItem(SettingItem {
                     title: "On Last Window Closed",
-                    description: "What to do when the last window is 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()
                         },
@@ -60,8 +77,9 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                 }),
                 SettingsPageItem::SettingItem(SettingItem {
                     title: "Use System Path Prompts",
-                    description: "Use native OS dialogs for 'Open' and 'Save As'",
+                    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()
                         },
@@ -74,8 +92,9 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                 }),
                 SettingsPageItem::SettingItem(SettingItem {
                     title: "Use System Prompts",
-                    description: "Use native OS dialogs for confirmations",
+                    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()
                         },
@@ -88,8 +107,9 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                 }),
                 SettingsPageItem::SettingItem(SettingItem {
                     title: "Redact Private Values",
-                    description: "Hide the values of variables in private files",
+                    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()
                         },
@@ -102,9 +122,10 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                 }),
                 SettingsPageItem::SettingItem(SettingItem {
                     title: "Private Files",
-                    description: "Globs to match against file paths to determine if a file is private",
+                    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()
                             },
@@ -120,8 +141,9 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                 SettingsPageItem::SectionHeader("Workspace Restoration"),
                 SettingsPageItem::SettingItem(SettingItem {
                     title: "Restore Unsaved Buffers",
-                    description: "Whether or not to restore unsaved buffers on restart",
+                    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
@@ -140,8 +162,9 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                 }),
                 SettingsPageItem::SettingItem(SettingItem {
                     title: "Restore On Startup",
-                    description: "What to restore from the previous session when opening Zed",
+                    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()
                         },
@@ -157,9 +180,10 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                     // todo(settings_ui): Implement another setting item type that just shows an edit in settings.json
                     files: USER,
                     title: "Preview Channel",
-                    description: "Which settings should be activated only in Preview build of Zed",
+                    description: "Which settings should be activated only in Preview build of Zed.",
                     field: Box::new(
                         SettingField {
+                            json_path: Some("use_system_prompts"),
                             pick: |settings_content| {
                                 settings_content.workspace.use_system_prompts.as_ref()
                             },
@@ -174,9 +198,10 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                 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",
+                    description: "Any number of settings profiles that are temporarily applied on top of your existing user settings.",
                     field: Box::new(
                         SettingField {
+                            json_path: Some(""),
                             pick: |settings_content| {
                                 settings_content.workspace.use_system_prompts.as_ref()
                             },
@@ -191,8 +216,9 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                 SettingsPageItem::SectionHeader("Privacy"),
                 SettingsPageItem::SettingItem(SettingItem {
                     title: "Telemetry Diagnostics",
-                    description: "Send debug information like crash reports",
+                    description: "Send debug information like crash reports.",
                     field: Box::new(SettingField {
+                        json_path: Some("telemetry.diagnostics"),
                         pick: |settings_content| {
                             settings_content
                                 .telemetry
@@ -211,8 +237,9 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                 }),
                 SettingsPageItem::SettingItem(SettingItem {
                     title: "Telemetry Metrics",
-                    description: "Send anonymized usage data like what languages you're using Zed with",
+                    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
@@ -229,8 +256,9 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                 SettingsPageItem::SectionHeader("Auto Update"),
                 SettingsPageItem::SettingItem(SettingItem {
                     title: "Auto Update",
-                    description: "Whether or not to automatically check for updates",
+                    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;
@@ -249,10 +277,11 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                     discriminant: SettingItem {
                         files: USER,
                         title: "Theme Mode",
-                        description: "How to select the theme",
+                        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(&<<settings::ThemeSelection as strum::IntoDiscriminant>::Discriminant as strum::VariantArray>::VARIANTS[
+                                Some(&dynamic_variants::<settings::ThemeSelection>()[
                                     settings_content
                                         .theme
                                         .theme
@@ -263,7 +292,9 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                                 let Some(value) = value else {
                                     return;
                                 };
-                                let settings_value = settings_content.theme.theme.as_mut().expect("Has Default");
+                                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 {
@@ -298,14 +329,15 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                     pick_discriminant: |settings_content| {
                         Some(settings_content.theme.theme.as_ref()?.discriminant() as usize)
                     },
-                    fields: <<settings::ThemeSelection as strum::IntoDiscriminant>::Discriminant as strum::VariantArray>::VARIANTS.into_iter().map(|variant| {
+                    fields: dynamic_variants::<settings::ThemeSelection>().into_iter().map(|variant| {
                         match variant {
                             settings::ThemeSelectionDiscriminants::Static => vec![
                                 SettingItem {
                                     files: USER,
                                     title: "Theme Name",
-                                    description: "The Name Of The Theme To Use",
+                                    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),
@@ -331,8 +363,9 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                                 SettingItem {
                                     files: USER,
                                     title: "Mode",
-                                    description: "How To Determine Whether to Use a Light or Dark Theme",
+                                    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),
@@ -356,8 +389,9 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                                 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 The System Is In Light Mode",
+                                    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),
@@ -381,8 +415,9 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                                 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 The System Is In Dark Mode",
+                                    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),
@@ -407,26 +442,181 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                         }
                     }).collect(),
                 }),
-                SettingsPageItem::SettingItem(SettingItem {
-                    files: USER,
-                    title: "Icon Theme",
-                    // todo(settings_ui)
-                    // This description is misleading because the icon theme is used in more places than the file explorer)
-                    description: "Choose the icon theme for file explorer",
-                    field: Box::new(
-                        SettingField {
-                            pick: |settings_content| settings_content.theme.icon_theme.as_ref(),
-                            write: |settings_content, value|{  settings_content.theme.icon_theme = value;},
+                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::IconThemeSelection>()[
+                                    settings_content
+                                        .theme
+                                        .icon_theme
+                                        .as_ref()?
+                                        .discriminant() as usize])
+                            },
+                            write: |settings_content, value| {
+                                let Some(value) = value else {
+                                    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::ThemeMode::Light => light.clone(),
+                                                    theme::ThemeMode::Dark => dark.clone(),
+                                                    theme::ThemeMode::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::ThemeMode::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::<settings::IconThemeSelection>().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,
+                                }
+                            ],
                         }
-                        .unimplemented(),
-                    ),
-                    metadata: None,
+                    }).collect(),
                 }),
                 SettingsPageItem::SectionHeader("Buffer Font"),
                 SettingsPageItem::SettingItem(SettingItem {
                     title: "Font Family",
-                    description: "Font family for editor text",
+                    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;},
                     }),
@@ -435,8 +625,9 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                 }),
                 SettingsPageItem::SettingItem(SettingItem {
                     title: "Font Size",
-                    description: "Font size for editor text",
+                    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;},
                     }),
@@ -445,32 +636,90 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                 }),
                 SettingsPageItem::SettingItem(SettingItem {
                     title: "Font Weight",
-                    description: "Font weight for editor text (100-900)",
+                    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,
                 }),
-                // todo(settings_ui): This needs custom ui
-                SettingsPageItem::SettingItem(SettingItem {
-                    files: USER,
-                    title: "Line Height",
-                    description: "Line height for editor text",
-                    field: Box::new(
-                        SettingField {
+                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| {
-                                settings_content.theme.buffer_line_height.as_ref()
+                                Some(&dynamic_variants::<settings::BufferLineHeight>()[
+                                    settings_content
+                                        .theme
+                                        .buffer_line_height
+                                        .as_ref()?
+                                        .discriminant() as usize])
                             },
                             write: |settings_content, value| {
-                                settings_content.theme.buffer_line_height = value;
-
+                                let Some(value) = value else {
+                                    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::<settings::BufferLineHeight>().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,
+                                }
+                            ],
                         }
-                        .unimplemented(),
-                    ),
-                    metadata: None,
+                    }).collect(),
                 }),
                 SettingsPageItem::SettingItem(SettingItem {
                     files: USER,
@@ -478,6 +727,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                     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()
                             },
@@ -496,6 +746,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                     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()
                             },
@@ -511,8 +762,9 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                 SettingsPageItem::SectionHeader("UI Font"),
                 SettingsPageItem::SettingItem(SettingItem {
                     title: "Font Family",
-                    description: "Font family for UI elements",
+                    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;},
                     }),
@@ -521,8 +773,9 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                 }),
                 SettingsPageItem::SettingItem(SettingItem {
                     title: "Font Size",
-                    description: "Font size for UI elements",
+                    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;},
                     }),
@@ -531,8 +784,9 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                 }),
                 SettingsPageItem::SettingItem(SettingItem {
                     title: "Font Weight",
-                    description: "Font weight for UI elements (100-900)",
+                    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;},
                     }),
@@ -542,9 +796,10 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                 SettingsPageItem::SettingItem(SettingItem {
                     files: USER,
                     title: "Font Features",
-                    description: "The OpenType features to enable for rendering in UI elements.",
+                    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()
                             },
@@ -563,6 +818,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                     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()
                             },
@@ -580,6 +836,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                     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
@@ -594,8 +851,9 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                 }),
                 SettingsPageItem::SettingItem(SettingItem {
                     title: "Buffer Font Size",
-                    description: "Font size for user messages text in the agent panel",
+                    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
@@ -614,8 +872,9 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                 SettingsPageItem::SectionHeader("Cursor"),
                 SettingsPageItem::SettingItem(SettingItem {
                     title: "Multi Cursor Modifier",
-                    description: "Modifier key for adding multiple cursors",
+                    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()
                         },
@@ -629,8 +888,9 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                 }),
                 SettingsPageItem::SettingItem(SettingItem {
                     title: "Cursor Blink",
-                    description: "Whether the cursor blinks in the editor",
+                    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;},
                     }),
@@ -639,8 +899,9 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                 }),
                 SettingsPageItem::SettingItem(SettingItem {
                     title: "Cursor Shape",
-                    description: "Cursor shape for the editor",
+                    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;},
                     }),
@@ -649,8 +910,9 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                 }),
                 SettingsPageItem::SettingItem(SettingItem {
                     title: "Hide Mouse",
-                    description: "When to hide the mouse cursor",
+                    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;},
                     }),
@@ -660,8 +922,9 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                 SettingsPageItem::SectionHeader("Highlighting"),
                 SettingsPageItem::SettingItem(SettingItem {
                     title: "Unnecessary Code Fade",
-                    description: "How much to fade out unused code (0.0 - 0.9)",
+                    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()
                         },
@@ -675,8 +938,9 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                 }),
                 SettingsPageItem::SettingItem(SettingItem {
                     title: "Current Line Highlight",
-                    description: "How to highlight the current line",
+                    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()
                         },
@@ -690,8 +954,9 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                 }),
                 SettingsPageItem::SettingItem(SettingItem {
                     title: "Selection Highlight",
-                    description: "Highlight all occurrences of selected text",
+                    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()
                         },
@@ -705,8 +970,9 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                 }),
                 SettingsPageItem::SettingItem(SettingItem {
                     title: "Rounded Selection",
-                    description: "Whether the text selection should have rounded corners",
+                    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;},
                     }),
@@ -715,8 +981,9 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                 }),
                 SettingsPageItem::SettingItem(SettingItem {
                     title: "Minimum Contrast For Highlights",
-                    description: "The minimum APCA perceptual contrast to maintain when rendering text over highlight backgrounds",
+                    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
@@ -734,8 +1001,9 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                 SettingsPageItem::SectionHeader("Guides"),
                 SettingsPageItem::SettingItem(SettingItem {
                     title: "Show Wrap Guides",
-                    description: "Show wrap guides (vertical rulers)",
+                    description: "Show wrap guides (vertical rulers).",
                     field: Box::new(SettingField {
+                        json_path: Some("show_wrap_guides"),
                         pick: |settings_content| {
                             settings_content
                                 .project
@@ -754,14 +1022,15 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                         },
                     }),
                     metadata: None,
-                    files: USER | LOCAL,
+                    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",
+                    description: "Character counts at which to show wrap guides.",
                     field: Box::new(
                         SettingField {
+                            json_path: Some("wrap_guides"),
                             pick: |settings_content| {
                                 settings_content
                                     .project
@@ -772,13 +1041,12 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                             },
                             write: |settings_content, value| {
                                 settings_content.project.all_languages.defaults.wrap_guides = value;
-
                             },
                         }
                         .unimplemented(),
                     ),
                     metadata: None,
-                    files: USER | LOCAL,
+                    files: USER | PROJECT,
                 }),
             ],
         },
@@ -788,8 +1056,9 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                 SettingsPageItem::SectionHeader("Base Keymap"),
                 SettingsPageItem::SettingItem(SettingItem {
                     title: "Base Keymap",
-                    description: "The name of a base set of key bindings to use",
+                    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;
@@ -806,8 +1075,9 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                 // behavior to have them both enabled at the same time
                 SettingsPageItem::SettingItem(SettingItem {
                     title: "Vim Mode",
-                    description: "Enable vim modes and key bindings",
+                    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;
@@ -818,8 +1088,9 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                 }),
                 SettingsPageItem::SettingItem(SettingItem {
                     title: "Helix Mode",
-                    description: "Enable helix modes and key bindings",
+                    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;
@@ -835,28 +1106,95 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
             items: {
                 let mut items = vec![
                     SettingsPageItem::SectionHeader("Auto Save"),
-                    SettingsPageItem::SettingItem(SettingItem {
-                        title: "Auto Save Mode",
-                        description: "When to Auto Save Buffer Changes",
-                        field: Box::new(
-                            SettingField {
+                    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| {
-                                    settings_content.workspace.autosave.as_ref()
+                                    Some(&dynamic_variants::<settings::AutosaveSetting>()[
+                                        settings_content
+                                            .workspace
+                                            .autosave
+                                            .as_ref()?
+                                            .discriminant() as usize])
                                 },
                                 write: |settings_content, value| {
-                                    settings_content.workspace.autosave = value;
+                                    let Some(value) = value else {
+                                        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::<settings::AutosaveSetting>().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 {
+                                                    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![],
                             }
-                            .unimplemented(),
-                        ),
-                        metadata: None,
-                        files: USER,
+                        }).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",
+                        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()
                             },
@@ -869,8 +1207,9 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                     }),
                     SettingsPageItem::SettingItem(SettingItem {
                         title: "Expand Excerpt Lines",
-                        description: "How many lines to expand the multibuffer excerpts by default",
+                        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()
                             },
@@ -883,8 +1222,9 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                     }),
                     SettingsPageItem::SettingItem(SettingItem {
                         title: "Excerpt Context Lines",
-                        description: "How many lines of context to provide in multibuffer excerpts by default",
+                        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()
                             },
@@ -897,8 +1237,9 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                     }),
                     SettingsPageItem::SettingItem(SettingItem {
                         title: "Expand Outlines With Depth",
-                        description: "Default depth to expand outline items in the current file",
+                        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
@@ -920,8 +1261,9 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                     SettingsPageItem::SectionHeader("Scrolling"),
                     SettingsPageItem::SettingItem(SettingItem {
                         title: "Scroll Beyond Last Line",
-                        description: "Whether the editor will scroll beyond the 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()
                             },
@@ -934,8 +1276,9 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                     }),
                     SettingsPageItem::SettingItem(SettingItem {
                         title: "Vertical Scroll Margin",
-                        description: "The number of lines to keep above/below the cursor when auto-scrolling",
+                        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()
                             },

crates/settings_ui/src/settings_ui.rs 🔗

@@ -6,13 +6,13 @@ use editor::{Editor, EditorEvent};
 use feature_flags::FeatureFlag;
 use fuzzy::StringMatchCandidate;
 use gpui::{
-    Action, App, Div, Entity, FocusHandle, Focusable, Global, ListState, ReadGlobal as _,
-    ScrollHandle, Stateful, Subscription, Task, TitlebarOptions, UniformListScrollHandle, Window,
-    WindowBounds, WindowHandle, WindowOptions, actions, div, list, point, prelude::*, px, size,
-    uniform_list,
+    Action, App, DEFAULT_ADDITIONAL_WINDOW_SIZE, Div, Entity, FocusHandle, Focusable, Global,
+    ListState, ReadGlobal as _, ScrollHandle, Stateful, Subscription, Task, TitlebarOptions,
+    UniformListScrollHandle, Window, WindowBounds, WindowHandle, WindowOptions, actions, div, list,
+    point, prelude::*, px, uniform_list,
 };
 use heck::ToTitleCase as _;
-use project::WorktreeId;
+use project::{Project, WorktreeId};
 use schemars::JsonSchema;
 use serde::Deserialize;
 use settings::{Settings, SettingsContent, SettingsStore};
@@ -27,16 +27,16 @@ use std::{
 };
 use title_bar::platform_title_bar::PlatformTitleBar;
 use ui::{
-    ContextMenu, Divider, DividerColor, DropdownMenu, DropdownStyle, IconButtonShape, KeyBinding,
-    KeybindingHint, PopoverMenu, Switch, SwitchColor, Tooltip, TreeViewItem, WithScrollbar,
-    prelude::*,
+    Banner, ContextMenu, Divider, DividerColor, DropdownMenu, DropdownStyle, IconButtonShape,
+    KeyBinding, KeybindingHint, PopoverMenu, Switch, SwitchColor, Tooltip, TreeViewItem,
+    WithScrollbar, prelude::*,
 };
 use ui_input::{NumberField, NumberFieldType};
 use util::{ResultExt as _, paths::PathStyle, rel_path::RelPath};
-use workspace::{OpenOptions, OpenVisible, Workspace, client_side_decorations};
-use zed_actions::OpenSettings;
+use workspace::{AppState, OpenOptions, OpenVisible, Workspace, client_side_decorations};
+use zed_actions::{OpenSettings, OpenSettingsAt};
 
-use crate::components::SettingsEditor;
+use crate::components::{SettingsInputField, font_picker, icon_theme_picker, theme_picker};
 
 const NAVBAR_CONTAINER_TAB_INDEX: isize = 0;
 const NAVBAR_GROUP_TAB_INDEX: isize = 1;
@@ -86,6 +86,25 @@ struct FocusFile(pub u32);
 struct SettingField<T: 'static> {
     pick: fn(&SettingsContent) -> Option<&T>,
     write: fn(&mut SettingsContent, Option<T>),
+
+    /// 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<T: 'static> Clone for SettingField<T> {
@@ -116,6 +135,7 @@ impl<T: 'static> SettingField<T> {
         SettingField {
             pick: |_| Some(&UnimplementedSettingField),
             write: |_, _| unreachable!(),
+            json_path: None,
         }
     }
 }
@@ -132,6 +152,8 @@ trait AnySettingField {
         file_set_in: &settings::SettingsFile,
         cx: &App,
     ) -> Option<Box<dyn Fn(&mut App)>>;
+
+    fn json_path(&self) -> Option<&'static str>;
 }
 
 impl<T: PartialEq + Clone + Send + Sync + 'static> AnySettingField for SettingField<T> {
@@ -197,6 +219,10 @@ impl<T: PartialEq + Clone + Send + Sync + 'static> AnySettingField for SettingFi
             .log_err();
         }));
     }
+
+    fn json_path(&self) -> Option<&'static str> {
+        self.json_path
+    }
 }
 
 #[derive(Default, Clone)]
@@ -344,13 +370,26 @@ impl FeatureFlag for SettingsUiFeatureFlag {
 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::<Workspace>()
+                    .expect("Workspaces are root Windows");
+                open_settings_editor(workspace, Some(&path), window_handle, cx);
+            },
+        );
+    })
+    .detach();
+
     cx.observe_new(|workspace: &mut workspace::Workspace, _, _| {
         workspace.register_action(|workspace, _: &OpenSettings, window, cx| {
             let window_handle = window
                 .window_handle()
                 .downcast::<Workspace>()
                 .expect("Workspaces are root Windows");
-            open_settings_editor(workspace, window_handle, cx);
+            open_settings_editor(workspace, None, window_handle, cx);
         });
     })
     .detach();
@@ -370,6 +409,7 @@ fn init_renderers(cx: &mut App) {
         })
         .add_basic_renderer::<bool>(render_toggle_button)
         .add_basic_renderer::<String>(render_text_field)
+        .add_basic_renderer::<SharedString>(render_text_field)
         .add_basic_renderer::<settings::SaturatingBool>(render_toggle_button)
         .add_basic_renderer::<settings::CursorShape>(render_dropdown)
         .add_basic_renderer::<settings::RestoreOnStartupBehavior>(render_dropdown)
@@ -417,7 +457,10 @@ fn init_renderers(cx: &mut App) {
         .add_basic_renderer::<NonZero<usize>>(render_number_field)
         .add_basic_renderer::<NonZeroU32>(render_number_field)
         .add_basic_renderer::<settings::CodeFade>(render_number_field)
+        .add_basic_renderer::<settings::DelayMs>(render_number_field)
         .add_basic_renderer::<gpui::FontWeight>(render_number_field)
+        .add_basic_renderer::<settings::CenteredPaddingSettings>(render_number_field)
+        .add_basic_renderer::<settings::InactiveOpacity>(render_number_field)
         .add_basic_renderer::<settings::MinimumContrast>(render_number_field)
         .add_basic_renderer::<settings::ShowScrollbar>(render_dropdown)
         .add_basic_renderer::<settings::ScrollbarDiagnostics>(render_dropdown)
@@ -437,15 +480,76 @@ fn init_renderers(cx: &mut App) {
         .add_basic_renderer::<settings::ThemeSelectionDiscriminants>(render_dropdown)
         .add_basic_renderer::<settings::ThemeMode>(render_dropdown)
         .add_basic_renderer::<settings::ThemeName>(render_theme_picker)
+        .add_basic_renderer::<settings::IconThemeSelectionDiscriminants>(render_dropdown)
+        .add_basic_renderer::<settings::IconThemeName>(render_icon_theme_picker)
+        .add_basic_renderer::<settings::BufferLineHeightDiscriminants>(render_dropdown)
+        .add_basic_renderer::<settings::AutosaveSettingDiscriminants>(render_dropdown)
+        .add_basic_renderer::<settings::WorkingDirectoryDiscriminants>(render_dropdown)
+        .add_basic_renderer::<settings::MaybeDiscriminants>(render_dropdown)
+        .add_basic_renderer::<settings::IncludeIgnoredContent>(render_dropdown)
+        .add_basic_renderer::<settings::ShowIndentGuides>(render_dropdown)
+        .add_basic_renderer::<settings::ShellDiscriminants>(render_dropdown)
         // please semicolon stay on next line
         ;
 }
 
 pub fn open_settings_editor(
     _workspace: &mut Workspace,
+    path: Option<&str>,
     workspace_handle: WindowHandle<Workspace>,
     cx: &mut App,
 ) {
+    /// Assumes a settings GUI window is already open
+    fn open_path(
+        path: &str,
+        settings_window: &mut SettingsWindow,
+        window: &mut Window,
+        cx: &mut Context<SettingsWindow>,
+    ) {
+        if path.starts_with("languages.$(language)") {
+            log::error!("language-specific settings links are not currently supported");
+            return;
+        }
+
+        settings_window.current_file = SettingsUiFile::User;
+        settings_window.build_ui(window, cx);
+
+        let mut item_info = None;
+        'search: for (nav_entry_index, entry) in settings_window.navbar_entries.iter().enumerate() {
+            if entry.is_root {
+                continue;
+            }
+            let page_index = entry.page_index;
+            let header_index = entry
+                .item_index
+                .expect("non-root entries should have an item index");
+            for item_index in header_index + 1..settings_window.pages[page_index].items.len() {
+                let item = &settings_window.pages[page_index].items[item_index];
+                if let SettingsPageItem::SectionHeader(_) = item {
+                    break;
+                }
+                if let SettingsPageItem::SettingItem(item) = item {
+                    if item.field.json_path() == Some(path) {
+                        if !item.files.contains(USER) {
+                            log::error!("Found item {}, but it is not a user setting", path);
+                            return;
+                        }
+                        item_info = Some((item_index, nav_entry_index));
+                        break 'search;
+                    }
+                }
+            }
+        }
+        let Some((item_index, navbar_entry_index)) = item_info else {
+            log::error!("Failed to find item for {}", path);
+            return;
+        };
+
+        settings_window.open_navbar_entry_page(navbar_entry_index);
+        window.focus(&settings_window.focus_handle_for_content_element(item_index, cx));
+        settings_window.scroll_to_content_item(item_index, window, cx);
+    }
+
     let existing_window = cx
         .windows()
         .into_iter()
@@ -455,8 +559,10 @@ pub fn open_settings_editor(
         existing_window
             .update(cx, |settings_window, window, cx| {
                 settings_window.original_window = Some(workspace_handle);
-                settings_window.observe_last_window_close(cx);
                 window.activate_window();
+                if let Some(path) = path {
+                    open_path(path, settings_window, window, cx);
+                }
             })
             .ok();
         return;
@@ -464,14 +570,21 @@ pub fn open_settings_editor(
 
     // 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 = size(px(900.), px(750.)); // 4:3 Aspect Ratio
+        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<Pixels> = default_bounds.map(|axis| axis * scale_factor);
 
+        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 {
@@ -484,11 +597,22 @@ pub fn open_settings_editor(
                 is_movable: true,
                 kind: gpui::WindowKind::Floating,
                 window_background: cx.theme().window_background_appearance(),
+                window_decorations: Some(window_decorations),
                 window_min_size: Some(scaled_bounds),
                 window_bounds: Some(WindowBounds::centered(scaled_bounds, cx)),
                 ..Default::default()
             },
-            |window, cx| cx.new(|cx| SettingsWindow::new(Some(workspace_handle), window, cx)),
+            |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, settings_window, window, cx);
+                    }
+                });
+
+                settings_window
+            },
         )
         .log_err();
     });
@@ -518,7 +642,6 @@ pub struct SettingsWindow {
     title_bar: Option<Entity<PlatformTitleBar>>,
     original_window: Option<WindowHandle<Workspace>>,
     files: Vec<(SettingsUiFile, FocusHandle)>,
-    drop_down_file: Option<usize>,
     worktree_root_dirs: HashMap<WorktreeId, String>,
     current_file: SettingsUiFile,
     pages: Vec<SettingsPage>,
@@ -541,7 +664,6 @@ pub struct SettingsWindow {
     content_focus_handle: Entity<NonFocusableHandle>,
     files_focus_handle: FocusHandle,
     search_index: Option<Arc<SearchIndex>>,
-    visible_items: Vec<usize>,
     list_state: ListState,
 }
 
@@ -612,6 +734,7 @@ impl SettingsPageItem {
         cx: &mut Context<SettingsWindow>,
     ) -> AnyElement {
         let file = settings_window.current_file.clone();
+
         let border_variant = cx.theme().colors().border_variant;
         let apply_padding = |element: Stateful<Div>| -> Stateful<Div> {
             let element = element.pt_4();
@@ -621,12 +744,14 @@ impl SettingsPageItem {
                 element.pb_4().border_b_1().border_color(border_variant)
             }
         };
+
         let mut render_setting_item_inner =
-            |setting_item: &SettingItem, cx: &mut Context<SettingsWindow>| {
+            |setting_item: &SettingItem, padding: bool, cx: &mut Context<SettingsWindow>| {
                 let renderer = cx.default_global::<SettingFieldRenderer>().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 =
@@ -665,8 +790,15 @@ impl SettingsPageItem {
                     ),
                 };
 
-                (field.map(apply_padding), field_renderer_or_warning.is_ok())
+                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()
@@ -680,15 +812,13 @@ impl SettingsPageItem {
                 .child(Divider::horizontal().color(DividerColor::BorderFaded))
                 .into_any_element(),
             SettingsPageItem::SettingItem(setting_item) => {
-                render_setting_item_inner(setting_item, cx)
-                    .0
-                    .into_any_element()
+                let (field_with_padding, _) = render_setting_item_inner(setting_item, true, cx);
+                field_with_padding.into_any_element()
             }
             SettingsPageItem::SubPageLink(sub_page_link) => h_flex()
                 .id(sub_page_link.title.clone())
                 .w_full()
                 .min_w_0()
-                .gap_2()
                 .justify_between()
                 .map(apply_padding)
                 .child(
@@ -707,7 +837,7 @@ impl SettingsPageItem {
                     .icon_position(IconPosition::End)
                     .icon_color(Color::Muted)
                     .icon_size(IconSize::Small)
-                    .style(ButtonStyle::Outlined)
+                    .style(ButtonStyle::OutlinedGhost)
                     .size(ButtonSize::Medium)
                     .on_click({
                         let sub_page_link = sub_page_link.clone();
@@ -742,18 +872,42 @@ impl SettingsPageItem {
                 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, cx);
-                let mut content = v_flex()
-                    .gap_2()
-                    .id("dynamic-item")
-                    .child(discriminant_element);
+                    render_setting_item_inner(discriminant_setting_item, true, cx);
+
+                let has_sub_fields =
+                    rendered_ok && discriminant.map(|d| !fields[d].is_empty()).unwrap_or(false);
+
+                let discriminant_element = if has_sub_fields {
+                    discriminant_element.pb_4().border_b_0()
+                } else {
+                    discriminant_element
+                };
+
+                let mut content = v_flex().id("dynamic-item").child(discriminant_element);
+
                 if rendered_ok {
                     let discriminant =
                         discriminant.expect("This should be Some if rendered_ok is true");
                     let sub_fields = &fields[discriminant];
-                    for field in sub_fields {
-                        content = content.child(render_setting_item_inner(field, cx).0.pl_6());
+                    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, cx);
+
+                        content = content.child(
+                            raw_field
+                                .p_4()
+                                .border_x_1()
+                                .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)),
+                        );
                     }
                 }
 
@@ -777,7 +931,6 @@ fn render_settings_item(
     h_flex()
         .id(setting_item.title)
         .min_w_0()
-        .gap_2()
         .justify_between()
         .child(
             v_flex()
@@ -861,7 +1014,7 @@ impl std::fmt::Debug for FileMask {
         if self.contains(USER) {
             items.push("USER");
         }
-        if self.contains(LOCAL) {
+        if self.contains(PROJECT) {
             items.push("LOCAL");
         }
         if self.contains(SERVER) {
@@ -873,7 +1026,7 @@ impl std::fmt::Debug for FileMask {
 }
 
 const USER: FileMask = FileMask(1 << 0);
-const LOCAL: FileMask = FileMask(1 << 2);
+const PROJECT: FileMask = FileMask(1 << 2);
 const SERVER: FileMask = FileMask(1 << 3);
 
 impl std::ops::BitAnd for FileMask {
@@ -983,14 +1136,14 @@ impl SettingsUiFile {
     fn mask(&self) -> FileMask {
         match self {
             SettingsUiFile::User => USER,
-            SettingsUiFile::Project(_) => LOCAL,
+            SettingsUiFile::Project(_) => PROJECT,
             SettingsUiFile::Server(_) => SERVER,
         }
     }
 }
 
 impl SettingsWindow {
-    pub fn new(
+    fn new(
         original_window: Option<WindowHandle<Workspace>>,
         window: &mut Window,
         cx: &mut Context<Self>,
@@ -1027,6 +1180,60 @@ impl SettingsWindow {
         })
         .detach();
 
+        cx.on_window_closed(|cx| {
+            if let Some(existing_window) = cx
+                .windows()
+                .into_iter()
+                .find_map(|window| window.downcast::<SettingsWindow>())
+                && cx.windows().len() == 1
+            {
+                cx.update_window(*existing_window, |_, window, _| {
+                    window.remove_window();
+                })
+                .ok();
+            }
+        })
+        .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::<Vec<_>>()
+            {
+                cx.subscribe_in(&project, window, Self::handle_project_event)
+                    .detach();
+            }
+        } else {
+            log::error!("App state doesn't exist when creating a new settings window");
+        }
+
+        let this_weak = cx.weak_entity();
+        cx.observe_new::<Project>({
+            move |_, window, cx| {
+                let project = cx.entity();
+                let Some(window) = window else {
+                    return;
+                };
+
+                this_weak
+                    .update(cx, |_, cx| {
+                        cx.subscribe_in(&project, window, Self::handle_project_event)
+                            .detach();
+                    })
+                    .ok();
+            }
+        })
+        .detach();
+
         let title_bar = if !cfg!(target_os = "macos") {
             Some(cx.new(|cx| PlatformTitleBar::new("settings-title-bar", cx)))
         } else {
@@ -1034,7 +1241,7 @@ impl SettingsWindow {
         };
 
         // high overdraw value so the list scrollbar len doesn't change too much
-        let list_state = gpui::ListState::new(0, gpui::ListAlignment::Top, px(100.0)).measure_all();
+        let list_state = gpui::ListState::new(0, gpui::ListAlignment::Top, px(0.0)).measure_all();
         list_state.set_scroll_handler(|_, _, _| {});
 
         let mut this = Self {
@@ -1043,7 +1250,7 @@ impl SettingsWindow {
 
             worktree_root_dirs: HashMap::default(),
             files: vec![],
-            drop_down_file: None,
+
             current_file: current_file,
             pages: vec![],
             navbar_entries: vec![],
@@ -1074,12 +1281,9 @@ impl SettingsWindow {
                 .tab_index(HEADER_CONTAINER_TAB_INDEX)
                 .tab_stop(false),
             search_index: None,
-            visible_items: Vec::default(),
             list_state,
         };
 
-        this.observe_last_window_close(cx);
-
         this.fetch_files(window, cx);
         this.build_ui(window, cx);
         this.build_search_index();
@@ -1091,21 +1295,21 @@ impl SettingsWindow {
         this
     }
 
-    fn observe_last_window_close(&mut self, cx: &mut App) {
-        cx.on_window_closed(|cx| {
-            if let Some(existing_window) = cx
-                .windows()
-                .into_iter()
-                .find_map(|window| window.downcast::<SettingsWindow>())
-                && cx.windows().len() == 1
-            {
-                cx.update_window(*existing_window, |_, window, _| {
-                    window.remove_window();
-                })
-                .ok();
+    fn handle_project_event(
+        &mut self,
+        _: &Entity<Project>,
+        event: &project::Event,
+        window: &mut Window,
+        cx: &mut Context<SettingsWindow>,
+    ) {
+        match event {
+            project::Event::WorktreeRemoved(_) | project::Event::WorktreeAdded(_) => {
+                cx.defer_in(window, |this, window, cx| {
+                    this.fetch_files(window, cx);
+                });
             }
-        })
-        .detach();
+            _ => {}
+        }
     }
 
     fn toggle_navbar_entry(&mut self, nav_entry_index: usize) {
@@ -1116,16 +1320,8 @@ impl SettingsWindow {
 
         let expanded = &mut self.navbar_entries[nav_entry_index].expanded;
         *expanded = !*expanded;
-        let expanded = *expanded;
-
-        let toggle_page_index = self.page_index_from_navbar_index(nav_entry_index);
-        let selected_page_index = self.page_index_from_navbar_index(self.navbar_entry);
-        // if currently selected page is a child of the parent page we are folding,
-        // set the current page to the parent page
-        if !expanded && selected_page_index == toggle_page_index {
-            self.navbar_entry = nav_entry_index;
-            // note: not opening page. Toggling does not change content just selected page
-        }
+        self.navbar_entry = nav_entry_index;
+        self.reset_list_state();
     }
 
     fn build_navbar(&mut self, cx: &App) {
@@ -1175,7 +1371,7 @@ impl SettingsWindow {
                 move |this: &mut SettingsWindow,
                       window: &mut Window,
                       cx: &mut Context<SettingsWindow>| {
-                    this.open_and_scroll_to_navbar_entry(entry_index, window, cx, false);
+                    this.open_and_scroll_to_navbar_entry(entry_index, None, false, window, cx);
                 },
             );
             focus_subscriptions.push(subscription);
@@ -1480,14 +1676,14 @@ impl SettingsWindow {
 
     fn reset_list_state(&mut self) {
         // plus one for the title
-        self.visible_items = self.visible_page_items().map(|(index, _)| index).collect();
+        let mut visible_items_count = self.visible_page_items().count();
 
-        if self.visible_items.is_empty() {
-            self.list_state.reset(0);
-        } else {
+        if visible_items_count > 0 {
             // show page title if page is non empty
-            self.list_state.reset(self.visible_items.len() + 1);
+            visible_items_count += 1;
         }
+
+        self.list_state.reset(visible_items_count);
     }
 
     fn build_ui(&mut self, window: &mut Window, cx: &mut Context<SettingsWindow>) {
@@ -1549,14 +1745,48 @@ impl SettingsWindow {
                 .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).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, false, cx);
+            self.change_file(0, window, cx);
         }
     }
 
@@ -1586,21 +1816,12 @@ impl SettingsWindow {
         self.open_navbar_entry_page(first_navbar_entry_index);
     }
 
-    fn change_file(
-        &mut self,
-        ix: usize,
-        window: &mut Window,
-        drop_down_file: bool,
-        cx: &mut Context<SettingsWindow>,
-    ) {
+    fn change_file(&mut self, ix: usize, window: &mut Window, cx: &mut Context<SettingsWindow>) {
         if ix >= self.files.len() {
             self.current_file = SettingsUiFile::User;
             self.build_ui(window, cx);
             return;
         }
-        if drop_down_file {
-            self.drop_down_file = Some(ix);
-        }
 
         if self.files[ix].0 == self.current_file {
             return;
@@ -1613,7 +1834,7 @@ impl SettingsWindow {
             .visible_navbar_entries()
             .any(|(index, _)| index == self.navbar_entry)
         {
-            self.open_and_scroll_to_navbar_entry(self.navbar_entry, window, cx, true);
+            self.open_and_scroll_to_navbar_entry(self.navbar_entry, None, true, window, cx);
         } else {
             self.open_first_nav_page();
         };
@@ -1624,7 +1845,7 @@ impl SettingsWindow {
         window: &mut Window,
         cx: &mut Context<SettingsWindow>,
     ) -> impl IntoElement {
-        const OVERFLOW_LIMIT: usize = 1;
+        static OVERFLOW_LIMIT: usize = 1;
 
         let file_button =
             |ix, file: &SettingsUiFile, focus_handle, cx: &mut Context<SettingsWindow>| {
@@ -1639,7 +1860,7 @@ impl SettingsWindow {
                 .on_click(cx.listener({
                     let focus_handle = focus_handle.clone();
                     move |this, _: &gpui::ClickEvent, window, cx| {
-                        this.change_file(ix, window, false, cx);
+                        this.change_file(ix, window, cx);
                         focus_handle.focus(window);
                     }
                 }))
@@ -1664,48 +1885,58 @@ impl SettingsWindow {
                         ),
                     )
                     .when(self.files.len() > OVERFLOW_LIMIT, |div| {
-                        div.children(
-                            self.files
-                                .iter()
-                                .enumerate()
-                                .skip(OVERFLOW_LIMIT)
-                                .find(|(_, (file, _))| file == &self.current_file)
-                                .map(|(ix, (file, focus_handle))| {
-                                    file_button(ix, file, focus_handle, cx)
-                                })
-                                .or_else(|| {
-                                    let ix = self.drop_down_file.unwrap_or(OVERFLOW_LIMIT);
-                                    self.files.get(ix).map(|(file, focus_handle)| {
-                                        file_button(ix, file, focus_handle, cx)
-                                    })
-                                }),
-                        )
-                        .when(
-                            self.files.len() > OVERFLOW_LIMIT + 1,
-                            |div| {
+                        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 (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 (ix, (file, focus_handle)) in self
+                                            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(
-                                                    self.display_name(file)
+                                                    display_name
                                                         .expect("Files should always have a name"),
                                                     None,
                                                     {
                                                         let this = this.clone();
-                                                        let focus_handle = focus_handle.clone();
                                                         move |window, cx| {
                                                             this.update(cx, |this, cx| {
-                                                                this.change_file(
-                                                                    ix, window, true, cx,
-                                                                );
+                                                                this.change_file(ix, window, cx);
                                                             });
                                                             focus_handle.focus(window);
                                                         }
@@ -1726,8 +1957,7 @@ impl SettingsWindow {
                                     })
                                     .tab_index(0),
                                 )
-                            },
-                        )
+                            })
                     }),
             )
             .child(
@@ -1812,6 +2042,9 @@ impl SettingsWindow {
             .read(cx)
             .handle
             .contains_focused(window, cx)
+            || self
+                .visible_navbar_entries()
+                .any(|(_, entry)| entry.focus_handle.is_focused(window))
         {
             "Focus Content"
         } else {
@@ -1819,12 +2052,6 @@ impl SettingsWindow {
         };
 
         v_flex()
-            .w_56()
-            .p_2p5()
-            .when(cfg!(target_os = "macos"), |c| c.pt_10())
-            .h_full()
-            .flex_none()
-            .border_r_1()
             .key_context("NavigationMenu")
             .on_action(cx.listener(|this, _: &CollapseNavEntry, window, cx| {
                 let Some(focused_entry) = this.focused_nav_entry(window, cx) else {
@@ -1912,7 +2139,13 @@ impl SettingsWindow {
                 let Some(next_entry_index) = next_index else {
                     return;
                 };
-                this.open_and_scroll_to_navbar_entry(next_entry_index, window, cx, false);
+                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
@@ -1928,8 +2161,20 @@ impl SettingsWindow {
                 let Some(prev_entry_index) = prev_index else {
                     return;
                 };
-                this.open_and_scroll_to_navbar_entry(prev_entry_index, window, cx, false);
+                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))
@@ -1948,24 +2193,22 @@ impl SettingsWindow {
                                 this.visible_navbar_entries()
                                     .skip(range.start.saturating_sub(1))
                                     .take(range.len())
-                                    .map(|(ix, entry)| {
+                                    .map(|(entry_index, entry)| {
                                         TreeViewItem::new(
-                                            ("settings-ui-navbar-entry", ix),
+                                            ("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(ix))
+                                        .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(ix);
-                                                        // Update selection state immediately before cx.notify
-                                                        // to prevent double selection flash
-                                                        this.navbar_entry = ix;
+                                                        this.toggle_navbar_entry(entry_index);
                                                         window.focus(
-                                                            &this.navbar_entries[ix].focus_handle,
+                                                            &this.navbar_entries[entry_index]
+                                                                .focus_handle,
                                                         );
                                                         cx.notify();
                                                     },
@@ -1974,7 +2217,11 @@ impl SettingsWindow {
                                         .on_click(
                                             cx.listener(move |this, _, window, cx| {
                                                 this.open_and_scroll_to_navbar_entry(
-                                                    ix, window, cx, true,
+                                                    entry_index,
+                                                    None,
+                                                    true,
+                                                    window,
+                                                    cx,
                                                 );
                                             }),
                                         )
@@ -1996,14 +2243,16 @@ impl SettingsWindow {
                     .flex_shrink_0()
                     .border_t_1()
                     .border_color(cx.theme().colors().border_variant)
-                    .children(
-                        KeyBinding::for_action(&ToggleFocusNav, window, cx).map(|this| {
-                            KeybindingHint::new(
-                                this,
-                                cx.theme().colors().surface_background.opacity(0.5),
-                            )
-                            .suffix(focus_keybind_label)
-                        }),
+                    .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),
                     ),
             )
     }
@@ -2011,13 +2260,16 @@ impl SettingsWindow {
     fn open_and_scroll_to_navbar_entry(
         &mut self,
         navbar_entry_index: usize,
+        scroll_strategy: Option<gpui::ScrollStrategy>,
+        focus_content: bool,
         window: &mut Window,
         cx: &mut Context<Self>,
-        focus_content: bool,
     ) {
         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)
         {
@@ -2029,38 +2281,51 @@ impl SettingsWindow {
                 else {
                     return;
                 };
-                self.focus_content_element(first_item_index, window, cx);
+                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 {
-                window.focus(&self.navbar_entries[navbar_entry_index].focus_handle);
+                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");
-            let Some(selected_item_index) = self
-                .visible_page_items()
-                .position(|(index, _)| index == entry_item_index)
-            else {
-                return;
-            };
-
-            self.list_state.scroll_to(gpui::ListOffset {
-                item_ix: selected_item_index + 1,
-                offset_in_item: px(0.),
-            });
+            self.scroll_to_content_item(entry_item_index, window, cx);
             if focus_content {
-                self.focus_content_element(entry_item_index, window, cx);
+                handle_to_focus = Some(self.focus_handle_for_content_element(entry_item_index, cx));
             } else {
-                window.focus(&self.navbar_entries[navbar_entry_index].focus_handle);
+                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, |_, window, cx| {
+        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();
             });

crates/snippet/Cargo.toml 🔗

@@ -15,4 +15,3 @@ doctest = false
 [dependencies]
 anyhow.workspace = true
 smallvec.workspace = true
-workspace-hack.workspace = true

crates/snippet_provider/Cargo.toml 🔗

@@ -26,7 +26,6 @@ serde_json_lenient.workspace = true
 snippet.workspace = true
 util.workspace = true
 schemars.workspace = true
-workspace-hack.workspace = true
 
 [dev-dependencies]
 fs = { workspace = true, features = ["test-support"] }

crates/snippets_ui/Cargo.toml 🔗

@@ -22,5 +22,4 @@ picker.workspace = true
 settings.workspace = true
 ui.workspace = true
 util.workspace = true
-workspace-hack.workspace = true
 workspace.workspace = true

crates/sqlez/Cargo.toml 🔗

@@ -21,4 +21,3 @@ sqlformat.workspace = true
 thread_local = "1.1.4"
 util.workspace = true
 uuid.workspace = true
-workspace-hack.workspace = true

crates/sqlez_macros/Cargo.toml 🔗

@@ -17,4 +17,3 @@ doctest = false
 sqlez.workspace = true
 sqlformat.workspace = true
 syn.workspace = true
-workspace-hack.workspace = true

crates/story/Cargo.toml 🔗

@@ -15,4 +15,3 @@ workspace = true
 gpui.workspace = true
 itertools.workspace = true
 smallvec.workspace = true
-workspace-hack.workspace = true

crates/storybook/Cargo.toml 🔗

@@ -37,7 +37,6 @@ theme.workspace = true
 title_bar = { workspace = true, features = ["stories"] }
 ui = { workspace = true, features = ["stories"] }
 workspace.workspace = true
-workspace-hack.workspace = true
 
 [dev-dependencies]
 gpui = { workspace = true, features = ["test-support"] }

crates/streaming_diff/Cargo.toml 🔗

@@ -14,7 +14,6 @@ path = "src/streaming_diff.rs"
 [dependencies]
 ordered-float.workspace = true
 rope.workspace = true
-workspace-hack.workspace = true
 
 [dev-dependencies]
 rand.workspace = true

crates/sum_tree/Cargo.toml 🔗

@@ -17,7 +17,6 @@ doctest = false
 arrayvec = "0.7.1"
 rayon.workspace = true
 log.workspace = true
-workspace-hack.workspace = true
 
 [dev-dependencies]
 ctor.workspace = true

crates/sum_tree/src/cursor.rs 🔗

@@ -388,6 +388,7 @@ where
     T: Item,
     D: Dimension<'a, T::Summary>,
 {
+    /// Returns whether we found the item you were seeking for.
     #[track_caller]
     pub fn seek<Target>(&mut self, pos: &Target, bias: Bias) -> bool
     where
@@ -397,6 +398,7 @@ where
         self.seek_internal(pos, bias, &mut ())
     }
 
+    /// Returns whether we found the item you were seeking for.
     #[track_caller]
     pub fn seek_forward<Target>(&mut self, pos: &Target, bias: Bias) -> bool
     where
@@ -437,7 +439,7 @@ where
         summary.0
     }
 
-    /// Returns whether we found the item you were seeking for
+    /// Returns whether we found the item you were seeking for.
     #[track_caller]
     fn seek_internal(
         &mut self,

crates/sum_tree/src/sum_tree.rs 🔗

@@ -82,6 +82,11 @@ pub trait Dimension<'a, S: Summary>: Clone {
     fn zero(cx: S::Context<'_>) -> Self;
 
     fn add_summary(&mut self, summary: &'a S, cx: S::Context<'_>);
+    #[must_use]
+    fn with_added_summary(mut self, summary: &'a S, cx: S::Context<'_>) -> Self {
+        self.add_summary(summary, cx);
+        self
+    }
 
     fn from_summary(summary: &'a S, cx: S::Context<'_>) -> Self {
         let mut dimension = Self::zero(cx);
@@ -371,12 +376,122 @@ impl<T: Item> SumTree<T> {
         Iter::new(self)
     }
 
-    pub fn cursor<'a, 'b, S>(
+    /// A more efficient version of `Cursor::new()` + `Cursor::seek()` + `Cursor::item()`.
+    ///
+    /// Only returns the item that exactly has the target match.
+    pub fn find_exact<'a, 'slf, D, Target>(
+        &'slf self,
+        cx: <T::Summary as Summary>::Context<'a>,
+        target: &Target,
+        bias: Bias,
+    ) -> (D, D, Option<&'slf T>)
+    where
+        D: Dimension<'slf, T::Summary>,
+        Target: SeekTarget<'slf, T::Summary, D>,
+    {
+        let tree_end = D::zero(cx).with_added_summary(self.summary(), cx);
+        let comparison = target.cmp(&tree_end, cx);
+        if comparison == Ordering::Greater || (comparison == Ordering::Equal && bias == Bias::Right)
+        {
+            return (tree_end.clone(), tree_end, None);
+        }
+
+        let mut pos = D::zero(cx);
+        return match Self::find_recurse::<_, _, true>(cx, target, bias, &mut pos, self) {
+            Some((item, end)) => (pos, end, Some(item)),
+            None => (pos.clone(), pos, None),
+        };
+    }
+
+    /// A more efficient version of `Cursor::new()` + `Cursor::seek()` + `Cursor::item()`
+    pub fn find<'a, 'slf, D, Target>(
+        &'slf self,
+        cx: <T::Summary as Summary>::Context<'a>,
+        target: &Target,
+        bias: Bias,
+    ) -> (D, D, Option<&'slf T>)
+    where
+        D: Dimension<'slf, T::Summary>,
+        Target: SeekTarget<'slf, T::Summary, D>,
+    {
+        let tree_end = D::zero(cx).with_added_summary(self.summary(), cx);
+        let comparison = target.cmp(&tree_end, cx);
+        if comparison == Ordering::Greater || (comparison == Ordering::Equal && bias == Bias::Right)
+        {
+            return (tree_end.clone(), tree_end, None);
+        }
+
+        let mut pos = D::zero(cx);
+        return match Self::find_recurse::<_, _, false>(cx, target, bias, &mut pos, self) {
+            Some((item, end)) => (pos, end, Some(item)),
+            None => (pos.clone(), pos, None),
+        };
+    }
+
+    fn find_recurse<'tree, 'a, D, Target, const EXACT: bool>(
+        cx: <T::Summary as Summary>::Context<'a>,
+        target: &Target,
+        bias: Bias,
+        position: &mut D,
+        this: &'tree SumTree<T>,
+    ) -> Option<(&'tree T, D)>
+    where
+        D: Dimension<'tree, T::Summary>,
+        Target: SeekTarget<'tree, T::Summary, D>,
+    {
+        match &*this.0 {
+            Node::Internal {
+                child_summaries,
+                child_trees,
+                ..
+            } => {
+                for (child_tree, child_summary) in child_trees.iter().zip(child_summaries) {
+                    let child_end = position.clone().with_added_summary(child_summary, cx);
+
+                    let comparison = target.cmp(&child_end, cx);
+                    let target_in_child = comparison == Ordering::Less
+                        || (comparison == Ordering::Equal && bias == Bias::Left);
+                    if target_in_child {
+                        return Self::find_recurse::<D, Target, EXACT>(
+                            cx, target, bias, position, child_tree,
+                        );
+                    }
+                    *position = child_end;
+                }
+            }
+            Node::Leaf {
+                items,
+                item_summaries,
+                ..
+            } => {
+                for (item, item_summary) in items.iter().zip(item_summaries) {
+                    let mut child_end = position.clone();
+                    child_end.add_summary(item_summary, cx);
+
+                    let comparison = target.cmp(&child_end, cx);
+                    let entry_found = if EXACT {
+                        comparison == Ordering::Equal
+                    } else {
+                        comparison == Ordering::Less
+                            || (comparison == Ordering::Equal && bias == Bias::Left)
+                    };
+                    if entry_found {
+                        return Some((item, child_end));
+                    }
+
+                    *position = child_end;
+                }
+            }
+        }
+        None
+    }
+
+    pub fn cursor<'a, 'b, D>(
         &'a self,
         cx: <T::Summary as Summary>::Context<'b>,
-    ) -> Cursor<'a, 'b, T, S>
+    ) -> Cursor<'a, 'b, T, D>
     where
-        S: Dimension<'a, T::Summary>,
+        D: Dimension<'a, T::Summary>,
     {
         Cursor::new(self, cx)
     }
@@ -787,9 +902,8 @@ impl<T: KeyedItem> SumTree<T> {
         key: &T::Key,
         cx: <T::Summary as Summary>::Context<'a>,
     ) -> Option<&'a T> {
-        let mut cursor = self.cursor::<T::Key>(cx);
-        if cursor.seek(key, Bias::Left) {
-            cursor.item()
+        if let (_, _, Some(item)) = self.find_exact::<T::Key, _>(cx, key, Bias::Left) {
+            Some(item)
         } else {
             None
         }

crates/sum_tree/src/tree_map.rs 🔗

@@ -54,9 +54,10 @@ impl<K: Clone + Ord, V: Clone> TreeMap<K, V> {
     }
 
     pub fn get(&self, key: &K) -> Option<&V> {
-        let mut cursor = self.0.cursor::<MapKeyRef<'_, K>>(());
-        cursor.seek(&MapKeyRef(Some(key)), Bias::Left);
-        if let Some(item) = cursor.item() {
+        let (.., item) = self
+            .0
+            .find::<MapKeyRef<'_, K>, _>((), &MapKeyRef(Some(key)), Bias::Left);
+        if let Some(item) = item {
             if Some(key) == item.key().0.as_ref() {
                 Some(&item.value)
             } else {

crates/supermaven/Cargo.toml 🔗

@@ -31,7 +31,6 @@ text.workspace = true
 ui.workspace = true
 unicode-segmentation.workspace = true
 util.workspace = true
-workspace-hack.workspace = true
 
 [dev-dependencies]
 editor = { workspace = true, features = ["test-support"] }

crates/supermaven_api/Cargo.toml 🔗

@@ -21,4 +21,3 @@ serde.workspace = true
 serde_json.workspace = true
 smol.workspace = true
 util.workspace = true
-workspace-hack.workspace = true

crates/svg_preview/Cargo.toml 🔗

@@ -18,4 +18,3 @@ gpui.workspace = true
 multi_buffer.workspace = true
 ui.workspace = true
 workspace.workspace = true
-workspace-hack.workspace = true

crates/system_specs/Cargo.toml 🔗

@@ -22,7 +22,6 @@ human_bytes.workspace = true
 release_channel.workspace = true
 serde.workspace = true
 sysinfo.workspace = true
-workspace-hack.workspace = true
 
 [target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies]
 pciid-parser.workspace = true

crates/tab_switcher/Cargo.toml 🔗

@@ -27,7 +27,6 @@ smol.workspace = true
 ui.workspace = true
 util.workspace = true
 workspace.workspace = true
-workspace-hack.workspace = true
 
 [dev-dependencies]
 anyhow.workspace = true

crates/task/Cargo.toml 🔗

@@ -34,7 +34,6 @@ serde_json_lenient.workspace = true
 sha2.workspace = true
 shellexpand.workspace = true
 util.workspace = true
-workspace-hack.workspace = true
 zed_actions.workspace = true
 
 [dev-dependencies]

crates/tasks_ui/Cargo.toml 🔗

@@ -29,7 +29,6 @@ util.workspace = true
 workspace.workspace = true
 language.workspace = true
 zed_actions.workspace = true
-workspace-hack.workspace = true
 
 [dev-dependencies]
 editor = { workspace = true, features = ["test-support"] }

crates/tasks_ui/src/modal.rs 🔗

@@ -664,10 +664,10 @@ impl PickerDelegate for TasksModalDelegate {
                 .child(
                     left_button
                         .map(|(label, action)| {
-                            let keybind = KeyBinding::for_action(&*action, window, cx);
+                            let keybind = KeyBinding::for_action(&*action, cx);
 
                             Button::new("edit-current-task", label)
-                                .when_some(keybind, |this, keybind| this.key_binding(keybind))
+                                .key_binding(keybind)
                                 .on_click(move |_, window, cx| {
                                     window.dispatch_action(action.boxed_clone(), cx);
                                 })
@@ -682,7 +682,7 @@ impl PickerDelegate for TasksModalDelegate {
                             secondary: current_modifiers.secondary(),
                         }
                         .boxed_clone();
-                        this.children(KeyBinding::for_action(&*action, window, cx).map(|keybind| {
+                        this.child({
                             let spawn_oneshot_label = if current_modifiers.secondary() {
                                 "Spawn Oneshot Without History"
                             } else {
@@ -690,44 +690,35 @@ impl PickerDelegate for TasksModalDelegate {
                             };
 
                             Button::new("spawn-onehshot", spawn_oneshot_label)
-                                .key_binding(keybind)
+                                .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.children(
-                            KeyBinding::for_action(&menu::SecondaryConfirm, window, cx).map(
-                                |keybind| {
-                                    let label = if is_recent_selected {
-                                        "Rerun Without History"
-                                    } else {
-                                        "Spawn Without History"
-                                    };
-                                    Button::new("spawn", label).key_binding(keybind).on_click(
-                                        move |_, window, cx| {
-                                            window.dispatch_action(
-                                                menu::SecondaryConfirm.boxed_clone(),
-                                                cx,
-                                            )
-                                        },
-                                    )
-                                },
-                            ),
-                        )
+                        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.children(KeyBinding::for_action(&menu::Confirm, window, cx).map(
-                            |keybind| {
-                                let run_entry_label =
-                                    if is_recent_selected { "Rerun" } else { "Spawn" };
-
-                                Button::new("spawn", run_entry_label)
-                                    .key_binding(keybind)
-                                    .on_click(|_, window, cx| {
-                                        window.dispatch_action(menu::Confirm.boxed_clone(), cx);
-                                    })
-                            },
-                        ))
+                        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(),

crates/telemetry/Cargo.toml 🔗

@@ -16,4 +16,3 @@ serde.workspace = true
 serde_json.workspace = true
 telemetry_events.workspace = true
 futures.workspace = true
-workspace-hack.workspace = true

crates/telemetry_events/Cargo.toml 🔗

@@ -15,4 +15,3 @@ path = "src/telemetry_events.rs"
 semantic_version.workspace = true
 serde.workspace = true
 serde_json.workspace = true
-workspace-hack.workspace = true

crates/terminal/Cargo.toml 🔗

@@ -39,7 +39,6 @@ thiserror.workspace = true
 util.workspace = true
 regex.workspace = true
 urlencoding.workspace = true
-workspace-hack.workspace = true
 itertools.workspace = true
 
 [target.'cfg(windows)'.dependencies]

crates/terminal/src/terminal.rs 🔗

@@ -423,232 +423,233 @@ impl TerminalBuilder {
         completion_tx: Option<Sender<Option<ExitStatus>>>,
         cx: &App,
         activation_script: Vec<String>,
-    ) -> Result<TerminalBuilder> {
-        // 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(),
-            release_channel::AppVersion::global(cx).to_string(),
-        );
-
-        #[derive(Default)]
-        struct ShellParams {
-            program: String,
-            args: Option<Vec<String>>,
-            title_override: Option<SharedString>,
-        }
-
-        impl ShellParams {
-            fn new(
+    ) -> Task<Result<TerminalBuilder>> {
+        let version = release_channel::AppVersion::global(cx);
+        cx.background_spawn(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<Vec<String>>,
                 title_override: Option<SharedString>,
-            ) -> Self {
-                log::info!("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
+            impl ShellParams {
+                fn new(
+                    program: String,
+                    args: Option<Vec<String>>,
+                    title_override: Option<SharedString>,
+                ) -> Self {
+                    log::debug!("Using {program} as shell");
+                    Self {
+                        program,
+                        args,
+                        title_override,
+                    }
                 }
             }
-            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;
+            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());
 
-            Self::resolve_path(&params.program)
-                .log_err()
-                .unwrap_or(params.program.clone())
-        });
+            #[cfg(windows)]
+            let shell_program = shell_params.as_ref().map(|params| {
+                use util::ResultExt;
 
-        // 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(),
-                )
+                Self::resolve_path(&params.program)
+                    .log_err()
+                    .unwrap_or(params.program.clone())
             });
 
-            alacritty_terminal::tty::Options {
-                shell: alac_shell,
-                working_directory: working_directory.clone(),
-                drain_on_exit: true,
-                env: env.clone().into_iter().collect(),
-                // We do not want to escape arguments if we are using CMD as our shell.
-                // If we do we end up with too many quotes/escaped quotes for CMD to handle.
-                #[cfg(windows)]
-                escape_args: shell_kind != util::shell::ShellKind::Cmd,
-            }
-        };
-
-        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()
-        };
+            // 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(),
+                    )
+                });
 
-        //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_terminal::tty::Options {
+                    shell: alac_shell,
+                    working_directory: working_directory.clone(),
+                    drain_on_exit: true,
+                    env: env.clone().into_iter().collect(),
+                    // We do not want to escape arguments if we are using CMD as our shell.
+                    // If we do we end up with too many quotes/escaped quotes for CMD to handle.
+                    #[cfg(windows)]
+                    escape_args: shell_kind != util::shell::ShellKind::Cmd,
+                }
+            };
 
-        //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 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()
+            };
 
-        let term = Arc::new(FairMutex::new(term));
+            //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()),
+            );
 
-        //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,
-                });
+            //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 pty_info = PtyProcessInfo::new(&pty);
+            let term = Arc::new(FairMutex::new(term));
 
-        //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")?;
+            //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,
+                    });
+                }
+            };
 
-        //Kick things off
-        let pty_tx = event_loop.channel();
-        let _io_thread = event_loop.spawn(); // DANGER
+            let pty_info = PtyProcessInfo::new(&pty);
 
-        let no_task = task.is_none();
+            //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 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(),
-            vi_mode_enabled: false,
-            is_ssh_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,
-                window_id,
-            },
-            child_exited: None,
-        };
+            //Kick things off
+            let pty_tx = event_loop.channel();
+            let _io_thread = event_loop.spawn(); // DANGER
+
+            let no_task = task.is_none();
 
-        if !activation_script.is_empty() && no_task {
-            for activation_script in activation_script {
-                terminal.write_to_pty(activation_script.into_bytes());
+            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(),
+                vi_mode_enabled: false,
+                is_ssh_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,
+                    window_id,
+                },
+                child_exited: None,
+            };
+
+            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
-                // 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,
+            Ok(TerminalBuilder {
+                terminal,
+                events_rx,
+            })
         })
     }
 
@@ -2153,7 +2154,7 @@ impl Terminal {
         self.vi_mode_enabled
     }
 
-    pub fn clone_builder(&self, cx: &App, cwd: Option<PathBuf>) -> Result<TerminalBuilder> {
+    pub fn clone_builder(&self, cx: &App, cwd: Option<PathBuf>) -> Task<Result<TerminalBuilder>> {
         let working_directory = self.working_directory().or_else(|| cwd);
         TerminalBuilder::new(
             working_directory,
@@ -2389,28 +2390,30 @@ mod tests {
         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 terminal = cx.new(|cx| {
-            TerminalBuilder::new(
-                None,
-                None,
-                task::Shell::WithArguments {
-                    program,
-                    args,
-                    title_override: None,
-                },
-                HashMap::default(),
-                CursorShape::default(),
-                AlternateScroll::On,
-                None,
-                false,
-                0,
-                Some(completion_tx),
-                cx,
-                vec![],
-            )
-            .unwrap()
-            .subscribe(cx)
-        });
+        let builder = cx
+            .update(|cx| {
+                TerminalBuilder::new(
+                    None,
+                    None,
+                    task::Shell::WithArguments {
+                        program,
+                        args,
+                        title_override: None,
+                    },
+                    HashMap::default(),
+                    CursorShape::default(),
+                    AlternateScroll::On,
+                    None,
+                    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())
@@ -2439,25 +2442,27 @@ mod tests {
         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,
+                    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| {
-            TerminalBuilder::new(
-                None,
-                None,
-                task::Shell::System,
-                HashMap::default(),
-                CursorShape::default(),
-                AlternateScroll::On,
-                None,
-                false,
-                0,
-                Some(completion_tx),
-                cx,
-                Vec::new(),
-            )
-            .unwrap()
-            .subscribe(cx)
-        });
+        let terminal = cx.new(|cx| builder.subscribe(cx));
 
         let (event_tx, event_rx) = smol::channel::unbounded::<Event>();
         cx.update(|cx| {
@@ -2508,28 +2513,30 @@ mod tests {
         let (completion_tx, completion_rx) = smol::channel::unbounded();
         let (program, args) = ShellBuilder::new(&Shell::System, false)
             .build(Some("asdasdasdasd".to_owned()), &["@@@@@".to_owned()]);
-        let terminal = cx.new(|cx| {
-            TerminalBuilder::new(
-                None,
-                None,
-                task::Shell::WithArguments {
-                    program,
-                    args,
-                    title_override: None,
-                },
-                HashMap::default(),
-                CursorShape::default(),
-                AlternateScroll::On,
-                None,
-                false,
-                0,
-                Some(completion_tx),
-                cx,
-                Vec::new(),
-            )
-            .unwrap()
-            .subscribe(cx)
-        });
+        let builder = cx
+            .update(|cx| {
+                TerminalBuilder::new(
+                    None,
+                    None,
+                    task::Shell::WithArguments {
+                        program,
+                        args,
+                        title_override: None,
+                    },
+                    HashMap::default(),
+                    CursorShape::default(),
+                    AlternateScroll::On,
+                    None,
+                    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::<Event>();
         cx.update(|cx| {

crates/terminal/src/terminal_settings.rs 🔗

@@ -8,9 +8,8 @@ use serde::{Deserialize, Serialize};
 
 pub use settings::AlternateScroll;
 use settings::{
-    CursorShapeContent, SettingsContent, ShowScrollbar, TerminalBlink, TerminalDockPosition,
-    TerminalLineHeight, TerminalSettingsContent, VenvSettings, WorkingDirectory,
-    merge_from::MergeFrom,
+    ShowScrollbar, TerminalBlink, TerminalDockPosition, TerminalLineHeight, VenvSettings,
+    WorkingDirectory, merge_from::MergeFrom,
 };
 use task::Shell;
 use theme::FontFamilyName;
@@ -116,81 +115,6 @@ impl settings::Settings for TerminalSettings {
             minimum_contrast: user_content.minimum_contrast.unwrap(),
         }
     }
-
-    fn import_from_vscode(vscode: &settings::VsCodeSettings, content: &mut SettingsContent) {
-        let mut default = TerminalSettingsContent::default();
-        let current = content.terminal.as_mut().unwrap_or(&mut default);
-        let name = |s| format!("terminal.integrated.{s}");
-
-        vscode.f32_setting(&name("fontSize"), &mut current.font_size);
-        vscode.font_family_setting(
-            &name("fontFamily"),
-            &mut current.font_family,
-            &mut current.font_fallbacks,
-        );
-        vscode.bool_setting(&name("copyOnSelection"), &mut current.copy_on_select);
-        vscode.bool_setting("macOptionIsMeta", &mut current.option_as_meta);
-        vscode.usize_setting("scrollback", &mut current.max_scroll_history_lines);
-        match vscode.read_bool(&name("cursorBlinking")) {
-            Some(true) => current.blinking = Some(TerminalBlink::On),
-            Some(false) => current.blinking = Some(TerminalBlink::Off),
-            None => {}
-        }
-        vscode.enum_setting(
-            &name("cursorStyle"),
-            &mut current.cursor_shape,
-            |s| match s {
-                "block" => Some(CursorShapeContent::Block),
-                "line" => Some(CursorShapeContent::Bar),
-                "underline" => Some(CursorShapeContent::Underline),
-                _ => None,
-            },
-        );
-        // they also have "none" and "outline" as options but just for the "Inactive" variant
-        if let Some(height) = vscode
-            .read_value(&name("lineHeight"))
-            .and_then(|v| v.as_f64())
-        {
-            current.line_height = Some(TerminalLineHeight::Custom(height as f32))
-        }
-
-        #[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";
-
-        // TODO: handle arguments
-        let shell_name = format!("{platform}Exec");
-        if let Some(s) = vscode.read_string(&name(&shell_name)) {
-            current.project.shell = Some(settings::Shell::Program(s.to_owned()))
-        }
-
-        if let Some(env) = vscode
-            .read_value(&name(&format!("env.{platform}")))
-            .and_then(|v| v.as_object())
-        {
-            for (k, v) in env {
-                if v.is_null()
-                    && let Some(zed_env) = current.project.env.as_mut()
-                {
-                    zed_env.remove(k);
-                }
-                let Some(v) = v.as_str() else { continue };
-                if let Some(zed_env) = current.project.env.as_mut() {
-                    zed_env.insert(k.clone(), v.to_owned());
-                } else {
-                    current.project.env = Some([(k.clone(), v.to_owned())].into_iter().collect())
-                }
-            }
-        }
-        if content.terminal.is_none() && default != TerminalSettingsContent::default() {
-            content.terminal = Some(default)
-        }
-    }
 }
 
 #[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]

crates/terminal_view/Cargo.toml 🔗

@@ -46,7 +46,6 @@ ui.workspace = true
 util.workspace = true
 workspace.workspace = true
 zed_actions.workspace = true
-workspace-hack.workspace = true
 
 [dev-dependencies]
 client = { workspace = true, features = ["test-support"] }

crates/terminal_view/src/persistence.rs 🔗

@@ -214,14 +214,6 @@ async fn deserialize_pane_group(
         }
         SerializedPaneGroup::Pane(serialized_pane) => {
             let active = serialized_pane.active;
-            let new_items = deserialize_terminal_views(
-                workspace_id,
-                project.clone(),
-                workspace.clone(),
-                serialized_pane.children.as_slice(),
-                cx,
-            )
-            .await;
 
             let pane = panel
                 .update_in(cx, |terminal_panel, window, cx| {
@@ -236,56 +228,71 @@ async fn deserialize_pane_group(
                 .log_err()?;
             let active_item = serialized_pane.active_item;
             let pinned_count = serialized_pane.pinned_count;
-            let terminal = pane
-                .update_in(cx, |pane, window, cx| {
-                    populate_pane_items(pane, new_items, active_item, window, cx);
-                    pane.set_pinned_count(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 pane.items_len() == 0 {
+                    if items.is_ok_and(|items| items == 0) {
                         let working_directory = workspace
                             .update(cx, |workspace, cx| default_working_directory(workspace, cx))
                             .ok()
                             .flatten();
-                        let terminal = project.update(cx, |project, cx| {
-                            project.create_terminal_shell(working_directory, cx)
-                        });
-                        Some(Some(terminal))
-                    } else {
-                        Some(None)
+                        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();
                     }
-                })
-                .ok()
-                .flatten()?;
-            if let Some(terminal) = terminal {
-                let terminal = terminal.await.ok()?;
-                pane.update_in(cx, |pane, window, cx| {
-                    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()?;
-            }
+                }
+            })
+            .detach();
             Some((Member::Pane(pane.clone()), active.then_some(pane)))
         }
     }
 }
 
-async fn deserialize_terminal_views(
+fn deserialize_terminal_views(
     workspace_id: WorkspaceId,
     project: Entity<Project>,
     workspace: WeakEntity<Workspace>,
     item_ids: &[u64],
     cx: &mut AsyncWindowContext,
-) -> Vec<Entity<TerminalView>> {
-    let mut items = Vec::with_capacity(item_ids.len());
+) -> impl Future<Output = Vec<Entity<TerminalView>>> + use<> {
     let mut deserialized_items = item_ids
         .iter()
         .map(|item_id| {
@@ -302,12 +309,15 @@ async fn deserialize_terminal_views(
             .unwrap_or_else(|e| Task::ready(Err(e.context("no window present"))))
         })
         .collect::<FuturesUnordered<_>>();
-    while let Some(item) = deserialized_items.next().await {
-        if let Some(item) = item.log_err() {
-            items.push(item);
+    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
     }
-    items
 }
 
 #[derive(Debug, Serialize, Deserialize)]

crates/terminal_view/src/terminal_panel.rs 🔗

@@ -22,8 +22,8 @@ use settings::{Settings, TerminalDockPosition};
 use task::{RevealStrategy, RevealTarget, Shell, ShellBuilder, SpawnInTerminal, TaskId};
 use terminal::{Terminal, terminal_settings::TerminalSettings};
 use ui::{
-    ButtonCommon, Clickable, ContextMenu, FluentBuilder, PopoverMenu, Toggleable, Tooltip,
-    prelude::*,
+    ButtonLike, Clickable, ContextMenu, FluentBuilder, PopoverMenu, SplitButton, Toggleable,
+    Tooltip, prelude::*,
 };
 use util::{ResultExt, TryFutureExt};
 use workspace::{
@@ -35,7 +35,6 @@ use workspace::{
     dock::{DockPosition, Panel, PanelEvent, PanelHandle},
     item::SerializableItem,
     move_active_item, move_item, pane,
-    ui::IconName,
 };
 
 use anyhow::{Result, anyhow};
@@ -211,11 +210,10 @@ impl TerminalPanel {
                             .on_click(cx.listener(|pane, _, window, cx| {
                                 pane.toggle_zoom(&workspace::ToggleZoom, window, cx);
                             }))
-                            .tooltip(move |window, cx| {
+                            .tooltip(move |_window, cx| {
                                 Tooltip::for_action(
                                     if zoomed { "Zoom Out" } else { "Zoom In" },
                                     &ToggleZoom,
-                                    window,
                                     cx,
                                 )
                             })
@@ -463,11 +461,11 @@ impl TerminalPanel {
         cx.spawn_in(window, async move |panel, cx| {
             let terminal = project
                 .update(cx, |project, cx| match terminal_view {
-                    Some(view) => Task::ready(project.clone_terminal(
+                    Some(view) => project.clone_terminal(
                         &view.read(cx).terminal.clone(),
                         cx,
                         working_directory,
-                    )),
+                    ),
                     None => project.create_terminal_shell(working_directory, cx),
                 })
                 .ok()?
@@ -813,6 +811,7 @@ impl TerminalPanel {
         cx: &mut Context<Self>,
     ) -> Task<Result<WeakEntity<Terminal>>> {
         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");
@@ -824,43 +823,59 @@ impl TerminalPanel {
             let project = workspace.read_with(cx, |workspace, _| workspace.project().clone())?;
             let terminal = project
                 .update(cx, |project, cx| project.create_terminal_shell(cwd, 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,
-                    )
-                }));
+                .await;
 
-                match reveal_strategy {
-                    RevealStrategy::Always => {
-                        workspace.focus_panel::<Self>(window, cx);
-                    }
-                    RevealStrategy::NoFocus => {
-                        workspace.open_panel::<Self>(window, cx);
-                    }
-                    RevealStrategy::Never => {}
-                }
+            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,
+                            )
+                        }));
 
-                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);
-                });
+                        match reveal_strategy {
+                            RevealStrategy::Always => {
+                                workspace.focus_panel::<Self>(window, cx);
+                            }
+                            RevealStrategy::NoFocus => {
+                                workspace.open_panel::<Self>(window, cx);
+                            }
+                            RevealStrategy::Never => {}
+                        }
 
-                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
+                        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)
+                }
+            }
         })
     }
 
@@ -1087,6 +1102,7 @@ pub fn new_terminal_pane(
             Default::default(),
             None,
             NewTerminal.boxed_clone(),
+            false,
             window,
             cx,
         );
@@ -1288,6 +1304,82 @@ fn add_paths_to_terminal(
     }
 }
 
+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<Self>) -> 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<PanelEvent> for TerminalPanel {}
 
 impl Render for TerminalPanel {
@@ -1572,6 +1664,10 @@ impl Panel for TerminalPanel {
         "TerminalPanel"
     }
 
+    fn panel_key() -> &'static str {
+        TERMINAL_PANEL_KEY
+    }
+
     fn icon(&self, _window: &Window, cx: &App) -> Option<IconName> {
         if (self.is_enabled(cx) || !self.has_no_terminals(cx))
             && TerminalSettings::get_global(cx).button
@@ -1642,22 +1738,18 @@ impl Render for InlineAssistTabBarButton {
             .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,
-                    window,
-                    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;
+    use gpui::{TestAppContext, UpdateGlobal as _};
     use pretty_assertions::assert_eq;
     use project::FakeFs;
     use settings::SettingsStore;
@@ -1712,6 +1804,46 @@ mod tests {
             .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]
@@ -1775,6 +1907,65 @@ mod tests {
             .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::<FailedToSpawnTerminal>().is_some()),
+                        "should spawn `FailedToSpawnTerminal` pane"
+                    );
+                })
+            })
+            .unwrap();
+    }
+
+    fn set_max_tabs(cx: &mut TestAppContext, value: Option<usize>) {
+        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);

crates/terminal_view/src/terminal_view.rs 🔗

@@ -840,9 +840,7 @@ impl TerminalView {
                 .size(ButtonSize::Compact)
                 .icon_color(Color::Default)
                 .shape(ui::IconButtonShape::Square)
-                .tooltip(move |window, cx| {
-                    Tooltip::for_action("Rerun task", &RerunTask, window, cx)
-                })
+                .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);
                 }),
@@ -1220,28 +1218,31 @@ impl Item for TerminalView {
         workspace_id: Option<WorkspaceId>,
         window: &mut Window,
         cx: &mut Context<Self>,
-    ) -> Option<Entity<Self>> {
-        let 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)
+    ) -> Task<Option<Entity<Self>>> {
+        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()?
-            .log_err()?;
-
-        Some(cx.new(|cx| {
-            TerminalView::new(
-                terminal,
-                self.workspace.clone(),
-                workspace_id,
-                self.project.clone(),
-                window,
-                cx,
-            )
-        }))
+            .ok()
+        })
     }
 
     fn is_dirty(&self, cx: &gpui::App) -> bool {

crates/text/Cargo.toml 🔗

@@ -28,7 +28,6 @@ rope.workspace = true
 smallvec.workspace = true
 sum_tree.workspace = true
 util.workspace = true
-workspace-hack.workspace = true
 
 [dev-dependencies]
 collections = { workspace = true, features = ["test-support"] }

crates/text/src/anchor.rs 🔗

@@ -6,7 +6,7 @@ use std::{cmp::Ordering, fmt::Debug, ops::Range};
 use sum_tree::{Bias, Dimensions};
 
 /// A timestamped position in a buffer
-#[derive(Copy, Clone, Eq, PartialEq, Debug, Hash, Default)]
+#[derive(Copy, Clone, Eq, PartialEq, Debug, Hash)]
 pub struct Anchor {
     pub timestamp: clock::Lamport,
     /// The byte offset in the buffer
@@ -45,19 +45,19 @@ impl Anchor {
             .then_with(|| self.bias.cmp(&other.bias))
     }
 
-    pub fn min(&self, other: &Self, buffer: &BufferSnapshot) -> Self {
+    pub fn min<'a>(&'a self, other: &'a Self, buffer: &BufferSnapshot) -> &'a Self {
         if self.cmp(other, buffer).is_le() {
-            *self
+            self
         } else {
-            *other
+            other
         }
     }
 
-    pub fn max(&self, other: &Self, buffer: &BufferSnapshot) -> Self {
+    pub fn max<'a>(&'a self, other: &'a Self, buffer: &BufferSnapshot) -> &'a Self {
         if self.cmp(other, buffer).is_ge() {
-            *self
+            self
         } else {
-            *other
+            other
         }
     }
 
@@ -99,13 +99,14 @@ impl Anchor {
             let Some(fragment_id) = buffer.try_fragment_id_for_anchor(self) else {
                 return false;
             };
-            let mut fragment_cursor = buffer
+            let (.., item) = buffer
                 .fragments
-                .cursor::<Dimensions<Option<&Locator>, usize>>(&None);
-            fragment_cursor.seek(&Some(fragment_id), Bias::Left);
-            fragment_cursor
-                .item()
-                .is_some_and(|fragment| fragment.visible)
+                .find::<Dimensions<Option<&Locator>, usize>, _>(
+                    &None,
+                    &Some(fragment_id),
+                    Bias::Left,
+                );
+            item.is_some_and(|fragment| fragment.visible)
         }
     }
 }

crates/text/src/operation_queue.rs 🔗

@@ -1,3 +1,4 @@
+use clock::Lamport;
 use std::{fmt::Debug, ops::Add};
 use sum_tree::{ContextLessSummary, Dimension, Edit, Item, KeyedItem, SumTree};
 
@@ -11,10 +12,10 @@ struct OperationItem<T>(T);
 #[derive(Clone, Debug)]
 pub struct OperationQueue<T: Operation>(SumTree<OperationItem<T>>);
 
-#[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd)]
+#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
 pub struct OperationKey(clock::Lamport);
 
-#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
 pub struct OperationSummary {
     pub key: OperationKey,
     pub len: usize,
@@ -69,7 +70,10 @@ impl<T: Operation> OperationQueue<T> {
 
 impl ContextLessSummary for OperationSummary {
     fn zero() -> Self {
-        Default::default()
+        OperationSummary {
+            key: OperationKey::new(Lamport::MIN),
+            len: 0,
+        }
     }
 
     fn add_summary(&mut self, other: &Self) {
@@ -93,7 +97,7 @@ impl Add<&Self> for OperationSummary {
 
 impl Dimension<'_, OperationSummary> for OperationKey {
     fn zero(_cx: ()) -> Self {
-        Default::default()
+        OperationKey::new(Lamport::MIN)
     }
 
     fn add_summary(&mut self, summary: &OperationSummary, _: ()) {
@@ -123,11 +127,13 @@ impl<T: Operation> KeyedItem for OperationItem<T> {
 
 #[cfg(test)]
 mod tests {
+    use clock::ReplicaId;
+
     use super::*;
 
     #[test]
     fn test_len() {
-        let mut clock = clock::Lamport::new(0);
+        let mut clock = clock::Lamport::new(ReplicaId::LOCAL);
 
         let mut queue = OperationQueue::new();
         assert_eq!(queue.len(), 0);

crates/text/src/tests.rs 🔗

@@ -16,7 +16,7 @@ fn init_logger() {
 
 #[test]
 fn test_edit() {
-    let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), "abc");
+    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");
@@ -40,7 +40,11 @@ fn test_random_edits(mut rng: StdRng) {
     let mut reference_string = RandomCharIter::new(&mut rng)
         .take(reference_string_len)
         .collect::<String>();
-    let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), reference_string.clone());
+    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)));
@@ -176,7 +180,11 @@ fn test_line_endings() {
         LineEnding::Windows
     );
 
-    let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), "one\r\ntwo\rthree");
+    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();
@@ -190,7 +198,7 @@ fn test_line_endings() {
 
 #[test]
 fn test_line_len() {
-    let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), "");
+    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")]);
@@ -207,7 +215,7 @@ fn test_line_len() {
 #[test]
 fn test_common_prefix_at_position() {
     let text = "a = str; b = δα";
-    let buffer = Buffer::new(0, BufferId::new(1).unwrap(), text);
+    let buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), text);
 
     let offset1 = offset_after(text, "str");
     let offset2 = offset_after(text, "δα");
@@ -256,7 +264,7 @@ fn test_common_prefix_at_position() {
 #[test]
 fn test_text_summary_for_range() {
     let buffer = Buffer::new(
-        0,
+        ReplicaId::LOCAL,
         BufferId::new(1).unwrap(),
         "ab\nefg\nhklm\nnopqrs\ntuvwxyz",
     );
@@ -348,7 +356,7 @@ fn test_text_summary_for_range() {
 
 #[test]
 fn test_chars_at() {
-    let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), "");
+    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")]);
@@ -370,7 +378,7 @@ fn test_chars_at() {
     assert_eq!(chars.collect::<String>(), "PQrs");
 
     // Regression test:
-    let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), "");
+    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")]);
 
@@ -380,7 +388,7 @@ fn test_chars_at() {
 
 #[test]
 fn test_anchors() {
-    let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), "");
+    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);
@@ -498,7 +506,7 @@ fn test_anchors() {
 
 #[test]
 fn test_anchors_at_start_and_end() {
-    let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), "");
+    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);
 
@@ -521,7 +529,7 @@ fn test_anchors_at_start_and_end() {
 
 #[test]
 fn test_undo_redo() {
-    let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), "1234");
+    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));
 
@@ -558,7 +566,7 @@ fn test_undo_redo() {
 #[test]
 fn test_history() {
     let mut now = Instant::now();
-    let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), "123456");
+    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();
@@ -625,7 +633,7 @@ fn test_history() {
 #[test]
 fn test_finalize_last_transaction() {
     let now = Instant::now();
-    let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), "123456");
+    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);
@@ -661,7 +669,7 @@ fn test_finalize_last_transaction() {
 #[test]
 fn test_edited_ranges_for_transaction() {
     let now = Instant::now();
-    let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), "1234567");
+    let mut buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), "1234567");
 
     buffer.start_transaction_at(now);
     buffer.edit([(2..4, "cd")]);
@@ -700,9 +708,9 @@ fn test_edited_ranges_for_transaction() {
 fn test_concurrent_edits() {
     let text = "abcdef";
 
-    let mut buffer1 = Buffer::new(1, BufferId::new(1).unwrap(), text);
-    let mut buffer2 = Buffer::new(2, BufferId::new(1).unwrap(), text);
-    let mut buffer3 = Buffer::new(3, BufferId::new(1).unwrap(), text);
+    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");
@@ -741,11 +749,15 @@ fn test_random_concurrent_edits(mut rng: StdRng) {
     let mut network = Network::new(rng.clone());
 
     for i in 0..peers {
-        let mut buffer = Buffer::new(i as ReplicaId, BufferId::new(1).unwrap(), base_text.clone());
+        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(i as u16);
-        network.add_peer(i as u16);
+        replica_ids.push(ReplicaId::new(i as u16));
+        network.add_peer(ReplicaId::new(i as u16));
     }
 
     log::info!("initial text: {:?}", base_text);
@@ -759,7 +771,7 @@ fn test_random_concurrent_edits(mut rng: StdRng) {
             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());
+                log::info!("buffer {:?} text: {:?}", buffer.replica_id, buffer.text());
                 mutation_count -= 1;
             }
             51..=70 if mutation_count != 0 => {
@@ -771,7 +783,7 @@ fn test_random_concurrent_edits(mut rng: StdRng) {
                 let ops = network.receive(replica_id);
                 if !ops.is_empty() {
                     log::info!(
-                        "peer {} applying {} ops from the network.",
+                        "peer {:?} applying {} ops from the network.",
                         replica_id,
                         ops.len()
                     );
@@ -792,7 +804,7 @@ fn test_random_concurrent_edits(mut rng: StdRng) {
         assert_eq!(
             buffer.text(),
             first_buffer.text(),
-            "Replica {} text != Replica 0 text",
+            "Replica {:?} text != Replica 0 text",
             buffer.replica_id
         );
         buffer.check_invariants();

crates/text/src/text.rs 🔗

@@ -12,7 +12,7 @@ mod undo_map;
 
 pub use anchor::*;
 use anyhow::{Context as _, Result};
-use clock::LOCAL_BRANCH_REPLICA_ID;
+use clock::Lamport;
 pub use clock::ReplicaId;
 use collections::{HashMap, HashSet};
 use locator::Locator;
@@ -573,7 +573,7 @@ struct InsertionFragment {
     fragment_id: Locator,
 }
 
-#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
+#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
 struct InsertionFragmentKey {
     timestamp: clock::Lamport,
     split_offset: usize,
@@ -709,7 +709,7 @@ impl FromIterator<char> for LineIndent {
 }
 
 impl Buffer {
-    pub fn new(replica_id: u16, remote_id: BufferId, base_text: impl Into<String>) -> Buffer {
+    pub fn new(replica_id: ReplicaId, remote_id: BufferId, base_text: impl Into<String>) -> Buffer {
         let mut base_text = base_text.into();
         let line_ending = LineEnding::detect(&base_text);
         LineEnding::normalize(&mut base_text);
@@ -717,7 +717,7 @@ impl Buffer {
     }
 
     pub fn new_normalized(
-        replica_id: u16,
+        replica_id: ReplicaId,
         remote_id: BufferId,
         line_ending: LineEnding,
         normalized: Rope,
@@ -731,10 +731,7 @@ impl Buffer {
 
         let visible_text = history.base_text.clone();
         if !visible_text.is_empty() {
-            let insertion_timestamp = clock::Lamport {
-                replica_id: 0,
-                value: 1,
-            };
+            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());
@@ -788,7 +785,7 @@ impl Buffer {
             history: History::new(self.base_text().clone()),
             deferred_ops: OperationQueue::new(),
             deferred_replicas: HashSet::default(),
-            lamport_clock: clock::Lamport::new(LOCAL_BRANCH_REPLICA_ID),
+            lamport_clock: clock::Lamport::new(ReplicaId::LOCAL_BRANCH),
             subscriptions: Default::default(),
             edit_id_resolvers: Default::default(),
             wait_for_version_txs: Default::default(),
@@ -1254,7 +1251,7 @@ impl Buffer {
         for edit_id in edit_ids {
             let insertion_slice = InsertionSlice {
                 edit_id: *edit_id,
-                insertion_id: clock::Lamport::default(),
+                insertion_id: clock::Lamport::MIN,
                 range: 0..0,
             };
             let slices = self
@@ -1858,7 +1855,7 @@ impl Buffer {
         T: rand::Rng,
     {
         let mut edits = self.get_random_edits(rng, edit_count);
-        log::info!("mutating buffer {} with {:?}", self.replica_id, edits);
+        log::info!("mutating buffer {:?} with {:?}", self.replica_id, edits);
 
         let op = self.edit(edits.iter().cloned());
         if let Operation::Edit(edit) = &op {
@@ -1881,7 +1878,7 @@ impl Buffer {
             if let Some(entry) = self.history.undo_stack.choose(rng) {
                 let transaction = entry.transaction.clone();
                 log::info!(
-                    "undoing buffer {} transaction {:?}",
+                    "undoing buffer {:?} transaction {:?}",
                     self.replica_id,
                     transaction
                 );
@@ -2054,6 +2051,14 @@ impl BufferSnapshot {
         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)
     }
@@ -2086,6 +2091,10 @@ impl BufferSnapshot {
         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
     }
@@ -2326,12 +2335,15 @@ impl BufferSnapshot {
                 );
             };
 
-            let mut fragment_cursor = self
+            let (start, _, item) = self
                 .fragments
-                .cursor::<Dimensions<Option<&Locator>, usize>>(&None);
-            fragment_cursor.seek(&Some(&insertion.fragment_id), Bias::Left);
-            let fragment = fragment_cursor.item().unwrap();
-            let mut fragment_offset = fragment_cursor.start().1;
+                .find::<Dimensions<Option<&Locator>, 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;
             }
@@ -2402,21 +2414,11 @@ impl BufferSnapshot {
         } else {
             if offset > self.visible_text.len() {
                 panic!("offset {} is out of bounds", offset)
-            } else if !self.visible_text.is_char_boundary(offset) {
-                // find the character
-                let char_start = self.visible_text.floor_char_boundary(offset);
-                // `char_start` must be less than len and a char boundary
-                let ch = self.visible_text.chars_at(char_start).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,
-                );
             }
-            let mut fragment_cursor = self.fragments.cursor::<usize>(&None);
-            fragment_cursor.seek(&offset, bias);
-            let fragment = fragment_cursor.item().unwrap();
-            let overshoot = offset - *fragment_cursor.start();
+            self.visible_text.assert_char_boundary(offset);
+            let (start, _, item) = self.fragments.find::<usize, _>(&None, &offset, bias);
+            let fragment = item.unwrap();
+            let overshoot = offset - start;
             Anchor {
                 timestamp: fragment.timestamp,
                 offset: fragment.insertion_offset + overshoot,
@@ -2497,15 +2499,17 @@ impl BufferSnapshot {
             cursor.next();
             Some(cursor)
         };
-        let mut cursor = self
-            .fragments
-            .cursor::<Dimensions<Option<&Locator>, FragmentTextSummary>>(&None);
-
         let start_fragment_id = self.fragment_id_for_anchor(&range.start);
-        cursor.seek(&Some(start_fragment_id), Bias::Left);
-        let mut visible_start = cursor.start().1.visible;
-        let mut deleted_start = cursor.start().1.deleted;
-        if let Some(fragment) = cursor.item() {
+        let (start, _, item) = self
+            .fragments
+            .find::<Dimensions<Option<&Locator>, 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;
@@ -2918,7 +2922,10 @@ impl InsertionFragment {
 
 impl sum_tree::ContextLessSummary for InsertionFragmentKey {
     fn zero() -> Self {
-        Default::default()
+        InsertionFragmentKey {
+            timestamp: Lamport::MIN,
+            split_offset: 0,
+        }
     }
 
     fn add_summary(&mut self, summary: &Self) {
@@ -3263,6 +3270,13 @@ impl LineEnding {
         }
     }
 
+    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) {

crates/text/src/undo_map.rs 🔗

@@ -1,4 +1,5 @@
 use crate::UndoOperation;
+use clock::Lamport;
 use std::cmp;
 use sum_tree::{Bias, SumTree};
 
@@ -24,7 +25,7 @@ impl sum_tree::KeyedItem for UndoMapEntry {
     }
 }
 
-#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
+#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
 struct UndoMapKey {
     edit_id: clock::Lamport,
     undo_id: clock::Lamport,
@@ -32,7 +33,10 @@ struct UndoMapKey {
 
 impl sum_tree::ContextLessSummary for UndoMapKey {
     fn zero() -> Self {
-        Default::default()
+        UndoMapKey {
+            edit_id: Lamport::MIN,
+            undo_id: Lamport::MIN,
+        }
     }
 
     fn add_summary(&mut self, summary: &Self) {
@@ -69,7 +73,7 @@ impl UndoMap {
         cursor.seek(
             &UndoMapKey {
                 edit_id,
-                undo_id: Default::default(),
+                undo_id: Lamport::MIN,
             },
             Bias::Left,
         );
@@ -93,7 +97,7 @@ impl UndoMap {
         cursor.seek(
             &UndoMapKey {
                 edit_id,
-                undo_id: Default::default(),
+                undo_id: Lamport::MIN,
             },
             Bias::Left,
         );

crates/theme/Cargo.toml 🔗

@@ -36,7 +36,6 @@ strum.workspace = true
 thiserror.workspace = true
 util.workspace = true
 uuid.workspace = true
-workspace-hack.workspace = true
 
 [dev-dependencies]
 fs = { workspace = true, features = ["test-support"] }

crates/theme/src/default_colors.rs 🔗

@@ -85,7 +85,7 @@ impl ThemeColors {
             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_alpha().step_4(),
+            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(),
@@ -154,6 +154,15 @@ impl ThemeColors {
             version_control_ignored: gray().light().step_12(),
             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,
         }
     }
 
@@ -211,7 +220,7 @@ impl ThemeColors {
             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_alpha().step_4(),
+            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(),
@@ -280,6 +289,15 @@ impl ThemeColors {
             version_control_ignored: gray().dark().step_12(),
             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,
         }
     }
 }

crates/theme/src/fallback_themes.rs 🔗

@@ -233,6 +233,16 @@ pub(crate) fn zed_default_dark() -> Theme {
                 version_control_ignored: crate::gray().light().step_12(),
                 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,

crates/theme/src/icon_theme.rs 🔗

@@ -152,7 +152,7 @@ const FILE_SUFFIXES_BY_ICON_KEY: &[(&str, &[&str])] = &[
     ),
     ("java", &["java"]),
     ("javascript", &["cjs", "js", "mjs"]),
-    ("json", &["json"]),
+    ("json", &["json", "jsonc"]),
     ("julia", &["jl"]),
     ("kdl", &["kdl"]),
     ("kotlin", &["kt"]),
@@ -199,9 +199,9 @@ const FILE_SUFFIXES_BY_ICON_KEY: &[(&str, &[&str])] = &[
     (
         "storage",
         &[
-            "accdb", "csv", "dat", "db", "dbf", "dll", "fmp", "fp7", "frm", "gdb", "ib", "jsonc",
-            "ldf", "mdb", "mdf", "myd", "myi", "pdb", "RData", "rdata", "sav", "sdf", "sql",
-            "sqlite", "tsv",
+            "accdb", "csv", "dat", "db", "dbf", "dll", "fmp", "fp7", "frm", "gdb", "ib", "ldf",
+            "mdb", "mdf", "myd", "myi", "pdb", "RData", "rdata", "sav", "sdf", "sql", "sqlite",
+            "tsv",
         ],
     ),
     (

crates/theme/src/schema.rs 🔗

@@ -756,6 +756,42 @@ pub fn theme_colors_refinement(
             .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()),
     }
 }
 

crates/theme/src/settings.rs 🔗

@@ -727,15 +727,4 @@ impl settings::Settings for ThemeSettings {
             unnecessary_code_fade: content.unnecessary_code_fade.unwrap().0.clamp(0.0, 0.9),
         }
     }
-
-    fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut SettingsContent) {
-        vscode.from_f32_setting("editor.fontWeight", &mut current.theme.buffer_font_weight);
-        vscode.from_f32_setting("editor.fontSize", &mut current.theme.buffer_font_size);
-        vscode.font_family_setting(
-            "editor.fontFamily",
-            &mut current.theme.buffer_font_family,
-            &mut current.theme.buffer_font_fallbacks,
-        )
-        // TODO: possibly map editor.fontLigatures to buffer_font_features?
-    }
 }

crates/theme/src/styles/colors.rs 🔗

@@ -162,6 +162,25 @@ pub struct ThemeColors {
     /// 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
     // ===

crates/theme_importer/Cargo.toml 🔗

@@ -23,4 +23,3 @@ simplelog.workspace= true
 strum = { workspace = true, features = ["derive"] }
 theme.workspace = true
 vscode_theme = "0.2.0"
-workspace-hack.workspace = true

crates/theme_selector/Cargo.toml 🔗

@@ -26,6 +26,5 @@ ui.workspace = true
 util.workspace = true
 workspace.workspace = true
 zed_actions.workspace = true
-workspace-hack.workspace = true
 
 [dev-dependencies]

crates/time_format/Cargo.toml 🔗

@@ -15,7 +15,6 @@ doctest = false
 [dependencies]
 sys-locale.workspace = true
 time.workspace = true
-workspace-hack.workspace = true
 
 [target.'cfg(target_os = "macos")'.dependencies]
 core-foundation.workspace = true

crates/title_bar/Cargo.toml 🔗

@@ -50,7 +50,6 @@ ui.workspace = true
 util.workspace = true
 workspace.workspace = true
 zed_actions.workspace = true
-workspace-hack.workspace = true
 
 [target.'cfg(windows)'.dependencies]
 windows.workspace = true

crates/title_bar/src/collab.rs 🔗

@@ -403,14 +403,13 @@ impl TitleBar {
                         IconName::Mic
                     },
                 )
-                .tooltip(move |window, cx| {
+                .tooltip(move |_window, cx| {
                     if is_muted {
                         if is_deafened {
                             Tooltip::with_meta(
                                 "Unmute Microphone",
                                 None,
                                 "Audio will be unmuted",
-                                window,
                                 cx,
                             )
                         } else {
@@ -444,12 +443,12 @@ impl TitleBar {
             .selected_style(ButtonStyle::Tinted(TintColor::Error))
             .icon_size(IconSize::Small)
             .toggle_state(is_deafened)
-            .tooltip(move |window, cx| {
+            .tooltip(move |_window, cx| {
                 if is_deafened {
                     let label = "Unmute Audio";
 
                     if !muted_by_user {
-                        Tooltip::with_meta(label, None, "Microphone will be unmuted", window, cx)
+                        Tooltip::with_meta(label, None, "Microphone will be unmuted", cx)
                     } else {
                         Tooltip::simple(label, cx)
                     }
@@ -457,7 +456,7 @@ impl TitleBar {
                     let label = "Mute Audio";
 
                     if !muted_by_user {
-                        Tooltip::with_meta(label, None, "Microphone will be muted", window, cx)
+                        Tooltip::with_meta(label, None, "Microphone will be muted", cx)
                     } else {
                         Tooltip::simple(label, cx)
                     }

crates/title_bar/src/onboarding_banner.rs 🔗

@@ -154,12 +154,11 @@ impl Render for OnboardingBanner {
                             telemetry::event!("Banner Dismissed", source = this.source);
                             this.dismiss(cx)
                         }))
-                        .tooltip(|window, cx| {
+                        .tooltip(|_window, cx| {
                             Tooltip::with_meta(
                                 "Close Announcement Banner",
                                 None,
                                 "It won't show again for this feature",
-                                window,
                                 cx,
                             )
                         }),

crates/title_bar/src/title_bar.rs 🔗

@@ -30,10 +30,7 @@ use gpui::{
     Subscription, WeakEntity, Window, actions, div,
 };
 use onboarding_banner::OnboardingBanner;
-use project::{
-    Project, WorktreeSettings,
-    git_store::{GitStoreEvent, RepositoryEvent},
-};
+use project::{Project, WorktreeSettings, git_store::GitStoreEvent};
 use remote::RemoteConnectionOptions;
 use settings::{Settings, SettingsLocation};
 use std::sync::Arc;
@@ -287,9 +284,7 @@ impl TitleBar {
         subscriptions.push(
             cx.subscribe(&git_store, move |_, _, event, cx| match event {
                 GitStoreEvent::ActiveRepositoryChanged(_)
-                | GitStoreEvent::RepositoryUpdated(_, RepositoryEvent::Updated { .. }, _)
-                | GitStoreEvent::RepositoryAdded(_)
-                | GitStoreEvent::RepositoryRemoved(_) => {
+                | GitStoreEvent::RepositoryUpdated(_, _, true) => {
                     cx.notify();
                 }
                 _ => {}
@@ -379,7 +374,7 @@ impl TitleBar {
                         )
                         .child(Label::new(nickname).size(LabelSize::Small).truncate()),
                 )
-                .tooltip(move |window, cx| {
+                .tooltip(move |_window, cx| {
                     Tooltip::with_meta(
                         "Remote Project",
                         Some(&OpenRemote {
@@ -387,7 +382,6 @@ impl TitleBar {
                             create_new_window: false,
                         }),
                         meta.clone(),
-                        window,
                         cx,
                     )
                 })
@@ -481,13 +475,12 @@ impl TitleBar {
             .when(!is_project_selected, |b| b.color(Color::Muted))
             .style(ButtonStyle::Subtle)
             .label_size(LabelSize::Small)
-            .tooltip(move |window, cx| {
+            .tooltip(move |_window, cx| {
                 Tooltip::for_action(
                     "Recent Projects",
                     &zed_actions::OpenRecent {
                         create_new_window: false,
                     },
-                    window,
                     cx,
                 )
             })
@@ -527,12 +520,11 @@ impl TitleBar {
                 .color(Color::Muted)
                 .style(ButtonStyle::Subtle)
                 .label_size(LabelSize::Small)
-                .tooltip(move |window, cx| {
+                .tooltip(move |_window, cx| {
                     Tooltip::with_meta(
                         "Recent Branches",
                         Some(&zed_actions::git::Branch),
                         "Local branches only",
-                        window,
                         cx,
                     )
                 })

crates/toolchain_selector/Cargo.toml 🔗

@@ -20,7 +20,6 @@ project.workspace = true
 ui.workspace = true
 util.workspace = true
 workspace.workspace = true
-workspace-hack.workspace = true
 
 [lints]
 workspace = true

crates/toolchain_selector/src/toolchain_selector.rs 🔗

@@ -490,7 +490,6 @@ impl Render for AddToolchainState {
                                                 .key_binding(KeyBinding::for_action_in(
                                                     &menu::Confirm,
                                                     &handle,
-                                                    window,
                                                     cx,
                                                 ))
                                                 .on_click(cx.listener(|this, _, window, cx| {
@@ -1117,7 +1116,6 @@ impl PickerDelegate for ToolchainSelectorDelegate {
                                 .key_binding(KeyBinding::for_action_in(
                                     &AddToolchain,
                                     &self.focus_handle,
-                                    _window,
                                     cx,
                                 ))
                                 .on_click(|_, window, cx| {
@@ -1129,7 +1127,6 @@ impl PickerDelegate for ToolchainSelectorDelegate {
                                 .key_binding(KeyBinding::for_action_in(
                                     &menu::Confirm,
                                     &self.focus_handle,
-                                    _window,
                                     cx,
                                 ))
                                 .on_click(|_, window, cx| {

crates/ui/Cargo.toml 🔗

@@ -30,7 +30,6 @@ strum.workspace = true
 theme.workspace = true
 ui_macros.workspace = true
 util.workspace = true
-workspace-hack.workspace = true
 
 [target.'cfg(windows)'.dependencies]
 windows.workspace = true

crates/ui/src/components/context_menu.rs 🔗

@@ -834,9 +834,9 @@ impl ContextMenu {
                 .disabled(true)
                 .child(Label::new(label.clone()))
                 .into_any_element(),
-            ContextMenuItem::Entry(entry) => self
-                .render_menu_entry(ix, entry, window, cx)
-                .into_any_element(),
+            ContextMenuItem::Entry(entry) => {
+                self.render_menu_entry(ix, entry, cx).into_any_element()
+            }
             ContextMenuItem::CustomEntry {
                 entry_render,
                 handler,
@@ -883,7 +883,6 @@ impl ContextMenu {
         &self,
         ix: usize,
         entry: &ContextMenuEntry,
-        window: &mut Window,
         cx: &mut Context<Self>,
     ) -> impl IntoElement {
         let ContextMenuEntry {
@@ -980,18 +979,18 @@ impl ContextMenu {
                             .justify_between()
                             .child(label_element)
                             .debug_selector(|| format!("MENU_ITEM-{}", label))
-                            .children(action.as_ref().and_then(|action| {
-                                self.action_context
+                            .children(action.as_ref().map(|action| {
+                                let binding = self
+                                    .action_context
                                     .as_ref()
-                                    .and_then(|focus| {
-                                        KeyBinding::for_action_in(&**action, focus, window, cx)
-                                    })
-                                    .or_else(|| KeyBinding::for_action(&**action, window, cx))
-                                    .map(|binding| {
-                                        div().ml_4().child(binding.disabled(*disabled)).when(
-                                            *disabled && documentation_aside.is_some(),
-                                            |parent| parent.invisible(),
-                                        )
+                                    .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| {
@@ -1016,7 +1015,7 @@ impl ContextMenu {
                                         let action_context = self.action_context.clone();
                                         let title = title.clone();
                                         let action = action.boxed_clone();
-                                        move |window, cx| {
+                                        move |_window, cx| {
                                             action_context
                                                 .as_ref()
                                                 .map(|focus| {
@@ -1024,17 +1023,11 @@ impl ContextMenu {
                                                         title.clone(),
                                                         &*action,
                                                         focus,
-                                                        window,
                                                         cx,
                                                     )
                                                 })
                                                 .unwrap_or_else(|| {
-                                                    Tooltip::for_action(
-                                                        title.clone(),
-                                                        &*action,
-                                                        window,
-                                                        cx,
-                                                    )
+                                                    Tooltip::for_action(title.clone(), &*action, cx)
                                                 })
                                         }
                                     })

crates/ui/src/components/keybinding.rs 🔗

@@ -1,3 +1,5 @@
+use std::rc::Rc;
+
 use crate::PlatformStyle;
 use crate::{Icon, IconName, IconSize, h_flex, prelude::*};
 use gpui::{
@@ -5,23 +7,49 @@ use gpui::{
     Modifiers, Window, relative,
 };
 use itertools::Itertools;
+use settings::KeybindSource;
+
+#[derive(Debug)]
+enum Source {
+    Action {
+        action: Box<dyn Action>,
+        focus_handle: Option<FocusHandle>,
+    },
+    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]>,
+    },
+}
 
-#[derive(Debug, IntoElement, Clone, RegisterComponent)]
-pub struct KeyBinding {
-    /// 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.
-    pub keystrokes: Vec<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<AbsoluteLength>,
     /// The [`PlatformStyle`] to use when displaying this keybinding.
     platform_style: PlatformStyle,
-    size: Option<AbsoluteLength>,
-
     /// Determines whether the keybinding is meant for vim mode.
     vim_mode: bool,
-
     /// Indicates whether the keybinding is currently disabled.
     disabled: bool,
 }
@@ -32,23 +60,13 @@ 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, window: &mut Window, cx: &App) -> Option<Self> {
-        if let Some(focused) = window.focused(cx) {
-            return Self::for_action_in(action, &focused, window, cx);
-        }
-        let key_binding = window.highest_precedence_binding_for_action(action)?;
-        Some(Self::new_from_gpui(key_binding, cx))
+    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,
-        window: &Window,
-        cx: &App,
-    ) -> Option<Self> {
-        let key_binding = window.highest_precedence_binding_for_action_in(action, focus)?;
-        Some(Self::new_from_gpui(key_binding, cx))
+    pub fn for_action_in(action: &dyn Action, focus: &FocusHandle, cx: &App) -> Self {
+        Self::new(action, Some(focus.clone()), cx)
     }
 
     pub fn set_vim_mode(cx: &mut App, enabled: bool) {
@@ -59,18 +77,27 @@ impl KeyBinding {
         cx.try_global::<VimStyle>().is_some_and(|g| g.0)
     }
 
-    pub fn new(keystrokes: Vec<KeybindingKeystroke>, cx: &App) -> Self {
+    pub fn new(action: &dyn Action, focus_handle: Option<FocusHandle>, cx: &App) -> Self {
         Self {
-            keystrokes,
-            platform_style: PlatformStyle::platform(),
+            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 new_from_gpui(key_binding: gpui::KeyBinding, cx: &App) -> Self {
-        Self::new(key_binding.keystrokes().to_vec(), cx)
+    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`].
@@ -91,11 +118,6 @@ impl KeyBinding {
         self.disabled = disabled;
         self
     }
-
-    pub fn vim_mode(mut self, enabled: bool) -> Self {
-        self.vim_mode = enabled;
-        self
-    }
 }
 
 fn render_key(
@@ -115,36 +137,54 @@ fn render_key(
 }
 
 impl RenderOnce for KeyBinding {
-    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
-        let color = self.disabled.then_some(Color::Disabled);
-
-        h_flex()
-            .debug_selector(|| {
-                format!(
-                    "KEY_BINDING-{}",
-                    self.keystrokes
-                        .iter()
-                        .map(|k| k.key().to_string())
-                        .collect::<Vec<_>>()
-                        .join(" ")
-                )
-            })
-            .gap(DynamicSpacing::Base04.rems(cx))
-            .flex_none()
-            .children(self.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,
-                        self.platform_style,
-                        self.vim_mode,
-                    ))
-            }))
+    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::<Vec<_>>()
+                            .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())
     }
 }
 
@@ -517,79 +557,79 @@ impl Component for KeyBinding {
         )
     }
 
-    fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
-        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(),
-        )
-    }
+    // fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
+    //     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)]

crates/ui/src/components/keybinding_hint.rs 🔗

@@ -14,10 +14,11 @@ use theme::Appearance;
 /// 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::new(vec![KeybindingKeystroke::from_keystroke(Keystroke::parse("ctrl-s").unwrap())], cx),
+///     KeyBinding::from_keystrokes(vec![KeybindingKeystroke::from_keystroke(Keystroke::parse("ctrl-s").unwrap())].into(), KeybindSource::Base),
 ///     Hsla::black()
 /// )
 ///     .prefix("Save:")
@@ -45,10 +46,11 @@ impl KeybindingHint {
     /// 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::new(vec![KeybindingKeystroke::from_keystroke(Keystroke::parse("ctrl-c").unwrap())], cx),
+    ///     KeyBinding::from_keystrokes(vec![KeybindingKeystroke::from_keystroke(Keystroke::parse("ctrl-c").unwrap())].into(), KeybindSource::Base),
     ///     Hsla::black()
     /// );
     /// # }
@@ -74,11 +76,12 @@ impl KeybindingHint {
     /// 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::new(vec![KeybindingKeystroke::from_keystroke(Keystroke::parse("ctrl-c").unwrap())], cx),
+    ///     KeyBinding::from_keystrokes(vec![KeybindingKeystroke::from_keystroke(Keystroke::parse("ctrl-c").unwrap())].into(), KeybindSource::Base),
     ///     Hsla::black()
     /// );
     /// # }
@@ -108,10 +111,11 @@ impl KeybindingHint {
     /// 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::new(vec![KeybindingKeystroke::from_keystroke(Keystroke::parse("ctrl-v").unwrap())], cx),
+    ///     KeyBinding::from_keystrokes(vec![KeybindingKeystroke::from_keystroke(Keystroke::parse("ctrl-v").unwrap())].into(), KeybindSource::Base),
     ///     "Paste",
     ///     Hsla::black()
     /// );
@@ -141,10 +145,11 @@ impl KeybindingHint {
     /// 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::new(vec![KeybindingKeystroke::from_keystroke(Keystroke::parse("ctrl-x").unwrap())], cx),
+    ///     KeyBinding::from_keystrokes(vec![KeybindingKeystroke::from_keystroke(Keystroke::parse("ctrl-x").unwrap())].into(), KeybindSource::Base),
     ///     Hsla::black()
     /// )
     ///     .prefix("Cut:");
@@ -165,10 +170,11 @@ impl KeybindingHint {
     /// 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::new(vec![KeybindingKeystroke::from_keystroke(Keystroke::parse("ctrl-f").unwrap())], cx),
+    ///     KeyBinding::from_keystrokes(vec![KeybindingKeystroke::from_keystroke(Keystroke::parse("ctrl-f").unwrap())].into(), KeybindSource::Base),
     ///     Hsla::black()
     /// )
     ///     .suffix("Find");
@@ -189,10 +195,11 @@ impl KeybindingHint {
     /// 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::new(vec![KeybindingKeystroke::from_keystroke(Keystroke::parse("ctrl-z").unwrap())], cx),
+    ///     KeyBinding::from_keystrokes(vec![KeybindingKeystroke::from_keystroke(Keystroke::parse("ctrl-z").unwrap())].into(), KeybindSource::Base),
     ///     Hsla::black()
     /// )
     ///     .size(Pixels::from(16.0));
@@ -265,10 +272,8 @@ impl Component for KeybindingHint {
         Some("Displays a keyboard shortcut hint with optional prefix and suffix text")
     }
 
-    fn preview(window: &mut Window, cx: &mut App) -> Option<AnyElement> {
-        let enter_fallback = gpui::KeyBinding::new("enter", menu::Confirm, None);
-        let enter = KeyBinding::for_action(&menu::Confirm, window, cx)
-            .unwrap_or(KeyBinding::new_from_gpui(enter_fallback, cx));
+    fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
+        let enter = KeyBinding::for_action(&menu::Confirm, cx);
 
         let bg_color = cx.theme().colors().surface_background;
 

crates/ui/src/components/stories/keybinding.rs 🔗

@@ -1,6 +1,7 @@
 use gpui::NoAction;
 use gpui::Render;
 use itertools::Itertools;
+use settings::KeybindSource;
 use story::Story;
 
 use crate::{KeyBinding, prelude::*};
@@ -15,19 +16,36 @@ impl Render for KeybindingStory {
     fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
         let all_modifier_permutations = ["ctrl", "alt", "cmd", "shift"].into_iter().permutations(2);
 
+        const SOURCE: KeybindSource = KeybindSource::Base;
+
         Story::container(cx)
             .child(Story::title_for::<KeyBinding>(cx))
             .child(Story::label("Single Key", cx))
-            .child(KeyBinding::new_from_gpui(binding("Z"), cx))
+            .child(KeyBinding::from_keystrokes(
+                binding("Z").keystrokes().into(),
+                SOURCE,
+            ))
             .child(Story::label("Single Key with Modifier", cx))
             .child(
                 div()
                     .flex()
                     .gap_3()
-                    .child(KeyBinding::new_from_gpui(binding("ctrl-c"), cx))
-                    .child(KeyBinding::new_from_gpui(binding("alt-c"), cx))
-                    .child(KeyBinding::new_from_gpui(binding("cmd-c"), cx))
-                    .child(KeyBinding::new_from_gpui(binding("shift-c"), cx)),
+                    .child(KeyBinding::from_keystrokes(
+                        binding("ctrl-c").keystrokes().into(),
+                        SOURCE,
+                    ))
+                    .child(KeyBinding::from_keystrokes(
+                        binding("alt-c").keystrokes().into(),
+                        SOURCE,
+                    ))
+                    .child(KeyBinding::from_keystrokes(
+                        binding("cmd-c").keystrokes().into(),
+                        SOURCE,
+                    ))
+                    .child(KeyBinding::from_keystrokes(
+                        binding("shift-c").keystrokes().into(),
+                        SOURCE,
+                    )),
             )
             .child(Story::label("Single Key with Modifier (Permuted)", cx))
             .child(
@@ -41,58 +59,77 @@ impl Render for KeybindingStory {
                                 .gap_4()
                                 .py_3()
                                 .children(chunk.map(|permutation| {
-                                    KeyBinding::new_from_gpui(
-                                        binding(&(permutation.join("-") + "-x")),
-                                        cx,
+                                    KeyBinding::from_keystrokes(
+                                        binding(&(permutation.join("-") + "-x"))
+                                            .keystrokes()
+                                            .into(),
+                                        SOURCE,
                                     )
                                 }))
                         }),
                 ),
             )
             .child(Story::label("Single Key with All Modifiers", cx))
-            .child(KeyBinding::new_from_gpui(
-                binding("ctrl-alt-cmd-shift-z"),
-                cx,
+            .child(KeyBinding::from_keystrokes(
+                binding("ctrl-alt-cmd-shift-z").keystrokes().into(),
+                SOURCE,
             ))
             .child(Story::label("Chord", cx))
-            .child(KeyBinding::new_from_gpui(binding("a z"), cx))
+            .child(KeyBinding::from_keystrokes(
+                binding("a z").keystrokes().into(),
+                SOURCE,
+            ))
             .child(Story::label("Chord with Modifier", cx))
-            .child(KeyBinding::new_from_gpui(binding("ctrl-a shift-z"), cx))
-            .child(KeyBinding::new_from_gpui(binding("fn-s"), cx))
+            .child(KeyBinding::from_keystrokes(
+                binding("ctrl-a shift-z").keystrokes().into(),
+                SOURCE,
+            ))
+            .child(KeyBinding::from_keystrokes(
+                binding("fn-s").keystrokes().into(),
+                SOURCE,
+            ))
             .child(Story::label("Single Key with All Modifiers (Linux)", cx))
             .child(
-                KeyBinding::new_from_gpui(binding("ctrl-alt-cmd-shift-z"), cx)
-                    .platform_style(PlatformStyle::Linux),
+                KeyBinding::from_keystrokes(
+                    binding("ctrl-alt-cmd-shift-z").keystrokes().into(),
+                    SOURCE,
+                )
+                .platform_style(PlatformStyle::Linux),
             )
             .child(Story::label("Chord (Linux)", cx))
             .child(
-                KeyBinding::new_from_gpui(binding("a z"), cx).platform_style(PlatformStyle::Linux),
+                KeyBinding::from_keystrokes(binding("a z").keystrokes().into(), SOURCE)
+                    .platform_style(PlatformStyle::Linux),
             )
             .child(Story::label("Chord with Modifier (Linux)", cx))
             .child(
-                KeyBinding::new_from_gpui(binding("ctrl-a shift-z"), cx)
+                KeyBinding::from_keystrokes(binding("ctrl-a shift-z").keystrokes().into(), SOURCE)
                     .platform_style(PlatformStyle::Linux),
             )
             .child(
-                KeyBinding::new_from_gpui(binding("fn-s"), cx).platform_style(PlatformStyle::Linux),
+                KeyBinding::from_keystrokes(binding("fn-s").keystrokes().into(), SOURCE)
+                    .platform_style(PlatformStyle::Linux),
             )
             .child(Story::label("Single Key with All Modifiers (Windows)", cx))
             .child(
-                KeyBinding::new_from_gpui(binding("ctrl-alt-cmd-shift-z"), cx)
-                    .platform_style(PlatformStyle::Windows),
+                KeyBinding::from_keystrokes(
+                    binding("ctrl-alt-cmd-shift-z").keystrokes().into(),
+                    SOURCE,
+                )
+                .platform_style(PlatformStyle::Windows),
             )
             .child(Story::label("Chord (Windows)", cx))
             .child(
-                KeyBinding::new_from_gpui(binding("a z"), cx)
+                KeyBinding::from_keystrokes(binding("a z").keystrokes().into(), SOURCE)
                     .platform_style(PlatformStyle::Windows),
             )
             .child(Story::label("Chord with Modifier (Windows)", cx))
             .child(
-                KeyBinding::new_from_gpui(binding("ctrl-a shift-z"), cx)
+                KeyBinding::from_keystrokes(binding("ctrl-a shift-z").keystrokes().into(), SOURCE)
                     .platform_style(PlatformStyle::Windows),
             )
             .child(
-                KeyBinding::new_from_gpui(binding("fn-s"), cx)
+                KeyBinding::from_keystrokes(binding("fn-s").keystrokes().into(), SOURCE)
                     .platform_style(PlatformStyle::Windows),
             )
     }

crates/ui/src/components/tooltip.rs 🔗

@@ -64,11 +64,11 @@ impl Tooltip {
     ) -> impl Fn(&mut Window, &mut App) -> AnyView + use<T> {
         let title = title.into();
         let action = action.boxed_clone();
-        move |window, cx| {
+        move |_, cx| {
             cx.new(|cx| Self {
                 title: Title::Str(title.clone()),
                 meta: None,
-                key_binding: KeyBinding::for_action(action.as_ref(), window, cx),
+                key_binding: Some(KeyBinding::for_action(action.as_ref(), cx)),
             })
             .into()
         }
@@ -82,11 +82,15 @@ impl Tooltip {
         let title = title.into();
         let action = action.boxed_clone();
         let focus_handle = focus_handle.clone();
-        move |window, cx| {
+        move |_, cx| {
             cx.new(|cx| Self {
                 title: Title::Str(title.clone()),
                 meta: None,
-                key_binding: KeyBinding::for_action_in(action.as_ref(), &focus_handle, window, cx),
+                key_binding: Some(KeyBinding::for_action_in(
+                    action.as_ref(),
+                    &focus_handle,
+                    cx,
+                )),
             })
             .into()
         }
@@ -95,13 +99,12 @@ impl Tooltip {
     pub fn for_action(
         title: impl Into<SharedString>,
         action: &dyn Action,
-        window: &mut Window,
         cx: &mut App,
     ) -> AnyView {
         cx.new(|cx| Self {
             title: Title::Str(title.into()),
             meta: None,
-            key_binding: KeyBinding::for_action(action, window, cx),
+            key_binding: Some(KeyBinding::for_action(action, cx)),
         })
         .into()
     }
@@ -110,13 +113,12 @@ impl Tooltip {
         title: impl Into<SharedString>,
         action: &dyn Action,
         focus_handle: &FocusHandle,
-        window: &mut Window,
         cx: &mut App,
     ) -> AnyView {
         cx.new(|cx| Self {
             title: title.into().into(),
             meta: None,
-            key_binding: KeyBinding::for_action_in(action, focus_handle, window, cx),
+            key_binding: Some(KeyBinding::for_action_in(action, focus_handle, cx)),
         })
         .into()
     }
@@ -125,13 +127,12 @@ impl Tooltip {
         title: impl Into<SharedString>,
         action: Option<&dyn Action>,
         meta: impl Into<SharedString>,
-        window: &mut Window,
         cx: &mut App,
     ) -> AnyView {
         cx.new(|cx| Self {
             title: title.into().into(),
             meta: Some(meta.into()),
-            key_binding: action.and_then(|action| KeyBinding::for_action(action, window, cx)),
+            key_binding: action.map(|action| KeyBinding::for_action(action, cx)),
         })
         .into()
     }
@@ -141,14 +142,12 @@ impl Tooltip {
         action: Option<&dyn Action>,
         meta: impl Into<SharedString>,
         focus_handle: &FocusHandle,
-        window: &mut Window,
         cx: &mut App,
     ) -> AnyView {
         cx.new(|cx| Self {
             title: title.into().into(),
             meta: Some(meta.into()),
-            key_binding: action
-                .and_then(|action| KeyBinding::for_action_in(action, focus_handle, window, cx)),
+            key_binding: action.map(|action| KeyBinding::for_action_in(action, focus_handle, cx)),
         })
         .into()
     }

crates/ui_input/Cargo.toml 🔗

@@ -14,14 +14,11 @@ path = "src/ui_input.rs"
 [dependencies]
 component.workspace = true
 editor.workspace = true
-fuzzy.workspace = true
 gpui.workspace = true
 menu.workspace = true
-picker.workspace = true
 settings.workspace = true
 theme.workspace = true
 ui.workspace = true
-workspace-hack.workspace = true
 
 [features]
 default = []

crates/ui_input/src/input_field.rs 🔗

@@ -0,0 +1,222 @@
+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<SharedString>,
+    /// The size of the label text.
+    label_size: LabelSize,
+    /// The placeholder text for the text field.
+    placeholder: SharedString,
+    /// Exposes the underlying [`Entity<Editor>`] 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<Editor>,
+    /// 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<IconName>,
+    /// Whether the text field is disabled.
+    disabled: bool,
+    /// The minimum width of for the input
+    min_width: Length,
+}
+
+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<SharedString>) -> 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(),
+        }
+    }
+
+    pub fn start_icon(mut self, icon: IconName) -> Self {
+        self.start_icon = Some(icon);
+        self
+    }
+
+    pub fn label(mut self, label: impl Into<SharedString>) -> 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<Length>) -> Self {
+        self.min_width = width.into();
+        self
+    }
+
+    pub fn set_disabled(&mut self, disabled: bool, cx: &mut Context<Self>) {
+        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<Editor> {
+        &self.editor
+    }
+
+    pub fn text(&self, cx: &App) -> String {
+        self.editor().read(cx).text(cx)
+    }
+
+    pub fn set_text(&self, text: impl Into<Arc<str>>, 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, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+        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()
+        };
+
+        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()
+                    .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_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<AnyElement> {
+        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(),
+        )
+    }
+}

crates/ui_input/src/number_field.rs 🔗

@@ -8,7 +8,7 @@ use std::{
 use editor::{Editor, EditorStyle};
 use gpui::{ClickEvent, Entity, FocusHandle, Focusable, FontWeight, Modifiers};
 
-use settings::{CodeFade, MinimumContrast};
+use settings::{CenteredPaddingSettings, CodeFade, DelayMs, InactiveOpacity, MinimumContrast};
 use ui::prelude::*;
 
 #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
@@ -31,78 +31,55 @@ pub trait NumberFieldType: Display + Copy + Clone + Sized + PartialOrd + FromStr
     fn saturating_sub(self, rhs: Self) -> Self;
 }
 
-impl NumberFieldType for gpui::FontWeight {
-    fn default_step() -> Self {
-        FontWeight(50.0)
-    }
-    fn large_step() -> Self {
-        FontWeight(100.0)
-    }
-    fn small_step() -> Self {
-        FontWeight(10.0)
-    }
-    fn min_value() -> Self {
-        gpui::FontWeight::THIN
-    }
-    fn max_value() -> Self {
-        gpui::FontWeight::BLACK
-    }
-    fn saturating_add(self, rhs: Self) -> Self {
-        FontWeight((self.0 + rhs.0).min(Self::max_value().0))
-    }
-    fn saturating_sub(self, rhs: Self) -> Self {
-        FontWeight((self.0 - rhs.0).max(Self::min_value().0))
-    }
-}
+macro_rules! impl_newtype_numeric_stepper {
+    ($type:ident, $default:expr, $large:expr, $small:expr, $min:expr, $max:expr) => {
+        impl NumberFieldType for $type {
+            fn default_step() -> Self {
+                $default.into()
+            }
 
-impl NumberFieldType for settings::CodeFade {
-    fn default_step() -> Self {
-        CodeFade(0.10)
-    }
-    fn large_step() -> Self {
-        CodeFade(0.20)
-    }
-    fn small_step() -> Self {
-        CodeFade(0.05)
-    }
-    fn min_value() -> Self {
-        CodeFade(0.0)
-    }
-    fn max_value() -> Self {
-        CodeFade(0.9)
-    }
-    fn saturating_add(self, rhs: Self) -> Self {
-        CodeFade((self.0 + rhs.0).min(Self::max_value().0))
-    }
-    fn saturating_sub(self, rhs: Self) -> Self {
-        CodeFade((self.0 - rhs.0).max(Self::min_value().0))
-    }
-}
+            fn large_step() -> Self {
+                $large.into()
+            }
 
-impl NumberFieldType for settings::MinimumContrast {
-    fn default_step() -> Self {
-        MinimumContrast(1.0)
-    }
-    fn large_step() -> Self {
-        MinimumContrast(10.0)
-    }
-    fn small_step() -> Self {
-        MinimumContrast(0.5)
-    }
-    fn min_value() -> Self {
-        MinimumContrast(0.0)
-    }
-    fn max_value() -> Self {
-        MinimumContrast(106.0)
-    }
-    fn saturating_add(self, rhs: Self) -> Self {
-        MinimumContrast((self.0 + rhs.0).min(Self::max_value().0))
-    }
-    fn saturating_sub(self, rhs: Self) -> Self {
-        MinimumContrast((self.0 - rhs.0).max(Self::min_value().0))
-    }
+            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))
+            }
+        }
+    };
 }
 
+#[rustfmt::skip]
+impl_newtype_numeric_stepper!(FontWeight, 50., 100., 10., FontWeight::THIN, FontWeight::BLACK);
+impl_newtype_numeric_stepper!(CodeFade, 0.1, 0.2, 0.05, 0.0, 0.9);
+impl_newtype_numeric_stepper!(InactiveOpacity, 0.1, 0.2, 0.05, 0.0, 1.0);
+impl_newtype_numeric_stepper!(MinimumContrast, 1., 10., 0.5, 0.0, 106.0);
+impl_newtype_numeric_stepper!(DelayMs, 100, 500, 10, 0, 2000);
+impl_newtype_numeric_stepper!(
+    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 {

crates/ui_input/src/ui_input.rs 🔗

@@ -1,233 +1,9 @@
-//! # UI – Text Field
-//!
-//! This crate provides a text field component that can be used to create text fields like search inputs, form fields, etc.
+//! 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 font_picker;
+mod input_field;
 mod number_field;
 
-use component::{example_group, single_example};
-use editor::{Editor, EditorElement, EditorStyle};
-pub use font_picker::*;
-use gpui::{App, Entity, FocusHandle, Focusable, FontStyle, Hsla, Length, TextStyle};
+pub use input_field::*;
 pub use number_field::*;
-use settings::Settings;
-use std::sync::Arc;
-use theme::ThemeSettings;
-use ui::prelude::*;
-
-pub struct SingleLineInputStyle {
-    text_color: Hsla,
-    background_color: Hsla,
-    border_color: Hsla,
-}
-
-/// A Text Field 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 SingleLineInput {
-    /// An optional label for the text field.
-    ///
-    /// Its position is determined by the [`FieldLabelLayout`].
-    label: Option<SharedString>,
-    /// The size of the label text.
-    label_size: LabelSize,
-    /// The placeholder text for the text field.
-    placeholder: SharedString,
-    /// Exposes the underlying [`Entity<Editor>`] 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<Editor>,
-    /// 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<IconName>,
-    /// Whether the text field is disabled.
-    disabled: bool,
-    /// The minimum width of for the input
-    min_width: Length,
-}
-
-impl Focusable for SingleLineInput {
-    fn focus_handle(&self, cx: &App) -> FocusHandle {
-        self.editor.focus_handle(cx)
-    }
-}
-
-impl SingleLineInput {
-    pub fn new(window: &mut Window, cx: &mut App, placeholder: impl Into<SharedString>) -> 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(),
-        }
-    }
-
-    pub fn start_icon(mut self, icon: IconName) -> Self {
-        self.start_icon = Some(icon);
-        self
-    }
-
-    pub fn label(mut self, label: impl Into<SharedString>) -> 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<Length>) -> Self {
-        self.min_width = width.into();
-        self
-    }
-
-    pub fn set_disabled(&mut self, disabled: bool, cx: &mut Context<Self>) {
-        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<Editor> {
-        &self.editor
-    }
-
-    pub fn text(&self, cx: &App) -> String {
-        self.editor().read(cx).text(cx)
-    }
-
-    pub fn set_text(&self, text: impl Into<Arc<str>>, window: &mut Window, cx: &mut App) {
-        self.editor()
-            .update(cx, |editor, cx| editor.set_text(text, window, cx))
-    }
-}
-
-impl Render for SingleLineInput {
-    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
-        let settings = ThemeSettings::get_global(cx);
-        let theme_color = cx.theme().colors();
-
-        let mut style = SingleLineInputStyle {
-            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()
-        };
-
-        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()
-                    .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_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 SingleLineInput {
-    fn scope() -> ComponentScope {
-        ComponentScope::Input
-    }
-
-    fn preview(window: &mut Window, cx: &mut App) -> Option<AnyElement> {
-        let input_small =
-            cx.new(|cx| SingleLineInput::new(window, cx, "placeholder").label("Small Label"));
-
-        let input_regular = cx.new(|cx| {
-            SingleLineInput::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(),
-        )
-    }
-}

crates/ui_macros/Cargo.toml 🔗

@@ -15,7 +15,6 @@ proc-macro = true
 [dependencies]
 quote.workspace = true
 syn.workspace = true
-workspace-hack.workspace = true
 
 [dev-dependencies]
 component.workspace = true

crates/ui_prompt/Cargo.toml 🔗

@@ -22,4 +22,3 @@ settings.workspace = true
 theme.workspace = true
 ui.workspace = true
 workspace.workspace = true
-workspace-hack.workspace = true

crates/util/Cargo.toml 🔗

@@ -45,7 +45,6 @@ unicase.workspace = true
 util_macros = { workspace = true, optional = true }
 walkdir.workspace = true
 which.workspace = true
-workspace-hack.workspace = true
 
 [target.'cfg(unix)'.dependencies]
 command-fds = "0.3.1"

crates/util/src/paths.rs 🔗

@@ -934,7 +934,7 @@ where
 /// 2. When encountering digits, treating consecutive digits as a single number
 /// 3. Comparing numbers by their numeric value rather than lexicographically
 /// 4. For non-numeric characters, using case-sensitive comparison with lowercase priority
-fn natural_sort(a: &str, b: &str) -> Ordering {
+pub fn natural_sort(a: &str, b: &str) -> Ordering {
     let mut a_iter = a.chars().peekable();
     let mut b_iter = b.chars().peekable();
 

crates/util/src/shell.rs 🔗

@@ -1,6 +1,7 @@
+use serde::{Deserialize, Serialize};
 use std::{borrow::Cow, fmt, path::Path, sync::LazyLock};
 
-#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
+#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
 pub enum ShellKind {
     #[default]
     Posix,

crates/util/src/util.rs 🔗

@@ -353,7 +353,10 @@ pub async fn load_login_shell_environment() -> Result<()> {
     // into shell's `cd` command (and hooks) to manipulate env.
     // We do this so that we get the env a user would have when spawning a shell
     // in home directory.
-    for (name, value) in shell_env::capture(get_system_shell(), &[], paths::home_dir()).await? {
+    for (name, value) in shell_env::capture(get_system_shell(), &[], paths::home_dir())
+        .await
+        .with_context(|| format!("capturing environment with {:?}", get_system_shell()))?
+    {
         unsafe { env::set_var(&name, &value) };
     }
 
@@ -627,7 +630,7 @@ where
 }
 
 pub fn log_err<E: std::fmt::Debug>(error: &E) {
-    log_error_with_caller(*Location::caller(), error, log::Level::Warn);
+    log_error_with_caller(*Location::caller(), error, log::Level::Error);
 }
 
 pub trait TryFutureExt {

crates/util_macros/Cargo.toml 🔗

@@ -18,7 +18,6 @@ doctest = false
 quote.workspace = true
 syn.workspace = true
 perf.workspace = true
-workspace-hack.workspace = true
 
 [features]
 perf-enabled = []

crates/vercel/Cargo.toml 🔗

@@ -20,4 +20,3 @@ anyhow.workspace = true
 schemars = { workspace = true, optional = true }
 serde.workspace = true
 strum.workspace = true
-workspace-hack.workspace = true

crates/vim/Cargo.toml 🔗

@@ -53,7 +53,6 @@ util_macros.workspace = true
 vim_mode_setting.workspace = true
 workspace.workspace = true
 zed_actions.workspace = true
-workspace-hack.workspace = true
 
 [dev-dependencies]
 assets.workspace = true

crates/vim/src/change_list.rs 🔗

@@ -50,7 +50,8 @@ impl Vim {
 
     pub(crate) fn push_to_change_list(&mut self, window: &mut Window, cx: &mut Context<Self>) {
         let Some((new_positions, buffer)) = self.update_editor(cx, |vim, editor, cx| {
-            let (map, selections) = editor.selections.all_adjusted_display(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
@@ -59,7 +60,7 @@ impl Vim {
                 .map(|previous| {
                     previous.len() == selections.len()
                         && previous.iter().enumerate().all(|(ix, p)| {
-                            p.to_display_point(&map).row() == selections[ix].head().row()
+                            p.to_display_point(&display_map).row() == selections[ix].head().row()
                         })
                 })
                 .unwrap_or(false);
@@ -68,11 +69,11 @@ impl Vim {
                 .into_iter()
                 .map(|s| {
                     let point = if vim.mode == Mode::Insert {
-                        movement::saturating_left(&map, s.head())
+                        movement::saturating_left(&display_map, s.head())
                     } else {
                         s.head()
                     };
-                    map.display_point_to_anchor(point, Bias::Left)
+                    display_map.display_point_to_anchor(point, Bias::Left)
                 })
                 .collect::<Vec<_>>();
 

crates/vim/src/command.rs 🔗

@@ -606,7 +606,9 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
         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::<Point>(cx);
+            let current = editor
+                .selections
+                .newest::<Point>(&editor.display_snapshot(cx));
             let target = snapshot
                 .buffer_snapshot()
                 .clip_point(Point::new(buffer_row.0, current.head().column), Bias::Left);
@@ -1903,7 +1905,9 @@ impl OnMatchingLines {
                         });
                         window.dispatch_action(action, cx);
                         cx.defer_in(window, move |editor, window, cx| {
-                            let newest = editor.selections.newest::<Point>(cx);
+                            let newest = editor
+                                .selections
+                                .newest::<Point>(&editor.display_snapshot(cx));
                             editor.change_selections(
                                 SelectionEffects::no_scroll(),
                                 window,
@@ -2000,7 +2004,9 @@ impl Vim {
         };
         let command = self.update_editor(cx, |_, editor, cx| {
             let snapshot = editor.snapshot(window, cx);
-            let start = editor.selections.newest_display(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(
@@ -2047,7 +2053,9 @@ impl Vim {
         };
         let command = self.update_editor(cx, |_, editor, cx| {
             let snapshot = editor.snapshot(window, cx);
-            let start = editor.selections.newest_display(cx);
+            let start = editor
+                .selections
+                .newest_display(&editor.display_snapshot(cx));
             let range = object
                 .range(&snapshot, start.clone(), around, None)
                 .unwrap_or(start.range());
@@ -2156,7 +2164,11 @@ impl ShellExec {
                 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::<Point>(cx).range().end;
+                let mut end = editor
+                    .selections
+                    .newest::<Point>(&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

crates/vim/src/helix.rs 🔗

@@ -345,7 +345,7 @@ impl Vim {
         self.update_editor(cx, |vim, editor, cx| {
             let has_selection = editor
                 .selections
-                .all_adjusted(cx)
+                .all_adjusted(&editor.display_snapshot(cx))
                 .iter()
                 .any(|selection| !selection.is_empty());
 
@@ -478,19 +478,20 @@ impl Vim {
     pub fn helix_replace(&mut self, text: &str, window: &mut Window, cx: &mut Context<Self>) {
         self.update_editor(cx, |_, editor, cx| {
             editor.transact(window, cx, |editor, window, cx| {
-                let (map, selections) = editor.selections.all_display(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(&map, Bias::Left);
-                        let end_offset = range.end.to_offset(&map, Bias::Left);
+                        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;
                         (
-                            map.buffer_snapshot().anchor_before(start_offset),
+                            display_map.buffer_snapshot().anchor_before(start_offset),
                             end_offset - start_offset,
                             was_empty,
                             was_reversed,
@@ -504,11 +505,11 @@ impl Vim {
 
                     // For empty selections, extend to replace one character
                     if range.is_empty() {
-                        range.end = movement::saturating_right(&map, range.start);
+                        range.end = movement::saturating_right(&display_map, range.start);
                     }
 
-                    let byte_range = range.start.to_offset(&map, Bias::Left)
-                        ..range.end.to_offset(&map, Bias::Left);
+                    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.len());
@@ -568,7 +569,7 @@ impl Vim {
         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::<Point>(cx);
+            let mut selections = editor.selections.all::<Point>(&display_map);
             let max_point = display_map.buffer_snapshot().max_point();
             let buffer_snapshot = &display_map.buffer_snapshot();
 
@@ -606,7 +607,9 @@ impl Vim {
         cx: &mut Context<Self>,
     ) {
         self.update_editor(cx, |_, editor, cx| {
-            let newest = editor.selections.newest::<usize>(cx);
+            let newest = editor
+                .selections
+                .newest::<usize>(&editor.display_snapshot(cx));
             editor.change_selections(Default::default(), window, cx, |s| s.select(vec![newest]));
         });
     }
@@ -633,7 +636,10 @@ impl Vim {
                 if yank {
                     vim.copy_selections_content(editor, MotionKind::Exclusive, window, cx);
                 }
-                let selections = editor.selections.all::<Point>(cx).into_iter();
+                let selections = editor
+                    .selections
+                    .all::<Point>(&editor.display_snapshot(cx))
+                    .into_iter();
                 let edits = selections.map(|selection| (selection.start..selection.end, ""));
                 editor.edit(edits, cx);
             });

crates/vim/src/helix/duplicate.rs 🔗

@@ -56,7 +56,8 @@ impl Vim {
         let times = times.unwrap_or(1);
         self.update_editor(cx, |_, editor, cx| {
             let mut selections = Vec::new();
-            let (map, mut original_selections) = editor.selections.all_display(cx);
+            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();

crates/vim/src/helix/paste.rs 🔗

@@ -44,7 +44,8 @@ impl Vim {
                     return;
                 };
 
-                let (display_map, current_selections) = editor.selections.all_adjusted_display(cx);
+                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

crates/vim/src/insert.rs 🔗

@@ -50,17 +50,23 @@ impl Vim {
         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);
 
-                if !HelixModeSetting::get_global(cx).0 {
-                    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)
-                        });
+                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);
@@ -84,7 +90,7 @@ impl Vim {
         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::<Point>(cx) {
+            for selection in editor.selections.all::<Point>(&editor.display_snapshot(cx)) {
                 let point = selection.head();
                 let new_row = match direction {
                     Direction::Next => point.row + 1,

crates/vim/src/mode_indicator.rs 🔗

@@ -1,4 +1,4 @@
-use gpui::{Context, Entity, Render, Subscription, WeakEntity, Window, div};
+use gpui::{Context, Element, Entity, FontWeight, Render, Subscription, WeakEntity, Window, div};
 use ui::text_for_keystrokes;
 use workspace::{StatusItemView, item::ItemHandle, ui::prelude::*};
 
@@ -93,13 +93,33 @@ impl Render for ModeIndicator {
         };
 
         let vim_readable = vim.read(cx);
-        let label = if let Some(label) = vim_readable.status_label.clone() {
-            label
+        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<SharedString>) = if let Some(label) = status_label
+        {
+            (label, None)
         } else {
-            let mode = if vim_readable.temp_mode {
-                format!("(insert) {}", vim_readable.mode)
+            let mode_str = if temp_mode {
+                format!("(insert) {}", mode)
             } else {
-                vim_readable.mode.to_string()
+                mode.to_string()
             };
 
             let current_operators_description = self.current_operators_description(vim.clone(), cx);
@@ -107,13 +127,45 @@ impl Render for ModeIndicator {
                 .pending_keys
                 .as_ref()
                 .unwrap_or(&current_operators_description);
-            format!("{} -- {} --", pending, mode).into()
+            let mode = if bg_color != system_transparent {
+                mode_str.into()
+            } else {
+                format!("-- {} --", mode_str).into()
+            };
+            (pending.into(), Some(mode))
         };
-
-        Label::new(label)
-            .size(LabelSize::Small)
-            .line_height_style(LineHeightStyle::UiLabel)
-            .into_any_element()
+        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()
     }
 }
 

crates/vim/src/motion.rs 🔗

@@ -3083,7 +3083,7 @@ mod test {
         state::Mode,
         test::{NeovimBackedTestContext, VimTestContext},
     };
-    use editor::display_map::Inlay;
+    use editor::Inlay;
     use indoc::indoc;
     use language::Point;
     use multi_buffer::MultiBufferRow;

crates/vim/src/normal.rs 🔗

@@ -657,7 +657,7 @@ impl Vim {
         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::<Point>(cx);
+                let selections = editor.selections.all::<Point>(&editor.display_snapshot(cx));
                 let snapshot = editor.buffer().read(cx).snapshot(cx);
 
                 let selection_start_rows: BTreeSet<u32> = selections
@@ -699,7 +699,7 @@ impl Vim {
         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::<Point>(cx);
+                let selections = editor.selections.all::<Point>(&editor.display_snapshot(cx));
                 let snapshot = editor.buffer().read(cx).snapshot(cx);
 
                 let selection_end_rows: BTreeSet<u32> = selections
@@ -745,7 +745,7 @@ impl Vim {
         Vim::take_forced_motion(cx);
         self.update_editor(cx, |_, editor, cx| {
             editor.transact(window, cx, |editor, _, cx| {
-                let selections = editor.selections.all::<Point>(cx);
+                let selections = editor.selections.all::<Point>(&editor.display_snapshot(cx));
 
                 let selection_start_rows: BTreeSet<u32> = selections
                     .into_iter()
@@ -774,9 +774,10 @@ impl Vim {
         Vim::take_forced_motion(cx);
         self.update_editor(cx, |_, editor, cx| {
             editor.transact(window, cx, |editor, window, cx| {
-                let selections = editor.selections.all::<Point>(cx);
+                let display_map = editor.display_snapshot(cx);
+                let selections = editor.selections.all::<Point>(&display_map);
                 let snapshot = editor.buffer().read(cx).snapshot(cx);
-                let (_map, display_selections) = editor.selections.all_display(cx);
+                let display_selections = editor.selections.all_display(&display_map);
                 let original_positions = display_selections
                     .iter()
                     .map(|s| (s.id, s.head()))
@@ -937,13 +938,14 @@ impl Vim {
         self.update_editor(cx, |_, editor, cx| {
             editor.transact(window, cx, |editor, window, cx| {
                 editor.set_clip_at_line_ends(false, cx);
-                let (map, display_selections) = editor.selections.all_display(cx);
+                let display_map = editor.display_snapshot(cx);
+                let display_selections = editor.selections.all_display(&display_map);
 
-                let mut edits = Vec::new();
+                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(&map, range.end);
+                        let new_point = movement::saturating_right(&display_map, range.end);
                         if range.end == new_point {
                             return;
                         }
@@ -951,8 +953,8 @@ impl Vim {
                     }
 
                     edits.push((
-                        range.start.to_offset(&map, Bias::Left)
-                            ..range.end.to_offset(&map, Bias::Left),
+                        range.start.to_offset(&display_map, Bias::Left)
+                            ..range.end.to_offset(&display_map, Bias::Left),
                         text.repeat(if is_return_char { 0 } else { count }),
                     ));
                 }
@@ -976,16 +978,16 @@ impl Vim {
     pub fn save_selection_starts(
         &self,
         editor: &Editor,
-
         cx: &mut Context<Editor>,
     ) -> HashMap<usize, Anchor> {
-        let (map, selections) = editor.selections.all_display(cx);
+        let display_map = editor.display_snapshot(cx);
+        let selections = editor.selections.all_display(&display_map);
         selections
             .iter()
             .map(|selection| {
                 (
                     selection.id,
-                    map.display_point_to_anchor(selection.start, Bias::Right),
+                    display_map.display_point_to_anchor(selection.start, Bias::Right),
                 )
             })
             .collect::<HashMap<_, _>>()

crates/vim/src/normal/convert.rs 🔗

@@ -199,7 +199,7 @@ impl Vim {
             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(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);

crates/vim/src/normal/increment.rs 🔗

@@ -58,7 +58,7 @@ impl Vim {
             let mut new_anchors = Vec::new();
 
             let snapshot = editor.buffer().read(cx).snapshot(cx);
-            for selection in editor.selections.all_adjusted(cx) {
+            for selection in editor.selections.all_adjusted(&editor.display_snapshot(cx)) {
                 if !selection.is_empty()
                     && (vim.mode != Mode::VisualBlock || new_anchors.is_empty())
                 {

crates/vim/src/normal/mark.rs 🔗

@@ -50,16 +50,19 @@ impl Vim {
         let mut reversed = vec![];
 
         self.update_editor(cx, |vim, editor, cx| {
-            let (map, selections) = editor.selections.all_display(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(&map, selection.end);
+                let end = movement::saturating_left(&display_map, selection.end);
                 ends.push(
-                    map.buffer_snapshot()
-                        .anchor_before(end.to_offset(&map, Bias::Left)),
+                    display_map
+                        .buffer_snapshot()
+                        .anchor_before(end.to_offset(&display_map, Bias::Left)),
                 );
                 starts.push(
-                    map.buffer_snapshot()
-                        .anchor_before(selection.start.to_offset(&map, Bias::Left)),
+                    display_map
+                        .buffer_snapshot()
+                        .anchor_before(selection.start.to_offset(&display_map, Bias::Left)),
                 );
                 reversed.push(selection.reversed)
             }
@@ -301,19 +304,21 @@ impl Vim {
             name = "'";
         }
         if matches!(name, "{" | "}" | "(" | ")") {
-            let (map, selections) = editor.selections.all_display(cx);
+            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(&map, selection.head(), 1),
-                        "}" => movement::end_of_paragraph(&map, selection.head(), 1),
-                        "(" => motion::sentence_backwards(&map, selection.head(), 1),
-                        ")" => motion::sentence_forwards(&map, selection.head(), 1),
+                        "{" => 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!(),
                     };
-                    map.buffer_snapshot()
-                        .anchor_before(point.to_offset(&map, Bias::Left))
+                    display_map
+                        .buffer_snapshot()
+                        .anchor_before(point.to_offset(&display_map, Bias::Left))
                 })
                 .collect::<Vec<Anchor>>();
             return Some(Mark::Local(anchors));

crates/vim/src/normal/paste.rs 🔗

@@ -56,7 +56,8 @@ impl Vim {
                     vim.copy_selections_content(editor, MotionKind::for_mode(vim.mode), window, cx);
                 }
 
-                let (display_map, current_selections) = editor.selections.all_adjusted_display(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
@@ -173,7 +174,7 @@ impl Vim {
                     original_indent_columns.push(original_indent_column);
                 }
 
-                let cursor_offset = editor.selections.last::<usize>(cx).head();
+                let cursor_offset = editor.selections.last::<usize>(&display_map).head();
                 if editor
                     .buffer()
                     .read(cx)

crates/vim/src/normal/scroll.rs 🔗

@@ -363,7 +363,10 @@ mod test {
                 point(0., 3.0)
             );
             assert_eq!(
-                editor.selections.newest(cx).range(),
+                editor
+                    .selections
+                    .newest(&editor.display_snapshot(cx))
+                    .range(),
                 Point::new(6, 0)..Point::new(6, 0)
             )
         });
@@ -380,7 +383,10 @@ mod test {
                 point(0., 3.0)
             );
             assert_eq!(
-                editor.selections.newest(cx).range(),
+                editor
+                    .selections
+                    .newest(&editor.display_snapshot(cx))
+                    .range(),
                 Point::new(0, 0)..Point::new(6, 1)
             )
         });

crates/vim/src/normal/substitute.rs 🔗

@@ -94,7 +94,10 @@ impl Vim {
                     MotionKind::Exclusive
                 };
                 vim.copy_selections_content(editor, kind, window, cx);
-                let selections = editor.selections.all::<Point>(cx).into_iter();
+                let selections = editor
+                    .selections
+                    .all::<Point>(&editor.display_snapshot(cx))
+                    .into_iter();
                 let edits = selections.map(|selection| (selection.start..selection.end, ""));
                 editor.edit(edits, cx);
             });

crates/vim/src/normal/yank.rs 🔗

@@ -106,7 +106,7 @@ impl Vim {
             true,
             editor
                 .selections
-                .all_adjusted(cx)
+                .all_adjusted(&editor.display_snapshot(cx))
                 .iter()
                 .map(|s| s.range())
                 .collect(),
@@ -128,7 +128,7 @@ impl Vim {
             false,
             editor
                 .selections
-                .all_adjusted(cx)
+                .all_adjusted(&editor.display_snapshot(cx))
                 .iter()
                 .map(|s| s.range())
                 .collect(),

crates/vim/src/replace.rs 🔗

@@ -53,7 +53,7 @@ impl Vim {
             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::<Point>(cx);
+                let display_selections = editor.selections.all::<Point>(&map.display_snapshot);
 
                 // Handles all string that require manipulation, including inserts and replaces
                 let edits = display_selections
@@ -98,7 +98,7 @@ impl Vim {
             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::<Point>(cx);
+                let selections = editor.selections.all::<Point>(&map.display_snapshot);
                 let mut new_selections = vec![];
                 let edits: Vec<(Range<Point>, String)> = selections
                     .into_iter()
@@ -150,7 +150,9 @@ impl Vim {
         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(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
@@ -196,7 +198,9 @@ impl Vim {
         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(cx);
+            let mut selection = editor
+                .selections
+                .newest_display(&editor.display_snapshot(cx));
             let snapshot = editor.snapshot(window, cx);
             motion.expand_selection(
                 &snapshot,

crates/vim/src/state.rs 🔗

@@ -863,7 +863,9 @@ impl VimGlobals {
                 }
             }
             '%' => editor.and_then(|editor| {
-                let selection = editor.selections.newest::<Point>(cx);
+                let selection = editor
+                    .selections
+                    .newest::<Point>(&editor.display_snapshot(cx));
                 if let Some((_, buffer, _)) = editor
                     .buffer()
                     .read(cx)

crates/vim/src/surrounds.rs 🔗

@@ -45,7 +45,8 @@ impl Vim {
                     },
                 };
                 let surround = pair.end != surround_alias((*text).as_ref());
-                let (display_map, display_selections) = editor.selections.all_adjusted_display(cx);
+                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();
 
@@ -144,7 +145,8 @@ impl Vim {
             editor.transact(window, cx, |editor, window, cx| {
                 editor.set_clip_at_line_ends(false, cx);
 
-                let (display_map, display_selections) = editor.selections.all_display(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();
 
@@ -256,7 +258,8 @@ impl Vim {
                     let preserve_space =
                         will_replace_pair.start == will_replace_pair.end || !opening;
 
-                    let (display_map, selections) = editor.selections.all_adjusted_display(cx);
+                    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();
 
@@ -382,7 +385,8 @@ impl Vim {
             self.update_editor(cx, |_, editor, cx| {
                 editor.transact(window, cx, |editor, window, cx| {
                     editor.set_clip_at_line_ends(false, cx);
-                    let (display_map, selections) = editor.selections.all_adjusted_display(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 {
@@ -500,7 +504,8 @@ impl Vim {
                 let mut min_range_size = usize::MAX;
 
                 let _ = self.editor.update(cx, |editor, cx| {
-                    let (display_map, selections) = editor.selections.all_adjusted_display(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

crates/vim/src/test.rs 🔗

@@ -2295,7 +2295,10 @@ async fn test_clipping_on_mode_change(cx: &mut gpui::TestAppContext) {
 
     let mut pixel_position = cx.update_editor(|editor, window, cx| {
         let snapshot = editor.snapshot(window, cx);
-        let current_head = editor.selections.newest_display(cx).end;
+        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)

crates/vim/src/vim.rs 🔗

@@ -1359,7 +1359,10 @@ impl Vim {
             return;
         };
         let newest_selection_empty = editor.update(cx, |editor, cx| {
-            editor.selections.newest::<usize>(cx).is_empty()
+            editor
+                .selections
+                .newest::<usize>(&editor.display_snapshot(cx))
+                .is_empty()
         });
         let editor = editor.read(cx);
         let editor_mode = editor.mode();
@@ -1455,9 +1458,11 @@ impl Vim {
         cx: &mut Context<Self>,
     ) -> Option<String> {
         self.update_editor(cx, |_, editor, cx| {
-            let selection = editor.selections.newest::<usize>(cx);
+            let snapshot = &editor.snapshot(window, cx);
+            let selection = editor
+                .selections
+                .newest::<usize>(&snapshot.display_snapshot);
 
-            let snapshot = editor.snapshot(window, cx);
             let snapshot = snapshot.buffer_snapshot();
             let (range, kind) =
                 snapshot.surrounding_word(selection.start, Some(CharScopeContext::Completion));
@@ -1484,9 +1489,11 @@ impl Vim {
 
                 let selections = self.editor().map(|editor| {
                     editor.update(cx, |editor, cx| {
+                        let snapshot = editor.display_snapshot(cx);
+
                         (
-                            editor.selections.oldest::<Point>(cx),
-                            editor.selections.newest::<Point>(cx),
+                            editor.selections.oldest::<Point>(&snapshot),
+                            editor.selections.newest::<Point>(&snapshot),
                         )
                     })
                 });

crates/vim/src/visual.rs 🔗

@@ -366,6 +366,8 @@ impl Vim {
 
             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);
@@ -396,13 +398,18 @@ impl Vim {
 
                     selections.push(selection);
                 }
-                if row == head.row() {
+
+                // 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;
                 }
 
-                // Move to the next or previous buffer row, ensuring that
-                // wrapped lines are handled correctly.
-                let direction = if tail.row() > head.row() { -1 } else { 1 };
+                // 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();
@@ -747,7 +754,8 @@ impl Vim {
         self.stop_recording(cx);
         self.update_editor(cx, |_, editor, cx| {
             editor.transact(window, cx, |editor, window, cx| {
-                let (display_map, selections) = editor.selections.all_adjusted_display(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
@@ -858,7 +866,9 @@ impl Vim {
             });
         }
         self.update_editor(cx, |_, editor, cx| {
-            let latest = editor.selections.newest::<usize>(cx);
+            let latest = editor
+                .selections
+                .newest::<usize>(&editor.display_snapshot(cx));
             start_selection = latest.start;
             end_selection = latest.end;
         });
@@ -879,7 +889,9 @@ impl Vim {
             return;
         }
         self.update_editor(cx, |_, editor, cx| {
-            let latest = editor.selections.newest::<usize>(cx);
+            let latest = editor
+                .selections
+                .newest::<usize>(&editor.display_snapshot(cx));
             if vim_is_normal {
                 start_selection = latest.start;
                 end_selection = latest.end;

crates/vim_mode_setting/src/vim_mode_setting.rs 🔗

@@ -19,10 +19,6 @@ impl Settings for VimModeSetting {
     fn from_settings(content: &SettingsContent) -> Self {
         Self(content.vim_mode.unwrap())
     }
-
-    fn import_from_vscode(_vscode: &settings::VsCodeSettings, _content: &mut SettingsContent) {
-        // TODO: could possibly check if any of the `vim.<foo>` keys are set?
-    }
 }
 
 pub struct HelixModeSetting(pub bool);
@@ -31,6 +27,4 @@ impl Settings for HelixModeSetting {
     fn from_settings(content: &SettingsContent) -> Self {
         Self(content.helix_mode.unwrap())
     }
-
-    fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut SettingsContent) {}
 }

crates/watch/Cargo.toml 🔗

@@ -14,7 +14,6 @@ doctest = true
 
 [dependencies]
 parking_lot.workspace = true
-workspace-hack.workspace = true
 
 [dev-dependencies]
 ctor.workspace = true

crates/web_search/Cargo.toml 🔗

@@ -17,4 +17,3 @@ cloud_llm_client.workspace = true
 collections.workspace = true
 gpui.workspace = true
 serde.workspace = true
-workspace-hack.workspace = true

crates/workspace/Cargo.toml 🔗

@@ -63,7 +63,6 @@ ui.workspace = true
 util.workspace = true
 uuid.workspace = true
 zed_actions.workspace = true
-workspace-hack.workspace = true
 
 [target.'cfg(target_os = "windows")'.dependencies]
 windows.workspace = true

crates/workspace/src/dock.rs 🔗

@@ -27,6 +27,7 @@ pub use proto::PanelId;
 
 pub trait Panel: Focusable + EventEmitter<PanelEvent> + 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<Self>);
@@ -61,6 +62,7 @@ pub trait Panel: Focusable + EventEmitter<PanelEvent> + Render + Sized {
 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);
@@ -108,6 +110,10 @@ where
         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)
     }
@@ -942,8 +948,8 @@ impl Render for PanelButtons {
                                     }
                                 })
                                 .when(!is_active, |this| {
-                                    this.tooltip(move |window, cx| {
-                                        Tooltip::for_action(tooltip.clone(), &*action, window, cx)
+                                    this.tooltip(move |_window, cx| {
+                                        Tooltip::for_action(tooltip.clone(), &*action, cx)
                                     })
                                 })
                         }),
@@ -1016,6 +1022,10 @@ pub mod test {
             "TestPanel"
         }
 
+        fn panel_key() -> &'static str {
+            "TestPanel"
+        }
+
         fn position(&self, _window: &Window, _: &App) -> super::DockPosition {
             self.position
         }

crates/workspace/src/invalid_item_view.rs 🔗

@@ -0,0 +1,113 @@
+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.
+pub struct InvalidItemView {
+    /// Which path was attempted to open.
+    pub abs_path: Arc<Path>,
+    /// 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<Self>) -> 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)),
+                                ),
+                            )
+                        }),
+                ),
+            )
+    }
+}

crates/workspace/src/item.rs 🔗

@@ -1,7 +1,7 @@
 use crate::{
     CollaboratorId, DelayedDebouncedEditAction, FollowableViewRegistry, ItemNavHistory,
     SerializableItemRegistry, ToolbarItemLocation, ViewId, Workspace, WorkspaceId,
-    invalid_buffer_view::InvalidBufferView,
+    invalid_item_view::InvalidItemView,
     pane::{self, Pane},
     persistence::model::ItemId,
     searchable::SearchableItemHandle,
@@ -11,8 +11,9 @@ use anyhow::Result;
 use client::{Client, proto};
 use futures::{StreamExt, channel::mpsc};
 use gpui::{
-    Action, AnyElement, AnyView, App, Context, Entity, EntityId, EventEmitter, FocusHandle,
-    Focusable, Font, HighlightStyle, Pixels, Point, Render, SharedString, Task, WeakEntity, Window,
+    Action, AnyElement, 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::{
@@ -76,40 +77,6 @@ impl Settings for ItemSettings {
             show_close_button: tabs.show_close_button.unwrap(),
         }
     }
-
-    fn import_from_vscode(
-        vscode: &settings::VsCodeSettings,
-        current: &mut settings::SettingsContent,
-    ) {
-        if let Some(b) = vscode.read_bool("workbench.editor.tabActionCloseVisibility") {
-            current.tabs.get_or_insert_default().show_close_button = Some(if b {
-                ShowCloseButton::Always
-            } else {
-                ShowCloseButton::Hidden
-            })
-        }
-        if let Some(s) = vscode.read_enum("workbench.editor.tabActionLocation", |s| match s {
-            "right" => Some(ClosePosition::Right),
-            "left" => Some(ClosePosition::Left),
-            _ => None,
-        }) {
-            current.tabs.get_or_insert_default().close_position = Some(s)
-        }
-        if let Some(b) = vscode.read_bool("workbench.editor.focusRecentEditorAfterClose") {
-            current.tabs.get_or_insert_default().activate_on_close = Some(if b {
-                ActivateOnClose::History
-            } else {
-                ActivateOnClose::LeftNeighbour
-            })
-        }
-
-        if let Some(b) = vscode.read_bool("workbench.editor.showIcons") {
-            current.tabs.get_or_insert_default().file_icons = Some(b);
-        };
-        if let Some(b) = vscode.read_bool("git.decorations.enabled") {
-            current.tabs.get_or_insert_default().git_status = Some(b);
-        }
-    }
 }
 
 impl Settings for PreviewTabsSettings {
@@ -123,31 +90,6 @@ impl Settings for PreviewTabsSettings {
                 .unwrap(),
         }
     }
-
-    fn import_from_vscode(
-        vscode: &settings::VsCodeSettings,
-        current: &mut settings::SettingsContent,
-    ) {
-        if let Some(enabled) = vscode.read_bool("workbench.editor.enablePreview") {
-            current.preview_tabs.get_or_insert_default().enabled = Some(enabled);
-        }
-        if let Some(enable_preview_from_code_navigation) =
-            vscode.read_bool("workbench.editor.enablePreviewFromCodeNavigation")
-        {
-            current
-                .preview_tabs
-                .get_or_insert_default()
-                .enable_preview_from_code_navigation = Some(enable_preview_from_code_navigation)
-        }
-        if let Some(enable_preview_from_file_finder) =
-            vscode.read_bool("workbench.editor.enablePreviewFromQuickOpen")
-        {
-            current
-                .preview_tabs
-                .get_or_insert_default()
-                .enable_preview_from_file_finder = Some(enable_preview_from_file_finder)
-        }
-    }
 }
 
 #[derive(Clone, Copy, Eq, PartialEq, Hash, Debug)]
@@ -276,11 +218,11 @@ pub trait Item: Focusable + EventEmitter<Self::Event> + Render + Sized {
         _workspace_id: Option<WorkspaceId>,
         _window: &mut Window,
         _: &mut Context<Self>,
-    ) -> Option<Entity<Self>>
+    ) -> Task<Option<Entity<Self>>>
     where
         Self: Sized,
     {
-        None
+        Task::ready(None)
     }
     fn is_dirty(&self, _: &App) -> bool {
         false
@@ -481,7 +423,7 @@ pub trait ItemHandle: 'static + Send {
         workspace_id: Option<WorkspaceId>,
         window: &mut Window,
         cx: &mut App,
-    ) -> Option<Box<dyn ItemHandle>>;
+    ) -> Task<Option<Box<dyn ItemHandle>>>;
     fn added_to_pane(
         &self,
         workspace: &mut Workspace,
@@ -694,9 +636,12 @@ impl<T: Item> ItemHandle for Entity<T> {
         workspace_id: Option<WorkspaceId>,
         window: &mut Window,
         cx: &mut App,
-    ) -> Option<Box<dyn ItemHandle>> {
-        self.update(cx, |item, cx| item.clone_on_split(workspace_id, window, cx))
-            .map(|handle| Box::new(handle) as Box<dyn ItemHandle>)
+    ) -> Task<Option<Box<dyn ItemHandle>>> {
+        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<dyn ItemHandle>)
+        })
     }
 
     fn added_to_pane(
@@ -870,7 +815,7 @@ impl<T: Item> ItemHandle for Entity<T> {
                             let autosave = item.workspace_settings(cx).autosave;
 
                             if let AutosaveSetting::AfterDelay { milliseconds } = autosave {
-                                let delay = Duration::from_millis(milliseconds);
+                                let delay = Duration::from_millis(milliseconds.0);
                                 let item = item.clone();
                                 pending_autosave.fire_new(
                                     delay,
@@ -1117,7 +1062,7 @@ pub trait ProjectItem: Item {
         _e: &anyhow::Error,
         _window: &mut Window,
         _cx: &mut App,
-    ) -> Option<InvalidBufferView>
+    ) -> Option<InvalidItemView>
     where
         Self: Sized,
     {
@@ -1563,11 +1508,11 @@ pub mod test {
             _workspace_id: Option<WorkspaceId>,
             _: &mut Window,
             cx: &mut Context<Self>,
-        ) -> Option<Entity<Self>>
+        ) -> Task<Option<Entity<Self>>>
         where
             Self: Sized,
         {
-            Some(cx.new(|cx| Self {
+            Task::ready(Some(cx.new(|cx| Self {
                 state: self.state.clone(),
                 label: self.label.clone(),
                 save_count: self.save_count,
@@ -1584,7 +1529,7 @@ pub mod test {
                 workspace_id: self.workspace_id,
                 focus_handle: cx.focus_handle(),
                 serialize: None,
-            }))
+            })))
         }
 
         fn is_dirty(&self, _: &App) -> bool {

crates/workspace/src/notifications.rs 🔗

@@ -315,19 +315,17 @@ impl Render for LanguageServerPrompt {
                                     )
                                     .child(
                                         IconButton::new(close_id, close_icon)
-                                            .tooltip(move |window, cx| {
+                                            .tooltip(move |_window, cx| {
                                                 if suppress {
                                                     Tooltip::for_action(
                                                         "Suppress.\nClose with click.",
                                                         &SuppressNotification,
-                                                        window,
                                                         cx,
                                                     )
                                                 } else {
                                                     Tooltip::for_action(
                                                         "Close.\nSuppress with shift-click.",
                                                         &menu::Cancel,
-                                                        window,
                                                         cx,
                                                     )
                                                 }
@@ -499,7 +497,7 @@ impl NotificationFrame {
     }
 
     /// Determines whether the given notification ID should be suppressible
-    /// Suppressed motifications will not be shown anymore
+    /// Suppressed notifications will not be shown anymore
     pub fn show_suppress_button(mut self, show: bool) -> Self {
         self.show_suppress_button = show;
         self
@@ -556,23 +554,21 @@ impl RenderOnce for NotificationFrame {
                         this.on_modifiers_changed(move |_, _, cx| cx.notify(entity))
                             .child(
                                 IconButton::new(close_id, close_icon)
-                                    .tooltip(move |window, cx| {
+                                    .tooltip(move |_window, cx| {
                                         if suppress {
                                             Tooltip::for_action(
                                                 "Suppress.\nClose with click.",
                                                 &SuppressNotification,
-                                                window,
                                                 cx,
                                             )
                                         } else if show_suppress_button {
                                             Tooltip::for_action(
                                                 "Close.\nSuppress with shift-click.",
                                                 &menu::Cancel,
-                                                window,
                                                 cx,
                                             )
                                         } else {
-                                            Tooltip::for_action("Close", &menu::Cancel, window, cx)
+                                            Tooltip::for_action("Close", &menu::Cancel, cx)
                                         }
                                     })
                                     .on_click({
@@ -761,8 +757,8 @@ pub mod simple_message_notification {
             self
         }
 
-        /// Determines whether the given notification ID should be supressable
-        /// Suppressed motifications will not be shown anymor
+        /// 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

crates/workspace/src/pane.rs 🔗

@@ -2,7 +2,7 @@ use crate::{
     CloseWindow, NewFile, NewTerminal, OpenInTerminal, OpenOptions, OpenTerminal, OpenVisible,
     SplitDirection, ToggleFileFinder, ToggleProjectSymbols, ToggleZoom, Workspace,
     WorkspaceItemBuilder,
-    invalid_buffer_view::InvalidBufferView,
+    invalid_item_view::InvalidItemView,
     item::{
         ActivateOnClose, ClosePosition, Item, ItemBufferKind, ItemHandle, ItemSettings,
         PreviewTabsSettings, ProjectItemKind, SaveOptions, ShowCloseButton, ShowDiagnostics,
@@ -376,6 +376,7 @@ pub struct Pane {
     render_tab_bar: Rc<dyn Fn(&mut Pane, &mut Window, &mut Context<Pane>) -> AnyElement>,
     show_tab_bar_buttons: bool,
     max_tabs: Option<NonZeroUsize>,
+    use_max_tabs: bool,
     _subscriptions: Vec<Subscription>,
     tab_bar_scroll_handle: ScrollHandle,
     /// This is set to true if a user scroll has occurred more recently than a system scroll
@@ -473,10 +474,16 @@ impl Pane {
         next_timestamp: Arc<AtomicUsize>,
         can_drop_predicate: Option<Arc<dyn Fn(&dyn Any, &mut Window, &mut App) -> bool + 'static>>,
         double_click_dispatch_action: Box<dyn Action>,
+        use_max_tabs: bool,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) -> 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),
@@ -498,7 +505,8 @@ impl Pane {
             zoomed: false,
             active_item_index: 0,
             preview_item_id: None,
-            max_tabs: WorkspaceSettings::get_global(cx).max_tabs,
+            max_tabs,
+            use_max_tabs,
             last_focus_handle_by_item: Default::default(),
             nav_history: NavHistory(Arc::new(Mutex::new(NavHistoryState {
                 mode: NavigationMode::Normal,
@@ -706,7 +714,7 @@ impl Pane {
             self.preview_item_id = None;
         }
 
-        if new_max_tabs != self.max_tabs {
+        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);
         }
@@ -954,6 +962,11 @@ impl Pane {
             if allow_preview {
                 pane.set_preview_item_id(Some(new_item.item_id()), cx);
             }
+
+            if let Some(text) = new_item.telemetry_event_text(cx) {
+                telemetry::event!(text);
+            }
+
             pane.add_item_inner(
                 new_item,
                 true,
@@ -979,11 +992,11 @@ impl Pane {
 
             let new_item = build_item(self, window, cx);
             // A special case that won't ever get a `project_entry_id` but has to be deduplicated nonetheless.
-            if let Some(invalid_buffer_view) = new_item.downcast::<InvalidBufferView>() {
+            if let Some(invalid_buffer_view) = new_item.downcast::<InvalidItemView>() {
                 let mut already_open_view = None;
                 let mut views_to_close = HashSet::default();
                 for existing_error_view in self
-                    .items_of_type::<InvalidBufferView>()
+                    .items_of_type::<InvalidItemView>()
                     .filter(|item| item.read(cx).abs_path == invalid_buffer_view.read(cx).abs_path)
                 {
                     if already_open_view.is_none()
@@ -1170,6 +1183,10 @@ impl Pane {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
+        if let Some(text) = item.telemetry_event_text(cx) {
+            telemetry::event!(text);
+        }
+
         self.add_item_inner(
             item,
             activate_pane,
@@ -2713,12 +2730,11 @@ impl Pane {
                 .map(|this| {
                     if is_active {
                         let focus_handle = focus_handle.clone();
-                        this.tooltip(move |window, cx| {
+                        this.tooltip(move |_window, cx| {
                             Tooltip::for_action_in(
                                 end_slot_tooltip_text,
                                 end_slot_action,
                                 &focus_handle,
-                                window,
                                 cx,
                             )
                         })
@@ -3021,9 +3037,7 @@ impl Pane {
             .disabled(!self.can_navigate_backward())
             .tooltip({
                 let focus_handle = focus_handle.clone();
-                move |window, cx| {
-                    Tooltip::for_action_in("Go Back", &GoBack, &focus_handle, window, cx)
-                }
+                move |_window, cx| Tooltip::for_action_in("Go Back", &GoBack, &focus_handle, cx)
             });
 
         let navigate_forward = IconButton::new("navigate_forward", IconName::ArrowRight)
@@ -3039,8 +3053,8 @@ impl Pane {
             .disabled(!self.can_navigate_forward())
             .tooltip({
                 let focus_handle = focus_handle.clone();
-                move |window, cx| {
-                    Tooltip::for_action_in("Go Forward", &GoForward, &focus_handle, window, cx)
+                move |_window, cx| {
+                    Tooltip::for_action_in("Go Forward", &GoForward, &focus_handle, cx)
                 }
             });
 
@@ -3278,11 +3292,18 @@ impl Pane {
                         else {
                             return;
                         };
-                        if let Some(item) = item.clone_on_split(database_id, window, cx) {
-                            to_pane.update(cx, |pane, cx| {
-                                pane.add_item(item, true, true, None, window, 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);
                     }
@@ -3636,11 +3657,10 @@ fn default_render_tab_bar_buttons(
                 .on_click(cx.listener(|pane, _, window, cx| {
                     pane.toggle_zoom(&crate::ToggleZoom, window, cx);
                 }))
-                .tooltip(move |window, cx| {
+                .tooltip(move |_window, cx| {
                     Tooltip::for_action(
                         if zoomed { "Zoom Out" } else { "Zoom In" },
                         &ToggleZoom,
-                        window,
                         cx,
                     )
                 })

crates/workspace/src/pane_group.rs 🔗

@@ -1306,7 +1306,7 @@ mod element {
             let overlay_opacity = WorkspaceSettings::get(None, cx)
                 .active_pane_modifiers
                 .inactive_opacity
-                .map(|val| val.clamp(0.0, 1.0))
+                .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;

crates/workspace/src/persistence/model.rs 🔗

@@ -3,7 +3,7 @@ use crate::{
     Member, Pane, PaneAxis, SerializableItemRegistry, Workspace, WorkspaceId, item::ItemHandle,
     path_list::PathList,
 };
-use anyhow::Result;
+use anyhow::{Context, Result};
 use async_recursion::async_recursion;
 use collections::IndexSet;
 use db::sqlez::{
@@ -220,6 +220,7 @@ impl SerializedPaneGroup {
                 let new_items = serialized_pane
                     .deserialize_to(project, &pane, workspace_id, workspace.clone(), cx)
                     .await
+                    .context("Could not deserialize pane)")
                     .log_err()?;
 
                 if pane

crates/workspace/src/shared_screen.rs 🔗

@@ -6,7 +6,7 @@ use call::{RemoteVideoTrack, RemoteVideoTrackView, Room};
 use client::{User, proto::PeerId};
 use gpui::{
     AppContext as _, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement,
-    ParentElement, Render, SharedString, Styled, div,
+    ParentElement, Render, SharedString, Styled, Task, div,
 };
 use std::sync::Arc;
 use ui::{Icon, IconName, prelude::*};
@@ -114,14 +114,14 @@ impl Item for SharedScreen {
         _workspace_id: Option<WorkspaceId>,
         window: &mut Window,
         cx: &mut Context<Self>,
-    ) -> Option<Entity<Self>> {
-        Some(cx.new(|cx| Self {
+    ) -> Task<Option<Entity<Self>>> {
+        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)) {

crates/workspace/src/theme_preview.rs 🔗

@@ -1,5 +1,7 @@
 #![allow(unused, dead_code)]
-use gpui::{AnyElement, App, Entity, EventEmitter, FocusHandle, Focusable, Hsla, actions, hsla};
+use gpui::{
+    AnyElement, App, Entity, EventEmitter, FocusHandle, Focusable, Hsla, Task, actions, hsla,
+};
 use strum::IntoEnumIterator;
 use theme::all_theme_colors;
 use ui::{
@@ -100,11 +102,11 @@ impl Item for ThemePreview {
         _workspace_id: Option<crate::WorkspaceId>,
         window: &mut Window,
         cx: &mut Context<Self>,
-    ) -> Option<Entity<Self>>
+    ) -> Task<Option<Entity<Self>>>
     where
         Self: Sized,
     {
-        Some(cx.new(|cx| Self::new(window, cx)))
+        Task::ready(Some(cx.new(|cx| Self::new(window, cx))))
     }
 }
 
@@ -317,13 +319,7 @@ impl ThemePreview {
                                 .style(ButtonStyle::Transparent)
                                 .tooltip(move |window, cx| {
                                     let name = name.clone();
-                                    Tooltip::with_meta(
-                                        name,
-                                        None,
-                                        format!("{:?}", color),
-                                        window,
-                                        cx,
-                                    )
+                                    Tooltip::with_meta(name, None, format!("{:?}", color), cx)
                                 }),
                         )
                     })),

crates/workspace/src/workspace.rs 🔗

@@ -1,6 +1,6 @@
 pub mod dock;
 pub mod history_manager;
-pub mod invalid_buffer_view;
+pub mod invalid_item_view;
 pub mod item;
 mod modal_layer;
 pub mod notifications;
@@ -83,7 +83,7 @@ use remote::{
 use schemars::JsonSchema;
 use serde::Deserialize;
 use session::AppSession;
-use settings::{Settings, SettingsLocation, update_settings_file};
+use settings::{CenteredPaddingSettings, Settings, SettingsLocation, update_settings_file};
 use shared_screen::SharedScreen;
 use sqlez::{
     bindable::{Bind, Column, StaticColumnCount},
@@ -102,7 +102,10 @@ use std::{
     path::{Path, PathBuf},
     process::ExitStatus,
     rc::Rc,
-    sync::{Arc, LazyLock, Weak, atomic::AtomicUsize},
+    sync::{
+        Arc, LazyLock, Weak,
+        atomic::{AtomicBool, AtomicUsize},
+    },
     time::Duration,
 };
 use task::{DebugScenario, SpawnInTerminal, TaskContext};
@@ -302,6 +305,12 @@ pub struct MoveItemToPaneInDirection {
     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
 }
@@ -1190,9 +1199,6 @@ struct FollowerView {
 }
 
 impl Workspace {
-    const DEFAULT_PADDING: f32 = 0.2;
-    const MAX_PADDING: f32 = 0.4;
-
     pub fn new(
         workspace_id: Option<WorkspaceId>,
         project: Entity<Project>,
@@ -1325,6 +1331,7 @@ impl Workspace {
                 pane_history_timestamp.clone(),
                 None,
                 NewFile.boxed_clone(),
+                true,
                 window,
                 cx,
             );
@@ -1451,7 +1458,7 @@ impl Workspace {
             }),
             cx.on_release(move |this, cx| {
                 this.app_state.workspace_store.update(cx, move |store, _| {
-                    store.workspaces.remove(&window_handle.clone());
+                    store.workspaces.remove(&window_handle);
                 })
             }),
         ];
@@ -3229,6 +3236,7 @@ impl Workspace {
                 self.pane_history_timestamp.clone(),
                 None,
                 NewFile.boxed_clone(),
+                true,
                 window,
                 cx,
             );
@@ -3294,10 +3302,6 @@ impl Workspace {
         window: &mut Window,
         cx: &mut App,
     ) {
-        if let Some(text) = item.telemetry_event_text(cx) {
-            telemetry::event!(text);
-        }
-
         pane.update(cx, |pane, cx| {
             pane.add_item(
                 item,
@@ -3623,7 +3627,8 @@ impl Workspace {
         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);
+            self.split_and_clone(self.active_pane.clone(), SplitDirection::Right, window, cx)
+                .detach();
         }
     }
 
@@ -3990,7 +3995,8 @@ impl Workspace {
                 clone_active_item,
             } => {
                 if *clone_active_item {
-                    self.split_and_clone(pane.clone(), *direction, window, cx);
+                    self.split_and_clone(pane.clone(), *direction, window, cx)
+                        .detach();
                 } else {
                     self.split_and_move(pane.clone(), *direction, window, cx);
                 }
@@ -4131,21 +4137,27 @@ impl Workspace {
         direction: SplitDirection,
         window: &mut Window,
         cx: &mut Context<Self>,
-    ) -> Option<Entity<Pane>> {
-        let item = pane.read(cx).active_item()?;
-        let maybe_pane_handle =
-            if let Some(clone) = item.clone_on_split(self.database_id(), window, cx) {
-                let new_pane = self.add_pane(window, cx);
-                new_pane.update(cx, |pane, cx| {
-                    pane.add_item(clone, true, true, None, window, cx)
-                });
-                self.center.split(&pane, &new_pane, direction).unwrap();
-                cx.notify();
-                Some(new_pane)
+    ) -> Task<Option<Entity<Pane>>> {
+        let Some(item) = pane.read(cx).active_item() else {
+            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
-            };
-        maybe_pane_handle
+            }
+        })
     }
 
     pub fn join_all_panes(&mut self, window: &mut Window, cx: &mut Context<Self>) {
@@ -5923,8 +5935,11 @@ impl Workspace {
 
     fn adjust_padding(padding: Option<f32>) -> f32 {
         padding
-            .unwrap_or(Self::DEFAULT_PADDING)
-            .clamp(0.0, Self::MAX_PADDING)
+            .unwrap_or(CenteredPaddingSettings::default().0)
+            .clamp(
+                CenteredPaddingSettings::MIN_PADDING,
+                CenteredPaddingSettings::MAX_PADDING,
+            )
     }
 
     fn render_dock(
@@ -6354,6 +6369,10 @@ impl Render for DraggedDock {
 
 impl Render for Workspace {
     fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> 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());
@@ -6371,6 +6390,24 @@ impl Render for Workspace {
             }
         }
 
+        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();
@@ -6386,8 +6423,12 @@ impl Render for Workspace {
         let paddings = if centered_layout {
             let settings = WorkspaceSettings::get_global(cx).centered_layout;
             (
-                render_padding(Self::adjust_padding(settings.left_padding)),
-                render_padding(Self::adjust_padding(settings.right_padding)),
+                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)
@@ -6978,7 +7019,9 @@ actions!(
     zed,
     [
         /// Opens the Zed log file.
-        OpenLog
+        OpenLog,
+        /// Reveals the Zed log file in the system file manager.
+        RevealLogInFileManager
     ]
 );
 
@@ -8148,19 +8191,27 @@ pub fn clone_active_item(
     let Some(active_item) = source.read(cx).active_item() else {
         return;
     };
-    destination.update(cx, |target_pane, cx| {
-        let Some(clone) = active_item.clone_on_split(workspace_id, window, cx) else {
-            return;
-        };
-        target_pane.add_item(
-            clone,
-            focus_destination,
-            focus_destination,
-            Some(target_pane.items_len()),
-            window,
-            cx,
-        );
-    });
+    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)]
@@ -8667,25 +8718,24 @@ mod tests {
                 cx,
             );
 
-            let right_pane = workspace
-                .split_and_clone(left_pane.clone(), SplitDirection::Right, window, cx)
-                .unwrap();
+            let right_pane =
+                workspace.split_and_clone(left_pane.clone(), SplitDirection::Right, window, cx);
 
-            right_pane.update(cx, |pane, cx| {
-                pane.add_item(
-                    single_entry_items[1].boxed_clone(),
-                    true,
-                    true,
-                    None,
-                    window,
-                    cx,
-                );
-                pane.add_item(Box::new(item_3_4.clone()), true, true, None, 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| {
@@ -8803,8 +8853,9 @@ mod tests {
         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 });
+                    settings.workspace.autosave = Some(AutosaveSetting::AfterDelay {
+                        milliseconds: 500.into(),
+                    });
                 })
             });
             item.is_dirty = true;
@@ -10501,7 +10552,10 @@ mod tests {
                 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!(

crates/workspace/src/workspace_settings.rs 🔗

@@ -4,11 +4,11 @@ use crate::DockPosition;
 use collections::HashMap;
 use serde::Deserialize;
 pub use settings::AutosaveSetting;
-use settings::Settings;
 pub use settings::{
     BottomDockLayout, PaneSplitDirectionHorizontal, PaneSplitDirectionVertical,
     RestoreOnStartupBehavior,
 };
+use settings::{InactiveOpacity, Settings};
 
 pub struct WorkspaceSettings {
     pub active_pane_modifiers: ActivePanelModifiers,
@@ -50,7 +50,7 @@ pub struct ActivePanelModifiers {
     ///
     /// Default: `1.0`
     // TODO: make this not an option, it is never None
-    pub inactive_opacity: Option<f32>,
+    pub inactive_opacity: Option<InactiveOpacity>,
 }
 
 #[derive(Deserialize)]
@@ -108,91 +108,6 @@ impl Settings for WorkspaceSettings {
             zoomed_padding: workspace.zoomed_padding.unwrap(),
         }
     }
-
-    fn import_from_vscode(
-        vscode: &settings::VsCodeSettings,
-        current: &mut settings::SettingsContent,
-    ) {
-        if vscode
-            .read_bool("accessibility.dimUnfocused.enabled")
-            .unwrap_or_default()
-            && let Some(opacity) = vscode
-                .read_value("accessibility.dimUnfocused.opacity")
-                .and_then(|v| v.as_f64())
-        {
-            current
-                .workspace
-                .active_pane_modifiers
-                .get_or_insert_default()
-                .inactive_opacity = Some(opacity as f32);
-        }
-
-        vscode.enum_setting(
-            "window.confirmBeforeClose",
-            &mut current.workspace.confirm_quit,
-            |s| match s {
-                "always" | "keyboardOnly" => Some(true),
-                "never" => Some(false),
-                _ => None,
-            },
-        );
-
-        vscode.bool_setting(
-            "workbench.editor.restoreViewState",
-            &mut current.workspace.restore_on_file_reopen,
-        );
-
-        if let Some(b) = vscode.read_bool("window.closeWhenEmpty") {
-            current.workspace.when_closing_with_no_tabs = Some(if b {
-                settings::CloseWindowWhenNoItems::CloseWindow
-            } else {
-                settings::CloseWindowWhenNoItems::KeepWindowOpen
-            });
-        }
-
-        if let Some(b) = vscode.read_bool("files.simpleDialog.enable") {
-            current.workspace.use_system_path_prompts = Some(!b);
-        }
-
-        if let Some(v) = vscode.read_enum("files.autoSave", |s| match s {
-            "off" => Some(AutosaveSetting::Off),
-            "afterDelay" => Some(AutosaveSetting::AfterDelay {
-                milliseconds: vscode
-                    .read_value("files.autoSaveDelay")
-                    .and_then(|v| v.as_u64())
-                    .unwrap_or(1000),
-            }),
-            "onFocusChange" => Some(AutosaveSetting::OnFocusChange),
-            "onWindowChange" => Some(AutosaveSetting::OnWindowChange),
-            _ => None,
-        }) {
-            current.workspace.autosave = Some(v);
-        }
-
-        // 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
-        if let Some(n) = vscode
-            .read_value("workbench.editor.limit.value")
-            .and_then(|v| v.as_u64())
-            .and_then(|n| NonZeroUsize::new(n as usize))
-            && vscode
-                .read_bool("workbench.editor.limit.enabled")
-                .unwrap_or_default()
-        {
-            current.workspace.max_tabs = Some(n)
-        }
-
-        if let Some(b) = vscode.read_bool("window.nativeTabs") {
-            current.workspace.use_system_window_tabs = Some(b);
-        }
-
-        // some combination of "window.restoreWindows" and "workbench.startupEditor" might
-        // map to our "restore_on_startup"
-
-        // there doesn't seem to be a way to read whether the bottom dock's "justified"
-        // setting is enabled in vscode. that'd be our equivalent to "bottom_dock_layout"
-    }
 }
 
 impl Settings for TabBarSettings {
@@ -204,22 +119,6 @@ impl Settings for TabBarSettings {
             show_tab_bar_buttons: tab_bar.show_tab_bar_buttons.unwrap(),
         }
     }
-
-    fn import_from_vscode(
-        vscode: &settings::VsCodeSettings,
-        current: &mut settings::SettingsContent,
-    ) {
-        if let Some(b) = vscode.read_enum("workbench.editor.showTabs", |s| match s {
-            "multiple" => Some(true),
-            "single" | "none" => Some(false),
-            _ => None,
-        }) {
-            current.tab_bar.get_or_insert_default().show = Some(b);
-        }
-        if Some("hidden") == vscode.read_string("workbench.editor.editorActionsLocation") {
-            current.tab_bar.get_or_insert_default().show_tab_bar_buttons = Some(false)
-        }
-    }
 }
 
 #[derive(Deserialize)]
@@ -227,6 +126,7 @@ 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 {
@@ -236,15 +136,7 @@ impl Settings for StatusBarSettings {
             show: status_bar.show.unwrap(),
             active_language_button: status_bar.active_language_button.unwrap(),
             cursor_position_button: status_bar.cursor_position_button.unwrap(),
-        }
-    }
-
-    fn import_from_vscode(
-        vscode: &settings::VsCodeSettings,
-        current: &mut settings::SettingsContent,
-    ) {
-        if let Some(show) = vscode.read_bool("workbench.statusBar.visible") {
-            current.status_bar.get_or_insert_default().show = Some(show);
+            line_endings_button: status_bar.line_endings_button.unwrap(),
         }
     }
 }

crates/worktree/Cargo.toml 🔗

@@ -47,7 +47,6 @@ smol.workspace = true
 sum_tree.workspace = true
 text.workspace = true
 util.workspace = true
-workspace-hack.workspace = true
 
 [dev-dependencies]
 clock = { workspace = true, features = ["test-support"] }

crates/worktree/src/worktree.rs 🔗

@@ -236,7 +236,7 @@ pub struct LocalSnapshot {
     /// All of the git repositories in the worktree, indexed by the project entry
     /// id of their parent directory.
     git_repositories: TreeMap<ProjectEntryId, LocalRepositoryEntry>,
-    /// The file handle of the worktree root. `None` if the worktree is a directory.
+    /// The file handle of the worktree root
     /// (so we can find it after it's been moved)
     root_file_handle: Option<Arc<dyn fs::FileHandle>>,
     executor: BackgroundExecutor,
@@ -656,7 +656,7 @@ impl Worktree {
 
     pub fn replica_id(&self) -> ReplicaId {
         match self {
-            Worktree::Local(_) => 0,
+            Worktree::Local(_) => ReplicaId::LOCAL,
             Worktree::Remote(worktree) => worktree.replica_id,
         }
     }
@@ -1706,9 +1706,9 @@ impl LocalWorktree {
             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()
-                    .context("reading path after update")
+                this.entry_for_path(&path).cloned().with_context(|| {
+                    format!("Could not find entry in worktree for {path:?} after refresh")
+                })
             })??;
             Ok(Some(new_entry))
         })
@@ -3830,7 +3830,7 @@ impl BackgroundScanner {
                         .unbounded_send(ScanState::RootUpdated { new_path })
                         .ok();
                 } else {
-                    log::warn!("root path could not be canonicalized: {}", err);
+                    log::warn!("root path could not be canonicalized: {:#}", err);
                 }
                 return;
             }

crates/worktree/src/worktree_settings.rs 🔗

@@ -1,7 +1,7 @@
 use std::path::Path;
 
 use anyhow::Context as _;
-use settings::{Settings, SettingsContent};
+use settings::Settings;
 use util::{
     ResultExt,
     paths::{PathMatcher, PathStyle},
@@ -50,7 +50,7 @@ impl Settings for WorktreeSettings {
             .collect();
 
         Self {
-            project_name: worktree.project_name.filter(|p| !p.is_empty()),
+            project_name: worktree.project_name.into_inner(),
             file_scan_exclusions: path_matchers(file_scan_exclusions, "file_scan_exclusions")
                 .log_err()
                 .unwrap_or_default(),
@@ -64,31 +64,6 @@ impl Settings for WorktreeSettings {
                 .unwrap_or_default(),
         }
     }
-
-    fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut SettingsContent) {
-        if let Some(inclusions) = vscode
-            .read_value("files.watcherInclude")
-            .and_then(|v| v.as_array())
-            .and_then(|v| v.iter().map(|n| n.as_str().map(str::to_owned)).collect())
-        {
-            if let Some(old) = current.project.worktree.file_scan_inclusions.as_mut() {
-                old.extend(inclusions)
-            } else {
-                current.project.worktree.file_scan_inclusions = Some(inclusions)
-            }
-        }
-        if let Some(exclusions) = vscode
-            .read_value("files.watcherExclude")
-            .and_then(|v| v.as_array())
-            .and_then(|v| v.iter().map(|n| n.as_str().map(str::to_owned)).collect())
-        {
-            if let Some(old) = current.project.worktree.file_scan_exclusions.as_mut() {
-                old.extend(exclusions)
-            } else {
-                current.project.worktree.file_scan_exclusions = Some(exclusions)
-            }
-        }
-    }
 }
 
 fn path_matchers(mut values: Vec<String>, context: &'static str) -> anyhow::Result<PathMatcher> {

crates/worktree_benchmarks/Cargo.toml 🔗

@@ -9,7 +9,6 @@ fs.workspace = true
 gpui = { workspace = true, features = ["windows-manifest"] }
 settings.workspace = true
 worktree.workspace = true
-workspace-hack = { version = "0.1", path = "../../tooling/workspace-hack" }
 
 [lints]
 workspace = true

crates/x_ai/Cargo.toml 🔗

@@ -20,4 +20,3 @@ anyhow.workspace = true
 schemars = { workspace = true, optional = true }
 serde.workspace = true
 strum.workspace = true
-workspace-hack.workspace = true

crates/zed/Cargo.toml 🔗

@@ -2,7 +2,7 @@
 description = "The fast, collaborative code editor."
 edition.workspace = true
 name = "zed"
-version = "0.210.0"
+version = "0.211.0"
 publish.workspace = true
 license = "GPL-3.0-or-later"
 authors = ["Zed Team <hi@zed.dev>"]
@@ -21,13 +21,11 @@ path = "src/main.rs"
 [dependencies]
 acp_tools.workspace = true
 activity_indicator.workspace = true
-agent.workspace = true
 agent_settings.workspace = true
 agent_ui.workspace = true
 anyhow.workspace = true
 askpass.workspace = true
 assets.workspace = true
-assistant_tools.workspace = true
 audio.workspace = true
 auto_update.workspace = true
 auto_update_ui.workspace = true
@@ -160,7 +158,6 @@ vim_mode_setting.workspace = true
 watch.workspace = true
 web_search.workspace = true
 web_search_providers.workspace = true
-workspace-hack.workspace = true
 workspace.workspace = true
 zed_actions.workspace = true
 zed_env_vars.workspace = true

crates/zed/src/main.rs 🔗

@@ -582,7 +582,6 @@ pub fn main() {
             false,
             cx,
         );
-        assistant_tools::init(app_state.client.http_client(), cx);
         repl::init(app_state.fs.clone(), cx);
         recent_projects::init(cx);
 
@@ -848,6 +847,18 @@ fn handle_open_request(request: OpenRequest, app_state: Arc<AppState>, cx: &mut
                     .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]
+                workspace::with_active_or_new_workspace(cx, |_workspace, window, cx| {
+                    window.dispatch_action(
+                        Box::new(zed_actions::OpenSettingsAt { path: setting_path }),
+                        cx,
+                    );
+                });
+            }
         }
 
         return;

crates/zed/src/zed.rs 🔗

@@ -25,6 +25,7 @@ use feature_flags::{FeatureFlagAppExt, PanicFeatureFlag};
 use fs::Fs;
 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::{
@@ -177,6 +178,9 @@ pub fn init(cx: &mut App) {
             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(
@@ -416,6 +420,8 @@ pub fn initialize_workspace(
 
         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);
@@ -424,6 +430,7 @@ pub fn initialize_workspace(
             status_bar.add_right_item(edit_prediction_button, 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);
@@ -1049,6 +1056,8 @@ fn initialize_pane(
             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(|cx| CommitViewToolbar::new(workspace, cx));
+            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));
@@ -2830,14 +2839,16 @@ mod tests {
         });
 
         // Split the pane with the first entry, then open the second entry again.
-        window
+        let (task1, task2) = window
             .update(cx, |w, window, cx| {
-                w.split_and_clone(w.active_pane().clone(), SplitDirection::Right, window, cx);
-                w.open_path(file2.clone(), None, true, window, cx)
+                (
+                    w.split_and_clone(w.active_pane().clone(), SplitDirection::Right, window, cx),
+                    w.open_path(file2.clone(), None, true, window, cx),
+                )
             })
-            .unwrap()
-            .await
             .unwrap();
+        task1.await.unwrap();
+        task2.await.unwrap();
 
         window
             .read_with(cx, |w, cx| {
@@ -3460,7 +3471,13 @@ mod tests {
                     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,
@@ -4563,6 +4580,7 @@ mod tests {
                     | "workspace::ActivatePane"
                     | "workspace::MoveItemToPane"
                     | "workspace::MoveItemToPaneInDirection"
+                    | "workspace::NewFileSplit"
                     | "workspace::OpenTerminal"
                     | "workspace::SendKeystrokes"
                     | "agent::NewNativeAgentThreadFromSummary"
@@ -4665,7 +4683,7 @@ mod tests {
                 "keymap_editor",
                 "keystroke_input",
                 "language_selector",
-                "line_ending",
+                "line_ending_selector",
                 "lsp_tool",
                 "markdown",
                 "menu",

crates/zed/src/zed/app_menus.rs 🔗

@@ -2,7 +2,7 @@ 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};
+use zed_actions::{ToggleFocus as ToggleDebugPanel, agent::AddSelectionToThread, dev};
 
 pub fn app_menus(cx: &mut App) -> Vec<Menu> {
     use zed_actions::Quit;
@@ -214,6 +214,8 @@ pub fn app_menus(cx: &mut App) -> Vec<Menu> {
                 MenuItem::action("Move Line Up", editor::actions::MoveLineUp),
                 MenuItem::action("Move Line Down", editor::actions::MoveLineDown),
                 MenuItem::action("Duplicate Selection", editor::actions::DuplicateLineDown),
+                MenuItem::separator(),
+                MenuItem::action("Add to Agent Thread", AddSelectionToThread),
             ],
         },
         Menu {

crates/zed/src/zed/component_preview.rs 🔗

@@ -17,7 +17,7 @@ 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::SingleLineInput;
+use ui_input::InputField;
 use workspace::{
     AppState, Item, ItemId, SerializableItem, Workspace, WorkspaceId, delete_unloaded_items,
     item::ItemEvent,
@@ -99,7 +99,7 @@ struct ComponentPreview {
     component_map: HashMap<ComponentId, ComponentMetadata>,
     components: Vec<ComponentMetadata>,
     cursor_index: usize,
-    filter_editor: Entity<SingleLineInput>,
+    filter_editor: Entity<InputField>,
     filter_text: String,
     focus_handle: FocusHandle,
     language_registry: Arc<LanguageRegistry>,
@@ -126,8 +126,7 @@ impl ComponentPreview {
         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| SingleLineInput::new(window, cx, "Find components or usages…"));
+        let filter_editor = cx.new(|cx| InputField::new(window, cx, "Find components or usages…"));
 
         let component_list = ListState::new(
             sorted_components.len(),
@@ -721,7 +720,7 @@ impl Item for ComponentPreview {
         _workspace_id: Option<WorkspaceId>,
         window: &mut Window,
         cx: &mut Context<Self>,
-    ) -> Option<gpui::Entity<Self>>
+    ) -> Task<Option<gpui::Entity<Self>>>
     where
         Self: Sized,
     {
@@ -743,13 +742,13 @@ impl Item for ComponentPreview {
             cx,
         );
 
-        match self_result {
+        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)) {

crates/zed/src/zed/open_listener.rs 🔗

@@ -47,6 +47,7 @@ pub enum OpenRequestKind {
     AgentPanel,
     DockMenuAction { index: usize },
     BuiltinJsonSchema { schema_path: String },
+    Setting { setting_path: String },
 }
 
 impl OpenRequest {
@@ -93,6 +94,10 @@ impl OpenRequest {
                 this.kind = Some(OpenRequestKind::BuiltinJsonSchema {
                     schema_path: schema_path.to_string(),
                 });
+            } else if let Some(setting_path) = url.strip_prefix("zed://settings/") {
+                this.kind = Some(OpenRequestKind::Setting {
+                    setting_path: 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) {
@@ -328,6 +333,7 @@ pub async fn handle_cli_connection(
                 wait,
                 wsl,
                 open_new_workspace,
+                reuse,
                 env,
                 user_data_dir: _,
             } => {
@@ -363,6 +369,7 @@ pub async fn handle_cli_connection(
                     paths,
                     diff_paths,
                     open_new_workspace,
+                    reuse,
                     &responses,
                     wait,
                     app_state.clone(),
@@ -382,6 +389,7 @@ async fn open_workspaces(
     paths: Vec<String>,
     diff_paths: Vec<[String; 2]>,
     open_new_workspace: Option<bool>,
+    reuse: bool,
     responses: &IpcSender<CliResponse>,
     wait: bool,
     app_state: Arc<AppState>,
@@ -441,6 +449,7 @@ async fn open_workspaces(
                         workspace_paths,
                         diff_paths.clone(),
                         open_new_workspace,
+                        reuse,
                         wait,
                         responses,
                         env.as_ref(),
@@ -487,6 +496,7 @@ async fn open_local_workspace(
     workspace_paths: Vec<String>,
     diff_paths: Vec<[String; 2]>,
     open_new_workspace: Option<bool>,
+    reuse: bool,
     wait: bool,
     responses: &IpcSender<CliResponse>,
     env: Option<&HashMap<String, String>>,
@@ -497,12 +507,30 @@ async fn open_local_workspace(
 
     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,
+            open_new_workspace: effective_open_new_workspace,
+            replace_window,
             env: env.cloned(),
             ..Default::default()
         },
@@ -614,7 +642,9 @@ mod tests {
     };
     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;
@@ -780,6 +810,7 @@ mod tests {
                     vec![],
                     open_new_workspace,
                     false,
+                    false,
                     &response_tx,
                     None,
                     &app_state,
@@ -791,4 +822,102 @@ mod tests {
 
         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::<CliResponse>().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);
+    }
 }

crates/zed/src/zed/quick_action_bar.rs 🔗

@@ -655,8 +655,8 @@ impl RenderOnce for QuickActionBarButton {
             .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, window, cx)
+            .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))
     }

crates/zed/src/zed/quick_action_bar/preview.rs 🔗

@@ -68,7 +68,7 @@ impl QuickActionBar {
         let button = IconButton::new(button_id, IconName::Eye)
             .icon_size(IconSize::Small)
             .style(ButtonStyle::Subtle)
-            .tooltip(move |window, cx| {
+            .tooltip(move |_window, cx| {
                 Tooltip::with_meta(
                     tooltip_text,
                     Some(open_action_for_tooltip),
@@ -76,7 +76,6 @@ impl QuickActionBar {
                         "{} to open in a split",
                         text_for_keystroke(&alt_click.modifiers, &alt_click.key, cx)
                     ),
-                    window,
                     cx,
                 )
             })

crates/zed/src/zed/quick_action_bar/repl_menu.rs 🔗

@@ -54,7 +54,8 @@ impl QuickActionBar {
                     .count()
                     .ne(&0)
                     .then(|| {
-                        let latest = this.selections.newest_display(cx);
+                        let snapshot = this.display_snapshot(cx);
+                        let latest = this.selections.newest_display(&snapshot);
                         !latest.is_empty()
                     })
                     .unwrap_or_default()

crates/zed/src/zed/windows_only_instance.rs 🔗

@@ -158,6 +158,7 @@ fn send_args_to_instance(args: &Args) -> anyhow::Result<()> {
             wait: false,
             wsl: args.wsl.clone(),
             open_new_workspace: None,
+            reuse: false,
             env: None,
             user_data_dir: args.user_data_dir.clone(),
         }

crates/zed_actions/Cargo.toml 🔗

@@ -12,5 +12,4 @@ workspace = true
 gpui.workspace = true
 schemars.workspace = true
 serde.workspace = true
-workspace-hack.workspace = true
 uuid.workspace = true

crates/zed_actions/src/lib.rs 🔗

@@ -30,12 +30,12 @@ pub struct OpenZedUrl {
 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 the settings editor.
         /// Opens the default keymap file.
         OpenDefaultKeymap,
         /// Opens the user keymap file.
@@ -107,6 +107,16 @@ pub struct IncreaseBufferFontSize {
     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 OpenSettingsAt {
+    /// A path to a specific setting (e.g. `theme.mode`)
+    #[serde(default)]
+    pub path: String,
+}
+
 /// Resets the buffer font size to the default value.
 #[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)]
 #[action(namespace = zed)]
@@ -296,7 +306,10 @@ pub mod agent {
             #[action(deprecated_aliases = ["assistant::ToggleModelSelector", "assistant2::ToggleModelSelector"])]
             ToggleModelSelector,
             /// Triggers re-authentication on Gemini
-            ReauthenticateAgent
+            ReauthenticateAgent,
+            /// Add the current selection as context for threads in the agent panel.
+            #[action(deprecated_aliases = ["assistant::QuoteSelection", "agent::QuoteSelection"])]
+            AddSelectionToThread,
         ]
     );
 }

crates/zeta/Cargo.toml 🔗

@@ -55,7 +55,6 @@ thiserror.workspace = true
 ui.workspace = true
 util.workspace = true
 uuid.workspace = true
-workspace-hack.workspace = true
 workspace.workspace = true
 worktree.workspace = true
 zed_actions.workspace = true

crates/zeta/src/rate_completion_modal.rs 🔗

@@ -382,11 +382,7 @@ impl RateCompletionModal {
         )
     }
 
-    fn render_active_completion(
-        &mut self,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) -> Option<impl IntoElement> {
+    fn render_active_completion(&mut self, cx: &mut Context<Self>) -> Option<impl IntoElement> {
         let active_completion = self.active_completion.as_ref()?;
         let completion_id = active_completion.completion.id;
         let focus_handle = &self.focus_handle(cx);
@@ -500,7 +496,6 @@ impl RateCompletionModal {
                                         .key_binding(KeyBinding::for_action_in(
                                             &ThumbsDownActiveCompletion,
                                             focus_handle,
-                                            window,
                                             cx
                                         ))
                                         .on_click(cx.listener(move |this, _, window, cx| {
@@ -521,7 +516,6 @@ impl RateCompletionModal {
                                         .key_binding(KeyBinding::for_action_in(
                                             &ThumbsUpActiveCompletion,
                                             focus_handle,
-                                            window,
                                             cx
                                         ))
                                         .on_click(cx.listener(move |this, _, window, cx| {
@@ -658,7 +652,7 @@ impl Render for RateCompletionModal {
                             )
                     ),
             )
-            .children(self.render_active_completion(window, cx))
+            .children(self.render_active_completion( cx))
             .on_mouse_down_out(cx.listener(|_, _, _, cx| cx.emit(DismissEvent)))
     }
 }

crates/zeta/src/zeta.rs 🔗

@@ -1581,7 +1581,7 @@ fn guess_token_count(bytes: usize) -> usize {
 #[cfg(test)]
 mod tests {
     use client::test::FakeServer;
-    use clock::FakeSystemClock;
+    use clock::{FakeSystemClock, ReplicaId};
     use cloud_api_types::{CreateLlmTokenResponse, LlmToken};
     use gpui::TestAppContext;
     use http_client::FakeHttpClient;
@@ -1839,7 +1839,7 @@ mod tests {
         let buffer = cx.new(|_cx| {
             Buffer::remote(
                 language::BufferId::new(1).unwrap(),
-                1,
+                ReplicaId::new(1),
                 language::Capability::ReadWrite,
                 "fn main() {\n    println!(\"Hello\");\n}",
             )

crates/zeta2/Cargo.toml 🔗

@@ -34,7 +34,6 @@ serde_json.workspace = true
 thiserror.workspace = true
 util.workspace = true
 uuid.workspace = true
-workspace-hack.workspace = true
 workspace.workspace = true
 worktree.workspace = true
 

crates/zeta2/src/zeta2.rs 🔗

@@ -11,7 +11,7 @@ use edit_prediction_context::{
     DeclarationId, DeclarationStyle, EditPredictionContext, EditPredictionContextOptions,
     EditPredictionExcerptOptions, EditPredictionScoreOptions, SyntaxIndex, SyntaxIndexState,
 };
-use feature_flags::FeatureFlag;
+use feature_flags::{FeatureFlag, FeatureFlagAppExt as _};
 use futures::AsyncReadExt as _;
 use futures::channel::{mpsc, oneshot};
 use gpui::http_client::{AsyncBody, Method};
@@ -32,7 +32,6 @@ use std::sync::Arc;
 use std::time::{Duration, Instant};
 use thiserror::Error;
 use util::rel_path::RelPathBuf;
-use util::some_or_debug_panic;
 use workspace::notifications::{ErrorMessagePrompt, NotificationId, show_app_notification};
 
 mod prediction;
@@ -48,7 +47,7 @@ const MAX_EVENT_COUNT: usize = 16;
 
 pub const DEFAULT_CONTEXT_OPTIONS: EditPredictionContextOptions = EditPredictionContextOptions {
     use_imports: true,
-    use_references: false,
+    max_retrieved_declarations: 0,
     excerpt: EditPredictionExcerptOptions {
         max_bytes: 512,
         min_bytes: 128,
@@ -103,12 +102,12 @@ pub struct ZetaOptions {
 }
 
 pub struct PredictionDebugInfo {
-    pub context: EditPredictionContext,
+    pub request: predict_edits_v3::PredictEditsRequest,
     pub retrieval_time: TimeDelta,
     pub buffer: WeakEntity<Buffer>,
     pub position: language::Anchor,
     pub local_prompt: Result<String, String>,
-    pub response_rx: oneshot::Receiver<Result<RequestDebugInfo, String>>,
+    pub response_rx: oneshot::Receiver<Result<predict_edits_v3::PredictEditsResponse, String>>,
 }
 
 pub type RequestDebugInfo = predict_edits_v3::DebugInfo;
@@ -571,6 +570,9 @@ impl Zeta {
             if path.pop() { Some(path) } else { None }
         });
 
+        // TODO data collection
+        let can_collect_data = cx.is_staff();
+
         let request_task = cx.background_spawn({
             let snapshot = snapshot.clone();
             let buffer = buffer.clone();
@@ -606,25 +608,22 @@ impl Zeta {
                         options.max_diagnostic_bytes,
                     );
 
-                let debug_context = debug_tx.map(|tx| (tx, context.clone()));
-
                 let request = make_cloud_request(
                     excerpt_path,
                     context,
                     events,
-                    // TODO data collection
-                    false,
+                    can_collect_data,
                     diagnostic_groups,
                     diagnostic_groups_truncated,
                     None,
-                    debug_context.is_some(),
+                    debug_tx.is_some(),
                     &worktree_snapshots,
                     index_state.as_deref(),
                     Some(options.max_prompt_bytes),
                     options.prompt_format,
                 );
 
-                let debug_response_tx = if let Some((debug_tx, context)) = debug_context {
+                let debug_response_tx = if let Some(debug_tx) = &debug_tx {
                     let (response_tx, response_rx) = oneshot::channel();
 
                     let local_prompt = PlannedPrompt::populate(&request)
@@ -633,7 +632,7 @@ impl Zeta {
 
                     debug_tx
                         .unbounded_send(PredictionDebugInfo {
-                            context,
+                            request: request.clone(),
                             retrieval_time,
                             buffer: buffer.downgrade(),
                             local_prompt,
@@ -660,12 +659,12 @@ impl Zeta {
 
                 if let Some(debug_response_tx) = debug_response_tx {
                     debug_response_tx
-                        .send(response.as_ref().map_err(|err| err.to_string()).and_then(
-                            |response| match some_or_debug_panic(response.0.debug_info.clone()) {
-                                Some(debug_info) => Ok(debug_info),
-                                None => Err("Missing debug info".to_string()),
-                            },
-                        ))
+                        .send(
+                            response
+                                .as_ref()
+                                .map_err(|err| err.to_string())
+                                .map(|response| response.0.clone()),
+                        )
                         .ok();
                 }
 

crates/zeta2_tools/Cargo.toml 🔗

@@ -27,11 +27,11 @@ multi_buffer.workspace = true
 ordered-float.workspace = true
 project.workspace = true
 serde.workspace = true
+telemetry.workspace = true
 text.workspace = true
 ui.workspace = true
 ui_input.workspace = true
 util.workspace = true
-workspace-hack.workspace = true
 workspace.workspace = true
 zeta2.workspace = true
 

crates/zeta2_tools/src/zeta2_tools.rs 🔗

@@ -1,37 +1,38 @@
-use std::{
-    cmp::Reverse, collections::hash_map::Entry, path::PathBuf, str::FromStr, sync::Arc,
-    time::Duration,
-};
+use std::{cmp::Reverse, path::PathBuf, str::FromStr, sync::Arc, time::Duration};
 
 use chrono::TimeDelta;
 use client::{Client, UserStore};
-use cloud_llm_client::predict_edits_v3::{DeclarationScoreComponents, PromptFormat};
+use cloud_llm_client::predict_edits_v3::{
+    self, DeclarationScoreComponents, PredictEditsRequest, PredictEditsResponse, PromptFormat,
+};
 use collections::HashMap;
 use editor::{Editor, EditorEvent, EditorMode, ExcerptRange, MultiBuffer};
 use feature_flags::FeatureFlagAppExt as _;
-use futures::{StreamExt as _, channel::oneshot};
+use futures::{FutureExt, StreamExt as _, channel::oneshot, future::Shared};
 use gpui::{
-    CursorStyle, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity,
-    actions, prelude::*,
+    CursorStyle, Empty, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task,
+    WeakEntity, actions, prelude::*,
 };
 use language::{Buffer, DiskState};
 use ordered_float::OrderedFloat;
-use project::{Project, WorktreeId};
-use ui::{ContextMenu, ContextMenuEntry, DropdownMenu, prelude::*};
-use ui_input::SingleLineInput;
+use project::{Project, WorktreeId, telemetry_snapshot::TelemetrySnapshot};
+use ui::{ButtonLike, ContextMenu, ContextMenuEntry, DropdownMenu, KeyBinding, prelude::*};
+use ui_input::InputField;
 use util::{ResultExt, paths::PathStyle, rel_path::RelPath};
 use workspace::{Item, SplitDirection, Workspace};
 use zeta2::{PredictionDebugInfo, Zeta, Zeta2FeatureFlag, ZetaOptions};
 
-use edit_prediction_context::{
-    DeclarationStyle, EditPredictionContextOptions, EditPredictionExcerptOptions,
-};
+use edit_prediction_context::{EditPredictionContextOptions, EditPredictionExcerptOptions};
 
 actions!(
     dev,
     [
         /// Opens the language server protocol logs viewer.
-        OpenZeta2Inspector
+        OpenZeta2Inspector,
+        /// Rate prediction as positive.
+        Zeta2RatePredictionPositive,
+        /// Rate prediction as negative.
+        Zeta2RatePredictionNegative,
     ]
 );
 
@@ -64,10 +65,11 @@ pub struct Zeta2Inspector {
     focus_handle: FocusHandle,
     project: Entity<Project>,
     last_prediction: Option<LastPrediction>,
-    max_excerpt_bytes_input: Entity<SingleLineInput>,
-    min_excerpt_bytes_input: Entity<SingleLineInput>,
-    cursor_context_ratio_input: Entity<SingleLineInput>,
-    max_prompt_bytes_input: Entity<SingleLineInput>,
+    max_excerpt_bytes_input: Entity<InputField>,
+    min_excerpt_bytes_input: Entity<InputField>,
+    cursor_context_ratio_input: Entity<InputField>,
+    max_prompt_bytes_input: Entity<InputField>,
+    max_retrieved_declarations: Entity<InputField>,
     active_view: ActiveView,
     zeta: Entity<Zeta>,
     _active_editor_subscription: Option<Subscription>,
@@ -88,16 +90,24 @@ struct LastPrediction {
     buffer: WeakEntity<Buffer>,
     position: language::Anchor,
     state: LastPredictionState,
+    request: PredictEditsRequest,
+    project_snapshot: Shared<Task<Arc<TelemetrySnapshot>>>,
     _task: Option<Task<()>>,
 }
 
+#[derive(Clone, Copy, PartialEq)]
+enum Feedback {
+    Positive,
+    Negative,
+}
+
 enum LastPredictionState {
     Requested,
     Success {
-        inference_time: TimeDelta,
-        parsing_time: TimeDelta,
-        prompt_planning_time: TimeDelta,
         model_response_editor: Entity<Editor>,
+        feedback_editor: Entity<Editor>,
+        feedback: Option<Feedback>,
+        response: predict_edits_v3::PredictEditsResponse,
     },
     Failed {
         message: String,
@@ -128,11 +138,12 @@ impl Zeta2Inspector {
             focus_handle: cx.focus_handle(),
             project: project.clone(),
             last_prediction: None,
-            active_view: ActiveView::Context,
+            active_view: ActiveView::Inference,
             max_excerpt_bytes_input: Self::number_input("Max Excerpt Bytes", window, cx),
             min_excerpt_bytes_input: Self::number_input("Min Excerpt Bytes", window, cx),
             cursor_context_ratio_input: Self::number_input("Cursor Context Ratio", window, cx),
             max_prompt_bytes_input: Self::number_input("Max Prompt Bytes", window, cx),
+            max_retrieved_declarations: Self::number_input("Max Retrieved Definitions", window, cx),
             zeta: zeta.clone(),
             _active_editor_subscription: None,
             _update_state_task: Task::ready(()),
@@ -170,6 +181,13 @@ impl Zeta2Inspector {
         self.max_prompt_bytes_input.update(cx, |input, cx| {
             input.set_text(options.max_prompt_bytes.to_string(), window, cx);
         });
+        self.max_retrieved_declarations.update(cx, |input, cx| {
+            input.set_text(
+                options.context.max_retrieved_declarations.to_string(),
+                window,
+                cx,
+            );
+        });
         cx.notify();
     }
 
@@ -207,9 +225,9 @@ impl Zeta2Inspector {
         label: &'static str,
         window: &mut Window,
         cx: &mut Context<Self>,
-    ) -> Entity<SingleLineInput> {
+    ) -> Entity<InputField> {
         let input = cx.new(|cx| {
-            SingleLineInput::new(window, cx, "")
+            InputField::new(window, cx, "")
                 .label(label)
                 .label_min_width(px(64.))
         });
@@ -223,7 +241,7 @@ impl Zeta2Inspector {
                 };
 
                 fn number_input_value<T: FromStr + Default>(
-                    input: &Entity<SingleLineInput>,
+                    input: &Entity<InputField>,
                     cx: &App,
                 ) -> T {
                     input
@@ -246,6 +264,10 @@ impl Zeta2Inspector {
                             cx,
                         ),
                     },
+                    max_retrieved_declarations: number_input_value(
+                        &this.max_retrieved_declarations,
+                        cx,
+                    ),
                     ..zeta_options.context
                 };
 
@@ -287,17 +309,23 @@ impl Zeta2Inspector {
             let language_registry = self.project.read(cx).languages().clone();
             async move |this, cx| {
                 let mut languages = HashMap::default();
-                for lang_id in prediction
-                    .context
-                    .declarations
+                for ext in prediction
+                    .request
+                    .referenced_declarations
                     .iter()
-                    .map(|snippet| snippet.declaration.identifier().language_id)
-                    .chain(prediction.context.excerpt_text.language_id)
+                    .filter_map(|snippet| snippet.path.extension())
+                    .chain(prediction.request.excerpt_path.extension())
                 {
-                    if let Entry::Vacant(entry) = languages.entry(lang_id) {
+                    if !languages.contains_key(ext) {
                         // Most snippets are gonna be the same language,
                         // so we think it's fine to do this sequentially for now
-                        entry.insert(language_registry.language_for_id(lang_id).await.ok());
+                        languages.insert(
+                            ext.to_owned(),
+                            language_registry
+                                .language_for_name_or_extension(&ext.to_string_lossy())
+                                .await
+                                .ok(),
+                        );
                     }
                 }
 
@@ -320,13 +348,12 @@ impl Zeta2Inspector {
 
                             let excerpt_buffer = cx.new(|cx| {
                                 let mut buffer =
-                                    Buffer::local(prediction.context.excerpt_text.body, cx);
+                                    Buffer::local(prediction.request.excerpt.clone(), cx);
                                 if let Some(language) = prediction
-                                    .context
-                                    .excerpt_text
-                                    .language_id
-                                    .as_ref()
-                                    .and_then(|id| languages.get(id))
+                                    .request
+                                    .excerpt_path
+                                    .extension()
+                                    .and_then(|ext| languages.get(ext))
                                 {
                                     buffer.set_language(language.clone(), cx);
                                 }
@@ -340,25 +367,18 @@ impl Zeta2Inspector {
                                 cx,
                             );
 
-                            let mut declarations = prediction.context.declarations.clone();
+                            let mut declarations =
+                                prediction.request.referenced_declarations.clone();
                             declarations.sort_unstable_by_key(|declaration| {
-                                Reverse(OrderedFloat(
-                                    declaration.score(DeclarationStyle::Declaration),
-                                ))
+                                Reverse(OrderedFloat(declaration.declaration_score))
                             });
 
                             for snippet in &declarations {
-                                let path = this
-                                    .project
-                                    .read(cx)
-                                    .path_for_entry(snippet.declaration.project_entry_id(), cx);
-
                                 let snippet_file = Arc::new(ExcerptMetadataFile {
                                     title: RelPath::unix(&format!(
                                         "{} (Score: {})",
-                                        path.map(|p| p.path.display(path_style).to_string())
-                                            .unwrap_or_else(|| "".to_string()),
-                                        snippet.score(DeclarationStyle::Declaration)
+                                        snippet.path.display(),
+                                        snippet.declaration_score
                                     ))
                                     .unwrap()
                                     .into(),
@@ -367,11 +387,10 @@ impl Zeta2Inspector {
                                 });
 
                                 let excerpt_buffer = cx.new(|cx| {
-                                    let mut buffer =
-                                        Buffer::local(snippet.declaration.item_text().0, cx);
+                                    let mut buffer = Buffer::local(snippet.text.clone(), cx);
                                     buffer.file_updated(snippet_file, cx);
-                                    if let Some(language) =
-                                        languages.get(&snippet.declaration.identifier().language_id)
+                                    if let Some(ext) = snippet.path.extension()
+                                        && let Some(language) = languages.get(ext)
                                     {
                                         buffer.set_language(language.clone(), cx);
                                     }
@@ -386,7 +405,7 @@ impl Zeta2Inspector {
                                 let excerpt_id = excerpt_ids.first().unwrap();
 
                                 excerpt_score_components
-                                    .insert(*excerpt_id, snippet.components.clone());
+                                    .insert(*excerpt_id, snippet.score_components.clone());
                             }
 
                             multibuffer
@@ -418,25 +437,91 @@ impl Zeta2Inspector {
                                 if let Some(prediction) = this.last_prediction.as_mut() {
                                     prediction.state = match response {
                                         Ok(Ok(response)) => {
-                                            prediction.prompt_editor.update(
-                                                cx,
-                                                |prompt_editor, cx| {
-                                                    prompt_editor.set_text(
-                                                        response.prompt,
-                                                        window,
+                                            if let Some(debug_info) = &response.debug_info {
+                                                prediction.prompt_editor.update(
+                                                    cx,
+                                                    |prompt_editor, cx| {
+                                                        prompt_editor.set_text(
+                                                            debug_info.prompt.as_str(),
+                                                            window,
+                                                            cx,
+                                                        );
+                                                    },
+                                                );
+                                            }
+
+                                            let feedback_editor = cx.new(|cx| {
+                                                let buffer = cx.new(|cx| {
+                                                    let mut buffer = Buffer::local("", cx);
+                                                    buffer.set_language(
+                                                        markdown_language.clone(),
                                                         cx,
                                                     );
+                                                    buffer
+                                                });
+                                                let buffer =
+                                                    cx.new(|cx| MultiBuffer::singleton(buffer, cx));
+                                                let mut editor = Editor::new(
+                                                    EditorMode::AutoHeight {
+                                                        min_lines: 3,
+                                                        max_lines: None,
+                                                    },
+                                                    buffer,
+                                                    None,
+                                                    window,
+                                                    cx,
+                                                );
+                                                editor.set_placeholder_text(
+                                                    "Write feedback here",
+                                                    window,
+                                                    cx,
+                                                );
+                                                editor.set_show_line_numbers(false, cx);
+                                                editor.set_show_gutter(false, cx);
+                                                editor.set_show_scrollbars(false, cx);
+                                                editor
+                                            });
+
+                                            cx.subscribe_in(
+                                                &feedback_editor,
+                                                window,
+                                                |this, editor, ev, window, cx| match ev {
+                                                    EditorEvent::BufferEdited => {
+                                                        if let Some(last_prediction) =
+                                                            this.last_prediction.as_mut()
+                                                            && let LastPredictionState::Success {
+                                                                feedback: feedback_state,
+                                                                ..
+                                                            } = &mut last_prediction.state
+                                                        {
+                                                            if feedback_state.take().is_some() {
+                                                                editor.update(cx, |editor, cx| {
+                                                                    editor.set_placeholder_text(
+                                                                        "Write feedback here",
+                                                                        window,
+                                                                        cx,
+                                                                    );
+                                                                });
+                                                                cx.notify();
+                                                            }
+                                                        }
+                                                    }
+                                                    _ => {}
                                                 },
-                                            );
+                                            )
+                                            .detach();
 
                                             LastPredictionState::Success {
-                                                prompt_planning_time: response.prompt_planning_time,
-                                                inference_time: response.inference_time,
-                                                parsing_time: response.parsing_time,
                                                 model_response_editor: cx.new(|cx| {
                                                     let buffer = cx.new(|cx| {
                                                         let mut buffer = Buffer::local(
-                                                            response.model_response,
+                                                            response
+                                                                .debug_info
+                                                                .as_ref()
+                                                                .map(|p| p.model_response.as_str())
+                                                                .unwrap_or(
+                                                                    "(Debug info not available)",
+                                                                ),
                                                             cx,
                                                         );
                                                         buffer.set_language(markdown_language, cx);
@@ -458,6 +543,9 @@ impl Zeta2Inspector {
                                                     editor.set_show_scrollbars(false, cx);
                                                     editor
                                                 }),
+                                                feedback_editor,
+                                                feedback: None,
+                                                response,
                                             }
                                         }
                                         Ok(Err(err)) => {
@@ -473,6 +561,8 @@ impl Zeta2Inspector {
                         }
                     });
 
+                    let project_snapshot_task = TelemetrySnapshot::new(&this.project, cx);
+
                     this.last_prediction = Some(LastPrediction {
                         context_editor,
                         prompt_editor: cx.new(|cx| {
@@ -495,6 +585,11 @@ impl Zeta2Inspector {
                         buffer,
                         position,
                         state: LastPredictionState::Requested,
+                        project_snapshot: cx
+                            .foreground_executor()
+                            .spawn(async move { Arc::new(project_snapshot_task.await) })
+                            .shared(),
+                        request: prediction.request,
                         _task: Some(task),
                     });
                     cx.notify();
@@ -504,6 +599,103 @@ impl Zeta2Inspector {
         });
     }
 
+    fn handle_rate_positive(
+        &mut self,
+        _action: &Zeta2RatePredictionPositive,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        self.handle_rate(Feedback::Positive, window, cx);
+    }
+
+    fn handle_rate_negative(
+        &mut self,
+        _action: &Zeta2RatePredictionNegative,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        self.handle_rate(Feedback::Negative, window, cx);
+    }
+
+    fn handle_rate(&mut self, kind: Feedback, window: &mut Window, cx: &mut Context<Self>) {
+        let Some(last_prediction) = self.last_prediction.as_mut() else {
+            return;
+        };
+        if !last_prediction.request.can_collect_data {
+            return;
+        }
+
+        let project_snapshot_task = last_prediction.project_snapshot.clone();
+
+        cx.spawn_in(window, async move |this, cx| {
+            let project_snapshot = project_snapshot_task.await;
+            this.update_in(cx, |this, window, cx| {
+                let Some(last_prediction) = this.last_prediction.as_mut() else {
+                    return;
+                };
+
+                let LastPredictionState::Success {
+                    feedback: feedback_state,
+                    feedback_editor,
+                    model_response_editor,
+                    response,
+                    ..
+                } = &mut last_prediction.state
+                else {
+                    return;
+                };
+
+                *feedback_state = Some(kind);
+                let text = feedback_editor.update(cx, |feedback_editor, cx| {
+                    feedback_editor.set_placeholder_text(
+                        "Submitted. Edit or submit again to change.",
+                        window,
+                        cx,
+                    );
+                    feedback_editor.text(cx)
+                });
+                cx.notify();
+
+                cx.defer_in(window, {
+                    let model_response_editor = model_response_editor.downgrade();
+                    move |_, window, cx| {
+                        if let Some(model_response_editor) = model_response_editor.upgrade() {
+                            model_response_editor.focus_handle(cx).focus(window);
+                        }
+                    }
+                });
+
+                let kind = match kind {
+                    Feedback::Positive => "positive",
+                    Feedback::Negative => "negative",
+                };
+
+                telemetry::event!(
+                    "Zeta2 Prediction Rated",
+                    id = response.request_id,
+                    kind = kind,
+                    text = text,
+                    request = last_prediction.request,
+                    response = response,
+                    project_snapshot = project_snapshot,
+                );
+            })
+            .log_err();
+        })
+        .detach();
+    }
+
+    fn focus_feedback(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+        if let Some(last_prediction) = self.last_prediction.as_mut() {
+            if let LastPredictionState::Success {
+                feedback_editor, ..
+            } = &mut last_prediction.state
+            {
+                feedback_editor.focus_handle(cx).focus(window);
+            }
+        };
+    }
+
     fn render_options(&self, window: &mut Window, cx: &mut Context<Self>) -> Div {
         v_flex()
             .gap_2()
@@ -536,6 +728,7 @@ impl Zeta2Inspector {
                         h_flex()
                             .gap_2()
                             .items_end()
+                            .child(self.max_retrieved_declarations.clone())
                             .child(self.max_prompt_bytes_input.clone())
                             .child(self.render_prompt_format_dropdown(window, cx)),
                     ),
@@ -604,8 +797,9 @@ impl Zeta2Inspector {
                     ),
                     ui::ToggleButtonSimple::new(
                         "Inference",
-                        cx.listener(|this, _, _, cx| {
+                        cx.listener(|this, _, window, cx| {
                             this.active_view = ActiveView::Inference;
+                            this.focus_feedback(window, cx);
                             cx.notify();
                         }),
                     ),
@@ -626,21 +820,24 @@ impl Zeta2Inspector {
             return None;
         };
 
-        let (prompt_planning_time, inference_time, parsing_time) = match &prediction.state {
-            LastPredictionState::Success {
-                inference_time,
-                parsing_time,
-                prompt_planning_time,
+        let (prompt_planning_time, inference_time, parsing_time) =
+            if let LastPredictionState::Success {
+                response:
+                    PredictEditsResponse {
+                        debug_info: Some(debug_info),
+                        ..
+                    },
                 ..
-            } => (
-                Some(*prompt_planning_time),
-                Some(*inference_time),
-                Some(*parsing_time),
-            ),
-            LastPredictionState::Requested | LastPredictionState::Failed { .. } => {
+            } = &prediction.state
+            {
+                (
+                    Some(debug_info.prompt_planning_time),
+                    Some(debug_info.inference_time),
+                    Some(debug_info.parsing_time),
+                )
+            } else {
                 (None, None, None)
-            }
-        };
+            };
 
         Some(
             v_flex()
@@ -676,7 +873,7 @@ impl Zeta2Inspector {
             })
     }
 
-    fn render_content(&self, cx: &mut Context<Self>) -> AnyElement {
+    fn render_content(&self, _: &mut Window, cx: &mut Context<Self>) -> AnyElement {
         if !cx.has_flag::<Zeta2FeatureFlag>() {
             return Self::render_message("`zeta2` feature flag is not enabled");
         }
@@ -734,24 +931,105 @@ impl Zeta2Inspector {
                         .flex_1()
                         .gap_2()
                         .h_full()
-                        .p_4()
-                        .child(ui::Headline::new("Model Response").size(ui::HeadlineSize::XSmall))
-                        .child(match &prediction.state {
-                            LastPredictionState::Success {
-                                model_response_editor,
-                                ..
-                            } => model_response_editor.clone().into_any_element(),
-                            LastPredictionState::Requested => v_flex()
-                                .p_4()
+                        .child(
+                            v_flex()
+                                .flex_1()
                                 .gap_2()
-                                .child(Label::new("Loading...").buffer_font(cx))
-                                .into_any(),
-                            LastPredictionState::Failed { message } => v_flex()
                                 .p_4()
-                                .gap_2()
-                                .child(Label::new(message.clone()).buffer_font(cx))
-                                .into_any(),
-                        }),
+                                .child(
+                                    ui::Headline::new("Model Response")
+                                        .size(ui::HeadlineSize::XSmall),
+                                )
+                                .child(match &prediction.state {
+                                    LastPredictionState::Success {
+                                        model_response_editor,
+                                        ..
+                                    } => model_response_editor.clone().into_any_element(),
+                                    LastPredictionState::Requested => v_flex()
+                                        .gap_2()
+                                        .child(Label::new("Loading...").buffer_font(cx))
+                                        .into_any_element(),
+                                    LastPredictionState::Failed { message } => v_flex()
+                                        .gap_2()
+                                        .max_w_96()
+                                        .child(Label::new(message.clone()).buffer_font(cx))
+                                        .into_any_element(),
+                                }),
+                        )
+                        .child(ui::divider())
+                        .child(
+                            if prediction.request.can_collect_data
+                                && let LastPredictionState::Success {
+                                    feedback_editor,
+                                    feedback: feedback_state,
+                                    ..
+                                } = &prediction.state
+                            {
+                                v_flex()
+                                    .key_context("Zeta2Feedback")
+                                    .on_action(cx.listener(Self::handle_rate_positive))
+                                    .on_action(cx.listener(Self::handle_rate_negative))
+                                    .gap_2()
+                                    .p_2()
+                                    .child(feedback_editor.clone())
+                                    .child(
+                                        h_flex()
+                                            .justify_end()
+                                            .w_full()
+                                            .child(
+                                                ButtonLike::new("rate-positive")
+                                                    .when(
+                                                        *feedback_state == Some(Feedback::Positive),
+                                                        |this| this.style(ButtonStyle::Filled),
+                                                    )
+                                                    .child(
+                                                        KeyBinding::for_action(
+                                                            &Zeta2RatePredictionPositive,
+                                                            cx,
+                                                        )
+                                                        .size(TextSize::Small.rems(cx)),
+                                                    )
+                                                    .child(ui::Icon::new(ui::IconName::ThumbsUp))
+                                                    .on_click(cx.listener(
+                                                        |this, _, window, cx| {
+                                                            this.handle_rate_positive(
+                                                                &Zeta2RatePredictionPositive,
+                                                                window,
+                                                                cx,
+                                                            );
+                                                        },
+                                                    )),
+                                            )
+                                            .child(
+                                                ButtonLike::new("rate-negative")
+                                                    .when(
+                                                        *feedback_state == Some(Feedback::Negative),
+                                                        |this| this.style(ButtonStyle::Filled),
+                                                    )
+                                                    .child(
+                                                        KeyBinding::for_action(
+                                                            &Zeta2RatePredictionNegative,
+                                                            cx,
+                                                        )
+                                                        .size(TextSize::Small.rems(cx)),
+                                                    )
+                                                    .child(ui::Icon::new(ui::IconName::ThumbsDown))
+                                                    .on_click(cx.listener(
+                                                        |this, _, window, cx| {
+                                                            this.handle_rate_negative(
+                                                                &Zeta2RatePredictionNegative,
+                                                                window,
+                                                                cx,
+                                                            );
+                                                        },
+                                                    )),
+                                            ),
+                                    )
+                                    .into_any()
+                            } else {
+                                Empty.into_any_element()
+                            },
+                        ),
                 ),
         }
     }
@@ -794,7 +1072,7 @@ impl Render for Zeta2Inspector {
                     .child(ui::vertical_divider())
                     .children(self.render_stats()),
             )
-            .child(self.render_content(cx))
+            .child(self.render_content(window, cx))
     }
 }
 

crates/zeta_cli/Cargo.toml 🔗

@@ -50,7 +50,6 @@ soa-rs = "0.8.1"
 terminal_view.workspace = true
 util.workspace = true
 watch.workspace = true
-workspace-hack.workspace = true
 zeta.workspace = true
 zeta2.workspace = true
 zlog.workspace = true

crates/zeta_cli/src/main.rs 🔗

@@ -94,8 +94,8 @@ struct Zeta2Args {
     file_indexing_parallelism: usize,
     #[arg(long, default_value_t = false)]
     disable_imports_gathering: bool,
-    #[arg(long, default_value_t = false)]
-    disable_reference_retrieval: bool,
+    #[arg(long, default_value_t = u8::MAX)]
+    max_retrieved_definitions: u8,
 }
 
 #[derive(clap::ValueEnum, Default, Debug, Clone)]
@@ -302,7 +302,7 @@ impl Zeta2Args {
     fn to_options(&self, omit_excerpt_overlaps: bool) -> zeta2::ZetaOptions {
         zeta2::ZetaOptions {
             context: EditPredictionContextOptions {
-                use_references: !self.disable_reference_retrieval,
+                max_retrieved_declarations: self.max_retrieved_definitions,
                 use_imports: !self.disable_imports_gathering,
                 excerpt: EditPredictionExcerptOptions {
                     max_bytes: self.max_excerpt_bytes,

crates/zlog/Cargo.toml 🔗

@@ -18,7 +18,6 @@ default = []
 collections.workspace = true
 chrono.workspace = true
 log.workspace = true
-workspace-hack.workspace = true
 anyhow.workspace = true
 
 [dev-dependencies]

crates/zlog_settings/Cargo.toml 🔗

@@ -19,4 +19,3 @@ gpui.workspace = true
 collections.workspace = true
 settings.workspace = true
 zlog.workspace = true
-workspace-hack.workspace = true

crates/zlog_settings/src/zlog_settings.rs 🔗

@@ -29,6 +29,4 @@ impl Settings for ZlogSettings {
             scopes: content.log.clone().unwrap(),
         }
     }
-
-    fn import_from_vscode(_: &settings::VsCodeSettings, _: &mut settings::SettingsContent) {}
 }

docs/src/SUMMARY.md 🔗

@@ -8,7 +8,7 @@
 - [Linux](./linux.md)
 - [Windows](./windows.md)
 - [Telemetry](./telemetry.md)
-- [Workspace Persistence](./workspace-persistence.md)
+- [Troubleshooting](./troubleshooting.md)
 - [Additional Learning Materials](./additional-learning-materials.md)
 
 # Configuration
@@ -160,4 +160,5 @@
   - [Using Debuggers](./development/debuggers.md)
   - [Glossary](./development/glossary.md)
 - [Release Process](./development/releases.md)
+- [Release Notes](./development/release-notes.md)
 - [Debugging Crashes](./development/debugging-crashes.md)

docs/src/additional-learning-materials.md 🔗

@@ -1,3 +1,4 @@
 # Additional Learning Materials
 
 - [Text Manipulation Kung Fu for the Aspiring Black Belt](https://zed.dev/blog/text-manipulation)
+- [Hidden Gems: Team Edition Part 1](https://zed.dev/blog/hidden-gems-team-edition-part-1)

docs/src/ai/edit-prediction.md 🔗

@@ -1,7 +1,11 @@
 # Edit Prediction
 
-Edit Prediction is Zed's native mechanism for predicting the code you want to write through AI.
-Each keystroke sends a new request to our [open source, open dataset Zeta model](https://huggingface.co/zed-industries/zeta) and it returns with individual or multi-line suggestions that can be quickly accepted by pressing `tab`.
+Edit Prediction is Zed's mechanism for predicting the code you want to write through AI.
+Each keystroke sends a new request to the edit prediction provider, which returns individual or multi-line suggestions that can be quickly accepted by pressing `tab`.
+
+The default provider is [Zeta, a proprietary open source and open dataset model](https://huggingface.co/zed-industries/zeta), which [requires being signed into Zed](../accounts.md#what-features-require-signing-in).
+
+Alternatively, you can use other providers like [GitHub Copilot](#github-copilot) (or [Enterprise](#github-copilot-enterprise)) or [Supermaven](#supermaven).
 
 ## Configuring Zeta
 

docs/src/ai/external-agents.md 🔗

@@ -3,9 +3,10 @@
 Zed supports terminal-based agents through the [Agent Client Protocol (ACP)](https://agentclientprotocol.com).
 
 Currently, [Gemini CLI](https://github.com/google-gemini/gemini-cli) serves as the reference implementation.
-[Claude Code](https://www.anthropic.com/claude-code) is also included by default, and you can [add custom ACP-compatible agents](#add-custom-agents) as well.
+[Claude Code](https://www.anthropic.com/claude-code) and [Codex](https://developers.openai.com/codex) are also included by default, and you can [add custom ACP-compatible agents](#add-custom-agents) as well.
 
-Zed's affordance for external agents is strictly UI-based; the billing and legal/terms arrangement is directly between you and the agent provider. Zed does not charge for use of external agents, and our [zero-data retention agreements/privacy guarantees](./ai-improvement.md) are **_only_** applicable for Zed's hosted models.
+> Note that Zed's affordance for external agents is strictly UI-based; the billing and legal/terms arrangement is directly between you and the agent provider.
+> Zed does not charge for use of external agents, and our [zero-data retention agreements/privacy guarantees](./ai-improvement.md) are **_only_** applicable for Zed's hosted models.
 
 ## Gemini CLI {#gemini-cli}
 
@@ -206,3 +207,10 @@ You can also specify a custom path, arguments, or environment for the builtin in
 When using external agents in Zed, you can access the debug view via with `dev: open acp logs` from the Command Palette. This lets you see the messages being sent and received between Zed and the agent.
 
 ![The debug view for ACP logs.](https://zed.dev/img/acp/acp-logs.webp)
+
+## MCP Servers
+
+Note that for external agents, access to MCP servers [installed from Zed](./mcp.md) may vary depending on the ACP agent implementation.
+
+Regarding the built-in ones, Claude Code and Codex both support it, and Gemini CLI does not yet.
+In the meantime, learn how to add MCP server support to Gemini CLI through [their documentation](https://github.com/google-gemini/gemini-cli?tab=readme-ov-file#using-mcp-servers).

docs/src/ai/mcp.md 🔗

@@ -11,7 +11,7 @@ Check out the [Anthropic news post](https://www.anthropic.com/news/model-context
 ### As Extensions
 
 One of the ways you can use MCP servers in Zed is by exposing them as an extension.
-To learn how to create your own, check out the [MCP Server Extensions](../extensions/mcp-extensions.md) page for more details.
+Check out the [MCP Server Extensions](../extensions/mcp-extensions.md) page to learn how to create your own.
 
 Thanks to our awesome community, many MCP servers have already been added as extensions.
 You can check which ones are available via any of these routes:
@@ -20,7 +20,7 @@ You can check which ones are available via any of these routes:
 2. in the app, open the Command Palette and run the `zed: extensions` action
 3. in the app, go to the Agent Panel's top-right menu and look for the "View Server Extensions" menu item
 
-In any case, here are some of the ones available:
+In any case, here are some popular available servers:
 
 - [Context7](https://zed.dev/extensions/context7-mcp-server)
 - [GitHub](https://zed.dev/extensions/github-mcp-server)
@@ -57,9 +57,9 @@ From there, you can add it through the modal that appears when you click the "Ad
 
 ### Configuration Check
 
-Regardless of how you've installed MCP servers, whether as an extension or adding them directly, most servers out there still require some sort of configuration as part of the set up process.
+Regardless of how you've installed MCP servers, whether as an extension or adding them directly, most servers out there still require some sort of configuration as part of the setup process.
 
-In the case of server extensions, after installing it, Zed will pop up a modal displaying what is required for you to properly set it up.
+In the case of extensions, after installing it, Zed will pop up a modal displaying what is required for you to properly set it up.
 For example, the GitHub MCP extension requires you to add a [Personal Access Token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens).
 
 In the case of custom servers, make sure you check the provider documentation to determine what type of command, arguments, and environment variables need to be added to the JSON.
@@ -68,14 +68,14 @@ To check if your MCP server is properly configured, go to the Agent Panel's sett
 If they're running correctly, the indicator will be green and its tooltip will say "Server is active".
 If not, other colors and tooltip messages will indicate what is happening.
 
-### Using it in the Agent Panel
+### Agent Panel Usage
 
 Once installation is complete, you can return to the Agent Panel and start prompting.
 
 Some models are better than others when it comes to picking up tools from MCP servers.
 Mentioning your server by name always helps the model to pick it up.
 
-However, if you want to ensure a given MCP server will be used, you can create [a custom profile](./agent-panel.md#custom-profiles) where all built-in tools (or the ones that could cause conflicts with the server's tools) are turned off and only the tools coming from the MCP server are turned on.
+However, if you want to _ensure_ a given MCP server will be used, you can create [a custom profile](./agent-panel.md#custom-profiles) where all built-in tools (or the ones that could cause conflicts with the server's tools) are turned off and only the tools coming from the MCP server are turned on.
 
 As an example, [the Dagger team suggests](https://container-use.com/agent-integrations#zed) doing that with their [Container Use MCP server](https://zed.dev/extensions/mcp-server-container-use):
 
@@ -127,3 +127,10 @@ As an example, [the Dagger team suggests](https://container-use.com/agent-integr
 Zed's Agent Panel includes the `agent.always_allow_tool_actions` setting that, if set to `false`, will require you to give permission for any editing attempt as well as tool calls coming from MCP servers.
 
 You can change this by setting this key to `true` in either your `settings.json` or through the Agent Panel's settings view.
+
+### External Agents
+
+Note that for [external agents](./external-agents.md) connected through the [Agent Client Protocol](https://agentclientprotocol.com/), access to MCP servers installed from Zed may vary depending on the ACP agent implementation.
+
+Regarding the built-in ones, Claude Code and Codex both support it, and Gemini CLI does not yet.
+In the meantime, learn how to add MCP server support to Gemini CLI through [their documentation](https://github.com/google-gemini/gemini-cli?tab=readme-ov-file#using-mcp-servers).

docs/src/ai/models.md 🔗

@@ -21,6 +21,10 @@ We’re working hard to expand the models supported by Zed’s subscription offe
 |                        | Anthropic | Output              | $15.00                       | $16.50                  |
 |                        | Anthropic | Input - Cache Write | $3.75                        | $4.125                  |
 |                        | Anthropic | Input - Cache Read  | $0.30                        | $0.33                   |
+| Claude Haiku 4.5       | Anthropic | Input               | $1.00                        | $1.10                   |
+|                        | Anthropic | Output              | $5.00                        | $5.50                   |
+|                        | Anthropic | Input - Cache Write | $1.25                        | $1.375                  |
+|                        | Anthropic | Input - Cache Read  | $0.10                        | $0.11                   |
 | GPT-5                  | OpenAI    | Input               | $1.25                        | $1.375                  |
 |                        | OpenAI    | Output              | $10.00                       | $11.00                  |
 |                        | OpenAI    | Cached Input        | $0.125                       | $0.1375                 |
@@ -62,6 +66,7 @@ A context window is the maximum span of text and code an LLM can consider at onc
 | Claude Opus 4.1   | Anthropic | 200k                      |
 | Claude Sonnet 4   | Anthropic | 200k                      |
 | Claude Sonnet 3.7 | Anthropic | 200k                      |
+| Claude Haiku 4.5  | Anthropic | 200k                      |
 | GPT-5             | OpenAI    | 400k                      |
 | GPT-5 mini        | OpenAI    | 400k                      |
 | GPT-5 nano        | OpenAI    | 400k                      |

docs/src/ai/overview.md 🔗

@@ -18,11 +18,11 @@ Learn how to get started using AI with Zed and all its capabilities.
 
 - [Rules](./rules.md): How to define rules for AI interactions.
 
-- [Tools](./tools.md): Explore the tools that enable agentic capabilities.
+- [Tools](./tools.md): Explore the tools that power Zed's built-in agent.
 
-- [Model Context Protocol](./mcp.md): Learn about how to install and configure MCP servers.
+- [Model Context Protocol](./mcp.md): Learn about how to configure and use MCP servers.
 
-- [Inline Assistant](./inline-assistant.md): Discover how to use the agent to power inline transformations directly within a file or terminal.
+- [Inline Assistant](./inline-assistant.md): Discover how to use AI to generate inline transformations directly within a file or terminal.
 
 ## Edit Prediction
 
@@ -30,4 +30,4 @@ Learn how to get started using AI with Zed and all its capabilities.
 
 ## Text Threads
 
-- [Text Threads](./text-threads.md): Learn about an alternative, text-based interface for interacting with language models.
+- [Text Threads](./text-threads.md): Learn about an editor-based interface for interacting with language models.

docs/src/ai/text-threads.md 🔗

@@ -16,7 +16,7 @@ To begin, type a message in a `You` block.
 
 As you type, the remaining tokens count for the selected model is updated.
 
-Inserting text from an editor is as simple as highlighting the text and running `agent: quote selection` ({#kb agent::QuoteSelection}); Zed will wrap it in a fenced code block if it is code.
+Inserting text from an editor is as simple as highlighting the text and running `agent: add selection to thread` ({#kb agent::AddSelectionToThread}); Zed will wrap it in a fenced code block if it is code.
 
 ![Quoting a selection](https://zed.dev/img/assistant/quoting-a-selection.png)
 
@@ -148,7 +148,7 @@ Usage: `/terminal [<number>]`
 
 The `/selection` command inserts the selected text in the editor into the context. This is useful for referencing specific parts of your code.
 
-This is equivalent to the `agent: quote selection` command ({#kb agent::QuoteSelection}).
+This is equivalent to the `agent: add selection to thread` command ({#kb agent::AddSelectionToThread}).
 
 Usage: `/selection`
 

docs/src/configuring-languages.md 🔗

@@ -364,18 +364,20 @@ Zed offers customization options for syntax highlighting and themes, allowing yo
 
 ### Customizing Syntax Highlighting
 
-Zed uses Tree-sitter grammars for syntax highlighting. Override the default highlighting using the `experimental.theme_overrides` setting.
+Zed uses Tree-sitter grammars for syntax highlighting. Override the default highlighting using the `theme_overrides` setting.
 
 This example makes comments italic and changes the color of strings:
 
 ```json [settings]
-"experimental.theme_overrides": {
-  "syntax": {
-    "comment": {
-      "font_style": "italic"
-    },
-    "string": {
-      "color": "#00AA00"
+"theme_overrides": {
+  "One Dark": {
+    "syntax": {
+      "comment": {
+        "font_style": "italic"
+      },
+      "string": {
+        "color": "#00AA00"
+      }
     }
   }
 }

docs/src/configuring-zed.md 🔗

@@ -1498,7 +1498,8 @@ Positive `integer` value between 1 and 32. Values outside of this range will be
 ```json [settings]
 "status_bar": {
   "active_language_button": true,
-  "cursor_position_button": true
+  "cursor_position_button": true,
+  "line_endings_button": false
 },
 ```
 
@@ -3326,7 +3327,7 @@ Positive integer values
 
 ## Use Auto Surround
 
-- Description: Whether to automatically surround selected text when typing opening parenthesis, bracket, brace, single or double quote characters. For example, when you select text and type (, Zed will surround the text with ().
+- Description: Whether to automatically surround selected text when typing opening parenthesis, bracket, brace, single or double quote characters. For example, when you select text and type '(', Zed will surround the text with ().
 - Setting: `use_auto_surround`
 - Default: `true`
 

docs/src/development/release-notes.md 🔗

@@ -0,0 +1,29 @@
+# Release Notes
+
+Whenever you open a pull request, the body is automatically populated based on this [pull request template](https://github.com/zed-industries/zed/blob/main/.github/pull_request_template.md).
+
+```md
+...
+
+Release Notes:
+
+- N/A _or_ Added/Fixed/Improved ...
+```
+
+On Wednesdays, we run a [`get-preview-channel-changes`](https://github.com/zed-industries/zed/blob/main/script/get-preview-channel-changes) script that scrapes `Release Notes` lines from pull requests landing in preview, as documented in our [Release](https://zed.dev/docs/development/releases) docs.
+
+The script outputs everything below the `Release Notes` line, including additional data such as the pull request author (if not a Zed team member) and a link to the pull request.
+If you use `N/A`, the script skips your pull request entirely.
+
+## Guidelines for crafting your `Release Notes` line(s)
+
+- A `Release Notes` line should only be written if the user can see or feel the difference in Zed.
+- A `Release Notes` line should be written such that a Zed user can understand what the change is.
+  Don't assume a user knows technical editor developer lingo; phrase your change in language they understand as a user of a text editor.
+- If you want to include technical details about your pull request for other team members to see, do so above the `Release Notes` line.
+- Changes to docs should be labeled as `N/A`.
+- If your pull request adds/changes a setting or a keybinding, always mention that setting or keybinding.
+  Don't make the user dig into docs or the pull request to find this information (although it should be included in docs as well).
+- For pull requests that are reverts:
+  - If the item being reverted **has already been shipped**, include a `Release Notes` line explaining why we reverted, as this is a breaking change.
+  - If the item being reverted **hasn't been shipped**, edit the original PR's `Release Notes` line to be `N/A`; otherwise, it will be included and the compiler of the release notes may not know to skip it, leading to a potentially-awkward situation where we are stating we shipped something we actually didn't.

docs/src/development/releases.md 🔗

@@ -11,7 +11,7 @@ Credentials for various services used in this process can be found in 1Password.
 Use the `releases` Slack channel to notify the team that releases will be starting.
 This is mostly a formality on Wednesday's minor update releases, but can be beneficial when doing patch releases, as other devs may have landed fixes they'd like to cherry pick.
 
----
+### Starting the Builds
 
 1. Checkout `main` and ensure your working copy is clean.
 
@@ -19,44 +19,74 @@ This is mostly a formality on Wednesday's minor update releases, but can be bene
 
 1. Run `git fetch --tags --force` to forcibly ensure your local tags are in sync with the remote.
 
-1. Run `./script/get-stable-channel-release-notes`.
-
-   - Follow the instructions at the end of the script and aggregate the release notes into one structure.
+1. Run `./script/get-stable-channel-release-notes` and store output locally.
 
 1. Run `./script/bump-zed-minor-versions`.
 
    - Push the tags and branches as instructed.
 
-1. Run `./script/get-preview-channel-changes`.
+1. Run `./script/get-preview-channel-changes` and store output locally.
 
-   - Take the script's output and build release notes by organizing each release note line into a category.
-   - Use a prior release for the initial outline.
-   - Make sure to append the `Credit` line, if present, to the end of the release note line.
+> **Note:** Always prioritize the stable release.
+> If you've completed aggregating stable release notes, you can move on to working on aggregating preview release notes, but once the stable build has finished, work through the rest of the stable steps to fully publish.
+> Preview can be finished up after.
 
-1. Once release drafts are up on [GitHub Releases](https://github.com/zed-industries/zed/releases), paste both preview and stable release notes into each and **save**.
+### Stable Release
 
-   - **Do not publish the drafts!**
+1. Aggregate stable release notes.
 
-1. Check the release assets.
+   - Follow the instructions at the end of the script and aggregate the release notes into one structure.
 
-   - Ensure the stable and preview release jobs have finished without error.
-   - Ensure each draft has the proper number of assets—releases currently have 11 assets each.
-   - Download the artifacts for each release draft and test that you can run them locally.
+1. Once the stable release draft is up on [GitHub Releases](https://github.com/zed-industries/zed/releases), paste the stable release notes into it and **save**.
+
+   - **Do not publish the draft!**
+
+1. Check the stable release assets.
 
-1. Publish the drafts.
+   - Ensure the stable release job has finished without error.
+   - Ensure the draft has the proper number of assets—releases currently have 11 assets each.
+   - Download the artifacts for the stable release draft and test that you can run them locally.
 
-   - Publish stable and preview drafts, one at a time.
-     - Use [Vercel](https://vercel.com/zed-industries/zed-dev) to check the progress of the website rebuild.
-       The release will be public once the rebuild has completed.
+1. Publish the stable draft on [GitHub Releases](https://github.com/zed-industries/zed/releases).
+
+   - Use [Vercel](https://vercel.com/zed-industries/zed-dev) to check the progress of the website rebuild.
+     The release will be public once the rebuild has completed.
 
 1. Post the stable release notes to social media.
 
    - Bluesky and X posts will already be built as drafts in [Buffer](https://buffer.com).
+   - Double-check links.
    - Publish both, one at a time, ensuring both are posted to each respective platform.
 
 1. Send the stable release notes email.
 
    - The email broadcast will already be built as a draft in [Kit](https://kit.com).
+   - Double-check links.
+   - Publish the email.
+
+### Preview Release
+
+1. Aggregate preview release notes.
+
+   - Take the script's output and build release notes by organizing each release note line into a category.
+   - Use a prior release for the initial outline.
+   - Make sure to append the `Credit` line, if present, to the end of the release note line.
+
+1. Once the preview release draft is up on [GitHub Releases](https://github.com/zed-industries/zed/releases), paste the preview release notes into it and **save**.
+
+   - **Do not publish the draft!**
+
+1. Check the preview release assets.
+
+   - Ensure the preview release job has finished without error.
+   - Ensure the draft has the proper number of assets—releases currently have 11 assets each.
+   - Download the artifacts for the preview release draft and test that you can run them locally.
+
+1. Publish the preview draft on [GitHub Releases](https://github.com/zed-industries/zed/releases).
+   - Use [Vercel](https://vercel.com/zed-industries/zed-dev) to check the progress of the website rebuild.
+     The release will be public once the rebuild has completed.
+
+### Prep Content for Next Week's Stable Release
 
 1. Build social media posts based on the popular items in preview.
 

docs/src/extensions/developing-extensions.md 🔗

@@ -23,11 +23,7 @@ From the extensions page, click the `Install Dev Extension` button (or the {#act
 
 If you need to troubleshoot, you can check the Zed.log ({#action zed::OpenLog}) for additional output. For debug output, close and relaunch zed with the `zed --foreground` from the command line which show more verbose INFO level logging.
 
-If you already have a published extension with the same name installed, your dev extension will override it.
-
-After installing, the `Extensions` page will indicate that the upstream extension is "Overridden by dev extension".
-
-Pre-installed extensions with the same name have to be uninstalled before installing the dev extension. See [#31106](https://github.com/zed-industries/zed/issues/31106) for more.
+If you already have the published version of the extension installed, the published version will be uninstalled prior to the installation of the dev extension. After successful installation, the `Extensions` page will indicate that the upstream extension is "Overridden by dev extension".
 
 ## Directory Structure of a Zed Extension
 
@@ -115,10 +111,13 @@ git submodule update
 
 ## Extension License Requirements
 
-As of October 1st, 2025, extension repositories must include one of the following licenses:
+As of October 1st, 2025, extension repositories must include a license.
+The following licenses are accepted:
 
-- [MIT](https://opensource.org/license/mit)
 - [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0)
+- [BSD 3-Clause](https://opensource.org/license/bsd-3-clause)
+- [GNU GPLv3](https://www.gnu.org/licenses/gpl-3.0.en.html)
+- [MIT](https://opensource.org/license/mit)
 
 This allows us to distribute the resulting binary produced from your extension code to our users.
 Without a valid license, the pull request to add or update your extension in the following steps will fail CI.

docs/src/git.md 🔗

@@ -17,6 +17,7 @@ Here's an overview of all currently supported features:
 - Git status in the Project Panel
 - Branch creating and switching
 - Git blame viewing
+- Git stash pop, apply, drop and view
 
 ## Git Panel
 
@@ -74,6 +75,41 @@ Zed offers two commit textareas:
 As soon as you commit in Zed, in the Git Panel, you'll see a bar right under the commit textarea, which will show the recently submitted commit.
 In there, you can use the "Uncommit" button, which performs the `git reset HEADˆ--soft` command.
 
+## Stashing
+
+Git stash allows you to temporarily save your uncommitted changes and revert your working directory to a clean state. This is particularly useful when you need to quickly switch branches or pull updates without committing incomplete work.
+
+### Creating Stashes
+
+To stash all your current changes, use the {#action git::StashAll} action. This will save both staged and unstaged changes to a new stash entry and clean your working directory.
+
+### Managing Stashes
+
+Zed provides a comprehensive stash picker accessible via {#action git::ViewStash}. From the stash picker, you can:
+
+- **View stash list**: Browse all your saved stashes with their descriptions and timestamps
+- **Open diffs**: See exactly what changes are stored in each stash
+- **Apply stashes**: Apply stash changes to your working directory while keeping the stash entry
+- **Pop stashes**: Apply stash changes and remove the stash entry from the list
+- **Drop stashes**: Delete unwanted stash entries without applying them
+
+### Quick Stash Operations
+
+For faster workflows, Zed provides direct actions to work with the most recent stash:
+
+- **Apply latest stash**: Use {#action git::StashApply} to apply the most recent stash without removing it
+- **Pop latest stash**: Use {#action git::StashPop} to apply and remove the most recent stash
+
+### Stash Diff View
+
+When viewing a specific stash in the diff view, you have additional options available through the interface:
+
+- Apply the current stash to your working directory
+- Pop the current stash (apply and remove)
+- Remove the stash without applying changes
+
+To open the stash diff view, select a stash from the stash picker and use the {#action stash_picker::ShowStashItem} ({#kb stash_picker::ShowStashItem}) keybinding.
+
 ## AI Support in Git
 
 Zed currently supports LLM-powered commit message generation.
@@ -151,6 +187,10 @@ When viewing files with changes, Zed displays diff hunks that can be expanded or
 | {#action git::Switch}                     | {#kb git::Switch}                     |
 | {#action git::CheckoutBranch}             | {#kb git::CheckoutBranch}             |
 | {#action git::Blame}                      | {#kb git::Blame}                      |
+| {#action git::StashAll}                   | {#kb git::StashAll}                   |
+| {#action git::StashPop}                   | {#kb git::StashPop}                   |
+| {#action git::StashApply}                 | {#kb git::StashApply}                 |
+| {#action git::ViewStash}                  | {#kb git::ViewStash}                  |
 | {#action editor::ToggleGitBlameInline}    | {#kb editor::ToggleGitBlameInline}    |
 | {#action editor::ExpandAllDiffHunks}      | {#kb editor::ExpandAllDiffHunks}      |
 | {#action editor::ToggleSelectedDiffHunks} | {#kb editor::ToggleSelectedDiffHunks} |

docs/src/languages/cpp.md 🔗

@@ -137,6 +137,7 @@ You can use CodeLLDB or GDB to debug native binaries. (Make sure that your build
 
 - [CodeLLDB configuration documentation](https://github.com/vadimcn/codelldb/blob/master/MANUAL.md#starting-a-new-debug-session)
 - [GDB configuration documentation](https://sourceware.org/gdb/current/onlinedocs/gdb.html/Debugger-Adapter-Protocol.html)
+  - GDB needs to be at least v14.1
 
 ### Build and Debug Binary
 

docs/src/languages/deno.md 🔗

@@ -69,7 +69,8 @@ To get completions for `deno.json` or `package.json` you can add the following t
           "schemas": [
             {
               "fileMatch": [
-                "deno.json"
+                "deno.json",
+                "deno.jsonc"
               ],
               "url": "https://raw.githubusercontent.com/denoland/deno/refs/heads/main/cli/schemas/config-file.v1.json"
             },

docs/src/languages/php.md 🔗

@@ -13,7 +13,7 @@ The PHP extension offers both `phpactor` and `intelephense` language server supp
 
 `phpactor` is enabled by default.
 
-## Phpactor
+### Phpactor
 
 The Zed PHP Extension can install `phpactor` automatically but requires `php` to be installed and available in your path:
 
@@ -25,7 +25,7 @@ The Zed PHP Extension can install `phpactor` automatically but requires `php` to
 which php
 ```
 
-## Intelephense
+### Intelephense
 
 [Intelephense](https://intelephense.com/) is a [proprietary](https://github.com/bmewburn/vscode-intelephense/blob/master/LICENSE.txt#L29) language server for PHP operating under a freemium model. Certain features require purchase of a [premium license](https://intelephense.com/).
 
@@ -60,3 +60,35 @@ To use the premium features, you can place your [licence.txt file](https://intel
 Zed supports syntax highlighting for PHPDoc comments.
 
 - Tree-sitter: [claytonrcarter/tree-sitter-phpdoc](https://github.com/claytonrcarter/tree-sitter-phpdoc)
+
+## Setting up Xdebug
+
+Zed’s PHP extension provides a debug adapter for PHP and Xdebug. The adapter name is `Xdebug`. Here a couple ways you can use it:
+
+```json
+[
+  {
+    "label": "PHP: Listen to Xdebug",
+    "adapter": "Xdebug",
+    "request": "launch",
+    "initialize_args": {
+      "port": 9003
+    }
+  },
+  {
+    "label": "PHP: Debug this test",
+    "adapter": "Xdebug",
+    "request": "launch",
+    "program": "vendor/bin/phpunit",
+    "args": ["--filter", "$ZED_SYMBOL"]
+  }
+]
+```
+
+In case you run into issues:
+
+- ensure that you have Xdebug installed for the version of PHP you’re running
+- ensure that Xdebug is configured to run in `debug` mode
+- ensure that Xdebug is actually starting a debugging session
+- check that the host and port matches between Xdebug and Zed
+- look at the diagnostics log by using the `xdebug_info()` function in the page you’re trying to debug

docs/src/languages/yaml.md 🔗

@@ -74,7 +74,7 @@ You can override any auto-detected schema via the `schemas` settings key (demons
 name: Issue Assignment
 on:
   issues:
-    types: [oppened]
+    types: [opened]
 ```
 
 You can disable the automatic detection and retrieval of schemas from the JSON Schema if desired:

docs/src/linux.md 🔗

@@ -51,10 +51,21 @@ There are several third-party Zed packages for various Linux distributions and p
 
 See [Repology](https://repology.org/project/zed-editor/versions) for a list of Zed packages in various repositories.
 
+### Community
+
 When installing a third-party package please be aware that it may not be completely up to date and may be slightly different from the Zed we package (a common change is to rename the binary to `zedit` or `zeditor` to avoid conflicting with other packages).
 
 We'd love your help making Zed available for everyone. If Zed is not yet available for your package manager, and you would like to fix that, we have some notes on [how to do it](./development/linux.md#notes-for-packaging-zed).
 
+The packages in this section provide binary installs for Zed but are not official packages within the associated distributions. These packages are maintained by community members and as such a higher level of caution should be taken when installing them.
+
+#### Debian
+
+Zed is available in [this community-maintained repository](https://debian.griffo.io/).
+
+Instructions for each version are available in the README of the repository where packages are built.
+Build, packaging and instructions for each version are available in the README of the [repository](https://github.com/dariogriffo/zed-debian)
+
 ### Downloading manually
 
 If you'd prefer, you can install Zed by downloading our pre-built .tar.gz. This is the same artifact that our install script uses, but you can customize the location of your installation by modifying the instructions below:

docs/src/themes.md 🔗

@@ -32,20 +32,22 @@ By default, Zed maintains two themes: one for light mode and one for dark mode.
 
 ## Theme Overrides
 
-To override specific attributes of a theme, use the `experimental.theme_overrides` setting.
+To override specific attributes of a theme, use the `theme_overrides` setting. This setting can be used to configure theme-specific overrides.
 
 For example, add the following to your `settings.json` if you wish to override the background color of the editor and display comments and doc comments as italics:
 
 ```json [settings]
 {
-  "experimental.theme_overrides": {
-    "editor.background": "#333",
-    "syntax": {
-      "comment": {
-        "font_style": "italic"
-      },
-      "comment.doc": {
-        "font_style": "italic"
+  "theme_overrides": {
+    "One Dark": {
+      "editor.background": "#333",
+      "syntax": {
+        "comment": {
+          "font_style": "italic"
+        },
+        "comment.doc": {
+          "font_style": "italic"
+        }
       }
     }
   }
@@ -58,7 +60,7 @@ To see a list of available theme attributes look at the JSON file for your theme
 
 ## Local Themes
 
-Store new themes locally by placing them in the `~/.config/zed/themes` directory.
+Store new themes locally by placing them in the `~/.config/zed/themes` directory (macOS and Linux) or `%USERPROFILE%\AppData\Roaming\Zed\themes\` (Windows).
 
 For example, to create a new theme called `my-cool-theme`, create a file called `my-cool-theme.json` in that directory. It will be available in the theme selector the next time Zed loads.
 

docs/src/troubleshooting.md 🔗

@@ -0,0 +1,80 @@
+# Troubleshooting
+
+This guide covers common troubleshooting techniques for Zed.
+Sometimes you'll be able to identify and resolve issues on your own using this information.
+Other times, troubleshooting means gathering the right information—logs, profiles, or reproduction steps—to help us diagnose and fix the problem.
+
+> **Note**: To open the command palette, use `cmd-shift-p` on macOS or `ctrl-shift-p` on Windows / Linux.
+
+## Retrieve Zed and System Information
+
+When reporting issues or seeking help, it's useful to know your Zed version and system specifications. You can retrieve this information using the following actions from the command palette:
+
+- {#action zed::About}: Find your Zed version number
+- {#action zed::CopySystemSpecsIntoClipboard}: Populate your clipboard with Zed version number, operating system version, and hardware specs
+
+## Zed Log
+
+Often, a good first place to look when troubleshooting any issue in Zed is the Zed log, which might contain clues about what's going wrong.
+You can review the most recent 1000 lines of the log by running the {#action zed::OpenLog} action from the command palette.
+If you want to view the full file, you can reveal it in your operating system's native file manager via {#action zed::RevealLogInFileManager} from the command palette.
+
+You'll find the Zed log in the respective location on each operating system:
+
+- macOS: `~/Library/Logs/Zed/Zed.log`
+- Windows: `C:\Users\YOU\AppData\Local\Zed\logs\Zed.log`
+- Linux: `~/.local/share/zed/logs/Zed.log` or `$XDG_DATA_HOME`
+
+> Note: In some cases, it might be useful to monitor the log live, such as when [developing a Zed extension](https://zed.dev/docs/extensions/developing-extensions).
+> Example: `tail -f ~/Library/Logs/Zed/Zed.log`
+
+The log may contain enough context to help you debug the issue yourself, or you may find specific errors that are useful when filing a [GitHub issue](https://github.com/zed-industries/zed/issues/new/choose) or when talking to Zed staff in our [Discord server](https://zed.dev/community-links#forums-and-discussions).
+
+## Performance Issues (Profiling)
+
+If you're running into performance issues in Zed—such as hitches, hangs, or general unresponsiveness—having a performance profile attached to your issue will help us zero in on what is getting stuck, so we can fix it.
+
+### macOS
+
+Xcode Instruments (which comes bundled with your [Xcode](https://apps.apple.com/us/app/xcode/id497799835) download) is the standard tool for profiling on macOS.
+
+1. With Zed running, open Instruments
+1. Select `Time Profiler` as the profiling template
+1. In the `Time Profiler` configuration, set the target to the running Zed process
+1. Start recording
+1. If the performance issue occurs when performing a specific action in Zed, perform that action now
+1. Stop recording
+1. Save the trace file
+1. Compress the trace file into a zip archive
+1. File a [GitHub issue](https://github.com/zed-industries/zed/issues/new/choose) with the trace zip attached
+
+<!--### Windows-->
+
+<!--### Linux-->
+
+## Startup and Workspace Issues
+
+Zed creates local SQLite databases to persist data relating to its workspace and your projects. These databases store, for instance, the tabs and panes you have open in a project, the scroll position of each open file, the list of all projects you've opened (for the recent projects modal picker), etc. You can find and explore these databases in the following locations:
+
+- macOS: `~/Library/Application Support/Zed/db`
+- Linux and FreeBSD: `~/.local/share/zed/db` (or within `XDG_DATA_HOME` or `FLATPAK_XDG_DATA_HOME`)
+- Windows: `%LOCALAPPDATA%\Zed\db`
+
+The naming convention of these databases takes on the form of `0-<zed_channel>`:
+
+- Stable: `0-stable`
+- Preview: `0-preview`
+- Nightly: `0-nightly`
+- Dev: `0-dev`
+
+While rare, we've seen a few cases where workspace databases became corrupted, which prevented Zed from starting.
+If you're experiencing startup issues, you can test whether it's workspace-related by temporarily moving the database from its location, then trying to start Zed again.
+
+> **Note**: Moving the workspace database will cause Zed to create a fresh one.
+> Your recent projects, open tabs, etc. will be reset to "factory".
+
+If your issue persists after regenerating the database, please [file an issue](https://github.com/zed-industries/zed/issues/new/choose).
+
+## Language Server Issues
+
+If you're experiencing language-server related issues, such as stale diagnostics or issues jumping to definitions, restarting the language server via {#action editor::RestartLanguageServer} from the command palette will often resolve the issue.

docs/src/visual-customization.md 🔗

@@ -182,7 +182,7 @@ TBD: Centered layout related settings
   "show_whitespaces": "selection",
   "whitespace_map": { // Which characters to show when `show_whitespaces` enabled
     "space": "•",
-    "tab": "→"
+    "tab": "⟶"       // use "→", for a shorter arrow
   },
 
   "unnecessary_code_fade": 0.3, // How much to fade out unused code.
@@ -319,6 +319,10 @@ TBD: Centered layout related settings
     // Clicking the button brings up an input for jumping to a line and column.
     // Defaults to true.
     "cursor_position_button": true,
+    // Show/hide a button that displays the buffer's line-ending mode.
+    // Clicking the button brings up the line-ending selector.
+    // Defaults to false.
+    "line_endings_button": false
   },
   "global_lsp_settings": {
     // Show/hide the LSP button in the status bar.

docs/src/workspace-persistence.md 🔗

@@ -1,31 +0,0 @@
-# Workspace Persistence
-
-Zed creates local SQLite databases to persist data relating to its workspace and your projects. These databases store, for instance, the tabs and panes you have open in a project, the scroll position of each open file, the list of all projects you've opened (for the recent projects modal picker), etc. You can find and explore these databases in the following locations:
-
-- macOS: `~/Library/Application Support/Zed`
-- Linux and FreeBSD: `~/.local/share/zed` (or within `XDG_DATA_HOME` or `FLATPAK_XDG_DATA_HOME`)
-- Windows: `%LOCALAPPDATA%\Zed`
-
-The naming convention of these databases takes on the form of `0-<zed_channel>`:
-
-- Stable: `0-stable`
-- Preview: `0-preview`
-
-**If you encounter workspace persistence issues in Zed, deleting the database and restarting Zed often resolves the problem, as the database may have been corrupted at some point.** If your issue continues after restarting Zed and regenerating a new database, please [file an issue](https://github.com/zed-industries/zed/issues/new?template=10_bug_report.yml).
-
-## Settings
-
-You can customize workspace restoration behavior with the following settings:
-
-```json [settings]
-{
-  // Workspace restoration behavior.
-  //   All workspaces ("last_session"), last workspace ("last_workspace") or "none"
-  "restore_on_startup": "last_session",
-  // Whether to attempt to restore previous file's state when opening it again.
-  // E.g. for editors, selections, folds and scroll positions are restored
-  "restore_on_file_reopen": true,
-  // Whether to automatically close files that have been deleted on disk.
-  "close_on_file_delete": false
-}
-```

extensions/html/src/html.rs 🔗

@@ -68,22 +68,24 @@ impl zed::Extension for HtmlExtension {
         worktree: &zed::Worktree,
     ) -> Result<zed::Command> {
         let server_path = if let Some(path) = worktree.which(BINARY_NAME) {
-            path
+            return Ok(zed::Command {
+                command: path,
+                args: vec!["--stdio".to_string()],
+                env: Default::default(),
+            });
         } else {
-            self.server_script_path(language_server_id)?
+            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![
-                env::current_dir()
-                    .unwrap()
-                    .join(&server_path)
-                    .to_string_lossy()
-                    .to_string(),
-                "--stdio".to_string(),
-            ],
+            args: vec![server_path, "--stdio".to_string()],
             env: Default::default(),
         })
     }

extensions/slash-commands-example/README.md 🔗

@@ -76,8 +76,7 @@ Rebuild to see these changes reflected:
 
 ## Troubleshooting / Logs
 
-- MacOS: `tail -f ~/Library/Logs/Zed/Zed.log`
-- Linux: `tail -f ~/.local/share/zed/logs/Zed.log`
+- [zed.dev docs: Troubleshooting](https://zed.dev/docs/troubleshooting)
 
 ## Documentation
 

renovate.json 🔗

@@ -12,7 +12,7 @@
   "timezone": "America/New_York",
   "schedule": ["after 3pm on Wednesday"],
   "prFooter": "Release Notes:\n\n- N/A",
-  "ignorePaths": ["**/node_modules/**", "tooling/workspace-hack/**"],
+  "ignorePaths": ["**/node_modules/**"],
   "packageRules": [
     {
       "description": "Group wasmtime crates together.",

script/bundle-linux 🔗

@@ -91,7 +91,7 @@ else
     if [[ -n "${SENTRY_AUTH_TOKEN:-}" ]]; then
         echo "Uploading zed debug symbols to sentry..."
         # note: this uploads the unstripped binary which is needed because it contains
-        # .eh_frame data for stack unwinindg. see https://github.com/getsentry/symbolic/issues/783
+        # .eh_frame data for stack unwinding. see https://github.com/getsentry/symbolic/issues/783
         sentry-cli debug-files upload --include-sources --wait -p zed -o zed-dev \
             "${target_dir}/${target_triple}"/release/zed \
             "${target_dir}/${remote_server_triple}"/release/remote_server

script/bundle-mac 🔗

@@ -375,7 +375,7 @@ function upload_debug_info() {
     if [[ -n "${SENTRY_AUTH_TOKEN:-}" ]]; then
         echo "Uploading zed debug symbols to sentry..."
         # note: this uploads the unstripped binary which is needed because it contains
-        # .eh_frame data for stack unwinindg. see https://github.com/getsentry/symbolic/issues/783
+        # .eh_frame data for stack unwinding. see https://github.com/getsentry/symbolic/issues/783
         sentry-cli debug-files upload --include-sources --wait -p zed -o zed-dev \
             "target/${architecture}/${target_dir}/zed" \
             "target/${architecture}/${target_dir}/remote_server" \

script/danger/dangerfile.ts 🔗

@@ -61,12 +61,11 @@ if (includesIssueUrl) {
 const PROMPT_PATHS = [
   "assets/prompts/content_prompt.hbs",
   "assets/prompts/terminal_assistant_prompt.hbs",
-  "crates/agent/src/prompts/stale_files_prompt_header.txt",
-  "crates/agent/src/prompts/summarize_thread_detailed_prompt.txt",
-  "crates/agent/src/prompts/summarize_thread_prompt.txt",
-  "crates/assistant_tools/src/templates/create_file_prompt.hbs",
-  "crates/assistant_tools/src/templates/edit_file_prompt_xml.hbs",
-  "crates/assistant_tools/src/templates/edit_file_prompt_diff_fenced.hbs",
+  "crates/agent_settings/src/prompts/summarize_thread_detailed_prompt.txt",
+  "crates/agent_settings/src/prompts/summarize_thread_prompt.txt",
+  "crates/agent/src/templates/create_file_prompt.hbs",
+  "crates/agent/src/templates/edit_file_prompt_xml.hbs",
+  "crates/agent/src/templates/edit_file_prompt_diff_fenced.hbs",
   "crates/git_ui/src/commit_message_prompt.txt",
 ];
 

script/licenses/zed-licenses.toml 🔗

@@ -34,147 +34,3 @@ license = "BSD-3-Clause"
 [[fuchsia-cprng.clarify.files]]
 path = 'LICENSE'
 checksum = '03b114f53e6587a398931762ee11e2395bfdba252a329940e2c8c9e81813845b'
-
-[pet.clarify]
-license = "MIT"
-[[pet.clarify.files]]
-path = '../../LICENSE'
-checksum = 'c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383'
-
-[pet-conda.clarify]
-license = "MIT"
-[[pet-conda.clarify.files]]
-path = '../../LICENSE'
-checksum = 'c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383'
-
-[pet-core.clarify]
-license = "MIT"
-[[pet-core.clarify.files]]
-path = '../../LICENSE'
-checksum = 'c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383'
-
-[pet-env-var-path.clarify]
-license = "MIT"
-[[pet-env-var-path.clarify.files]]
-path = '../../LICENSE'
-checksum = 'c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383'
-
-[pet-fs.clarify]
-license = "MIT"
-[[pet-fs.clarify.files]]
-path = '../../LICENSE'
-checksum = 'c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383'
-
-[pet-global-virtualenvs.clarify]
-license = "MIT"
-[[pet-global-virtualenvs.clarify.files]]
-path = '../../LICENSE'
-checksum = 'c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383'
-
-[pet-homebrew.clarify]
-license = "MIT"
-[[pet-homebrew.clarify.files]]
-path = '../../LICENSE'
-checksum = 'c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383'
-
-[pet-jsonrpc.clarify]
-license = "MIT"
-[[pet-jsonrpc.clarify.files]]
-path = '../../LICENSE'
-checksum = 'c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383'
-
-[pet-linux-global-python.clarify]
-license = "MIT"
-[[pet-linux-global-python.clarify.files]]
-path = '../../LICENSE'
-checksum = 'c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383'
-
-[pet-mac-commandlinetools.clarify]
-license = "MIT"
-[[pet-mac-commandlinetools.clarify.files]]
-path = '../../LICENSE'
-checksum = 'c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383'
-
-[pet-mac-python-org.clarify]
-license = "MIT"
-[[pet-mac-python-org.clarify.files]]
-path = '../../LICENSE'
-checksum = 'c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383'
-
-[pet-mac-xcode.clarify]
-license = "MIT"
-[[pet-mac-xcode.clarify.files]]
-path = '../../LICENSE'
-checksum = 'c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383'
-
-[pet-pipenv.clarify]
-license = "MIT"
-[[pet-pipenv.clarify.files]]
-path = '../../LICENSE'
-checksum = 'c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383'
-
-[pet-pixi.clarify]
-license = "MIT"
-[[pet-pixi.clarify.files]]
-path = '../../LICENSE'
-checksum = 'c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383'
-
-[pet-poetry.clarify]
-license = "MIT"
-[[pet-poetry.clarify.files]]
-path = '../../LICENSE'
-checksum = 'c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383'
-
-[pet-pyenv.clarify]
-license = "MIT"
-[[pet-pyenv.clarify.files]]
-path = '../../LICENSE'
-checksum = 'c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383'
-
-[pet-python-utils.clarify]
-license = "MIT"
-[[pet-python-utils.clarify.files]]
-path = '../../LICENSE'
-checksum = 'c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383'
-
-[pet-reporter.clarify]
-license = "MIT"
-[[pet-reporter.clarify.files]]
-path = '../../LICENSE'
-checksum = 'c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383'
-
-[pet-telemetry.clarify]
-license = "MIT"
-[[pet-telemetry.clarify.files]]
-path = '../../LICENSE'
-checksum = 'c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383'
-
-[pet-venv.clarify]
-license = "MIT"
-[[pet-venv.clarify.files]]
-path = '../../LICENSE'
-checksum = 'c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383'
-
-[pet-virtualenv.clarify]
-license = "MIT"
-[[pet-virtualenv.clarify.files]]
-path = '../../LICENSE'
-checksum = 'c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383'
-
-[pet-virtualenvwrapper.clarify]
-license = "MIT"
-[[pet-virtualenvwrapper.clarify.files]]
-path = '../../LICENSE'
-checksum = 'c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383'
-
-[pet-windows-registry.clarify]
-license = "MIT"
-[[pet-windows-registry.clarify.files]]
-path = '../../LICENSE'
-checksum = 'c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383'
-
-[pet-windows-store.clarify]
-license = "MIT"
-[[pet-windows-store.clarify.files]]
-path = '../../LICENSE'
-checksum = 'c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383'

script/new-crate 🔗

@@ -63,7 +63,6 @@ anyhow.workspace = true
 gpui.workspace = true
 ui.workspace = true
 util.workspace = true
-workspace-hack.workspace = true
 
 # Uncomment other workspace dependencies as needed
 # assistant.workspace = true

script/update-workspace-hack 🔗

@@ -1,20 +0,0 @@
-#!/usr/bin/env bash
-
-set -euo pipefail
-
-HAKARI_VERSION="0.9"
-
-cd "$(dirname "$0")/.." || exit 1
-
-if ! cargo hakari --version | grep "cargo-hakari $HAKARI_VERSION" >/dev/null; then
-    echo "Installing cargo-hakari@^$HAKARI_VERSION..."
-    cargo install "cargo-hakari@^$HAKARI_VERSION"
-else
-    echo "cargo-hakari@^$HAKARI_VERSION is already installed."
-fi
-
-# update the workspace-hack crate
-cargo hakari generate
-
-# make sure workspace-hack is added as a dep for all crates in the workspace
-cargo hakari manage-deps

script/update-workspace-hack.ps1 🔗

@@ -1,36 +0,0 @@
-$ErrorActionPreference = "Stop"
-
-$HAKARI_VERSION = "0.9"
-
-$scriptPath = Split-Path -Parent $MyInvocation.MyCommand.Path
-Set-Location (Split-Path -Parent $scriptPath)
-
-$hakariInstalled = $false
-try {
-    $versionOutput = cargo hakari --version 2>&1
-    if ($versionOutput -match "cargo-hakari $HAKARI_VERSION") {
-        $hakariInstalled = $true
-    }
-}
-catch {
-    $hakariInstalled = $false
-}
-
-if (-not $hakariInstalled) {
-    Write-Host "Installing cargo-hakari@^$HAKARI_VERSION..."
-    cargo install "cargo-hakari@^$HAKARI_VERSION"
-    if ($LASTEXITCODE -ne 0) {
-        throw "Failed to install cargo-hakari@^$HAKARI_VERSION"
-    }
-}
-else {
-    Write-Host "cargo-hakari@^$HAKARI_VERSION is already installed."
-}
-
-# update the workspace-hack crate
-cargo hakari generate
-if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
-
-# make sure workspace-hack is added as a dep for all crates in the workspace
-cargo hakari manage-deps
-if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }

tooling/perf/Cargo.toml 🔗

@@ -30,4 +30,3 @@ disallowed_methods = { level = "allow", priority = 1}
 collections.workspace = true
 serde.workspace = true
 serde_json.workspace = true
-workspace-hack.workspace = true

tooling/perf/src/implementation.rs 🔗

@@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize};
 use std::{num::NonZero, time::Duration};
 
 pub mod consts {
-    //! Preset idenitifiers and constants so that the profiler and proc macro agree
+    //! Preset identifiers and constants so that the profiler and proc macro agree
     //! on their communication protocol.
 
     /// The suffix on the actual test function.
@@ -420,13 +420,16 @@ impl std::fmt::Display for PerfReport {
         for (cat, delta) in sorted.into_iter().rev() {
             const SIGN_POS: &str = "↑";
             const SIGN_NEG: &str = "↓";
-            const SIGN_NEUTRAL: &str = "±";
+            const SIGN_NEUTRAL_POS: &str = "±↑";
+            const SIGN_NEUTRAL_NEG: &str = "±↓";
 
             let prettify = |time: f64| {
                 let sign = if time > 0.05 {
                     SIGN_POS
-                } else if time < 0.05 && time > -0.05 {
-                    SIGN_NEUTRAL
+                } else if time > 0. {
+                    SIGN_NEUTRAL_POS
+                } else if time > -0.05 {
+                    SIGN_NEUTRAL_NEG
                 } else {
                     SIGN_NEG
                 };

tooling/perf/src/main.rs 🔗

@@ -228,8 +228,8 @@ fn compare_profiles(args: &[String]) {
                 a.strip_prefix("--save=")
                     .expect("FATAL: save param formatted incorrectly"),
             );
+            ident_idx = 1;
         }
-        ident_idx = 1;
     });
     let ident_new = args
         .get(ident_idx)

tooling/workspace-hack/.gitattributes 🔗

@@ -1,4 +0,0 @@
-# Avoid putting conflict markers in the generated Cargo.toml file, since their presence breaks
-# Cargo.
-# Also do not check out the file as CRLF on Windows, as that's what hakari needs.
-Cargo.toml merge=binary -crlf

tooling/workspace-hack/Cargo.toml 🔗

@@ -1,700 +0,0 @@
-# This file is generated by `cargo hakari`.
-# To regenerate, run:
-#     cargo install cargo-hakari
-#     cargo hakari generate
-
-[package]
-name = "workspace-hack"
-version = "0.1.0"
-description = "workspace-hack package, managed by hakari"
-edition.workspace = true
-publish.workspace = true
-
-# The parts of the file between the BEGIN HAKARI SECTION and END HAKARI SECTION comments
-# are managed by hakari.
-
-### BEGIN HAKARI SECTION
-[dependencies]
-ahash = { version = "0.8", features = ["serde"] }
-aho-corasick = { version = "1" }
-anstream = { version = "0.6" }
-arrayvec = { version = "0.7", features = ["serde"] }
-async-compression = { version = "0.4", default-features = false, features = ["deflate", "deflate64", "futures-io", "gzip"] }
-async-std = { version = "1", features = ["attributes", "unstable"] }
-async-tungstenite = { version = "0.29", features = ["tokio-rustls-manual-roots"] }
-aws-config = { version = "1", features = ["behavior-version-latest"] }
-aws-credential-types = { version = "1", default-features = false, features = ["hardcoded-credentials", "test-util"] }
-aws-runtime = { version = "1", default-features = false, features = ["event-stream", "http-02x", "sigv4a"] }
-aws-sigv4 = { version = "1", features = ["http0-compat", "sign-eventstream", "sigv4a"] }
-aws-smithy-async = { version = "1", default-features = false, features = ["rt-tokio"] }
-aws-smithy-http = { version = "0.62", default-features = false, features = ["event-stream"] }
-aws-smithy-runtime = { version = "1", default-features = false, features = ["client", "default-https-client", "rt-tokio", "tls-rustls"] }
-aws-smithy-runtime-api = { version = "1", features = ["client", "http-02x", "http-auth", "test-util"] }
-aws-smithy-types = { version = "1", default-features = false, features = ["byte-stream-poll-next", "http-body-0-4-x", "http-body-1-x", "rt-tokio", "test-util"] }
-base64 = { version = "0.22" }
-base64ct = { version = "1", default-features = false, features = ["std"] }
-bigdecimal = { version = "0.4", features = ["serde"] }
-bit-set = { version = "0.8", default-features = false, features = ["std"] }
-bit-vec = { version = "0.8", default-features = false, features = ["std"] }
-bitflags = { version = "2", default-features = false, features = ["serde", "std"] }
-bstr = { version = "1" }
-bytemuck = { version = "1", default-features = false, features = ["aarch64_simd", "derive", "extern_crate_alloc", "must_cast"] }
-byteorder = { version = "1" }
-bytes = { version = "1", features = ["serde"] }
-chrono = { version = "0.4", features = ["serde"] }
-clap = { version = "4", features = ["cargo", "derive", "string", "wrap_help"] }
-clap_builder = { version = "4", default-features = false, features = ["cargo", "color", "std", "string", "suggestions", "usage", "wrap_help"] }
-concurrent-queue = { version = "2" }
-cranelift-codegen = { version = "0.116", default-features = false, features = ["host-arch", "incremental-cache", "std", "timing", "unwind"] }
-crossbeam-channel = { version = "0.5" }
-crossbeam-epoch = { version = "0.9" }
-crossbeam-utils = { version = "0.8" }
-deranged = { version = "0.4", default-features = false, features = ["powerfmt", "serde", "std"] }
-digest = { version = "0.10", features = ["mac", "oid", "std"] }
-either = { version = "1", features = ["serde", "use_std"] }
-euclid = { version = "0.22" }
-event-listener = { version = "5" }
-event-listener-strategy = { version = "0.5" }
-flate2 = { version = "1", features = ["zlib-rs"] }
-foldhash = { version = "0.1" }
-form_urlencoded = { version = "1" }
-futures = { version = "0.3", features = ["io-compat"] }
-futures-channel = { version = "0.3", features = ["sink"] }
-futures-core = { version = "0.3" }
-futures-executor = { version = "0.3" }
-futures-io = { version = "0.3" }
-futures-sink = { version = "0.3" }
-futures-task = { version = "0.3", default-features = false, features = ["std"] }
-futures-util = { version = "0.3", features = ["channel", "io-compat", "sink"] }
-getrandom-6f8ce4dd05d13bba = { package = "getrandom", version = "0.2", default-features = false, features = ["std"] }
-half = { version = "2", features = ["bytemuck", "num-traits", "rand_distr", "use-intrinsics"] }
-handlebars = { version = "4", features = ["rust-embed"] }
-hashbrown-3575ec1268b04181 = { package = "hashbrown", version = "0.15", features = ["rayon", "serde"] }
-hashbrown-582f2526e08bb6a0 = { package = "hashbrown", version = "0.14", features = ["raw"] }
-hmac = { version = "0.12", default-features = false, features = ["reset"] }
-hyper = { version = "0.14", features = ["client", "http1", "http2", "runtime", "server", "stream"] }
-idna = { version = "1" }
-indexmap = { version = "2", features = ["serde"] }
-itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12" }
-lazy_static = { version = "1", default-features = false, features = ["spin_no_std"] }
-libc = { version = "0.2", features = ["extra_traits"] }
-libsqlite3-sys = { version = "0.30", features = ["bundled", "unlock_notify"] }
-log = { version = "0.4", default-features = false, features = ["kv_unstable_serde"] }
-lyon = { version = "1", default-features = false, features = ["extra"] }
-lyon_path = { version = "1" }
-md-5 = { version = "0.10" }
-memchr = { version = "2" }
-memmap2 = { version = "0.9", default-features = false, features = ["stable_deref_trait"] }
-mime_guess = { version = "2" }
-miniz_oxide = { version = "0.8", features = ["simd"] }
-nom = { version = "7" }
-num-bigint = { version = "0.4" }
-num-integer = { version = "0.1", features = ["i128"] }
-num-iter = { version = "0.1", default-features = false, features = ["i128", "std"] }
-num-rational = { version = "0.4", features = ["num-bigint-std"] }
-num-traits = { version = "0.2", features = ["i128", "libm"] }
-once_cell = { version = "1" }
-percent-encoding = { version = "2" }
-phf = { version = "0.11", features = ["macros"] }
-phf_shared = { version = "0.11" }
-prost-274715c4dabd11b0 = { package = "prost", version = "0.9" }
-prost-types = { version = "0.9" }
-rand-c38e5c1d305a1b54 = { package = "rand", version = "0.8", features = ["small_rng"] }
-rand_chacha = { version = "0.3", default-features = false, features = ["std"] }
-rand_core = { version = "0.6", default-features = false, features = ["std"] }
-rand_distr = { version = "0.5" }
-regalloc2 = { version = "0.11", features = ["checker", "enable-serde"] }
-regex = { version = "1" }
-regex-automata = { version = "0.4" }
-regex-syntax = { version = "0.8" }
-reqwest = { version = "0.12", default-features = false, features = ["blocking", "http2", "json", "rustls-tls-native-roots", "stream"] }
-ring = { version = "0.17", features = ["std"] }
-rust_decimal = { version = "1", default-features = false, features = ["maths", "serde", "std"] }
-rustc-hash = { version = "1" }
-rustix-dff4ba8e3ae991db = { package = "rustix", version = "1", features = ["fs", "net"] }
-rustls = { version = "0.23", features = ["ring"] }
-rustls-webpki = { version = "0.103", default-features = false, features = ["aws-lc-rs", "ring", "std"] }
-sea-orm = { version = "1", features = ["runtime-tokio-rustls", "sqlx-postgres", "sqlx-sqlite"] }
-sea-query-binder = { version = "0.7", default-features = false, features = ["postgres-array", "sqlx-postgres", "sqlx-sqlite", "with-bigdecimal", "with-chrono", "with-json", "with-rust_decimal", "with-time", "with-uuid"] }
-semver = { version = "1", features = ["serde"] }
-serde = { version = "1", features = ["alloc", "derive", "rc"] }
-serde_core = { version = "1", default-features = false, features = ["alloc", "rc", "result", "std"] }
-serde_json = { version = "1", features = ["alloc", "preserve_order", "raw_value", "unbounded_depth"] }
-simd-adler32 = { version = "0.3" }
-smallvec = { version = "1", default-features = false, features = ["const_new", "serde", "union"] }
-spin = { version = "0.9" }
-sqlx = { version = "0.8", features = ["bigdecimal", "chrono", "postgres", "runtime-tokio-rustls", "rust_decimal", "sqlite", "time", "uuid"] }
-sqlx-postgres = { version = "0.8", default-features = false, features = ["any", "bigdecimal", "chrono", "json", "migrate", "offline", "rust_decimal", "time", "uuid"] }
-sqlx-sqlite = { version = "0.8", default-features = false, features = ["any", "bundled", "chrono", "json", "migrate", "offline", "time", "uuid"] }
-stable_deref_trait = { version = "1" }
-strum = { version = "0.26", features = ["derive"] }
-subtle = { version = "2" }
-thiserror = { version = "2" }
-time = { version = "0.3", features = ["local-offset", "macros", "serde-well-known"] }
-tokio = { version = "1", features = ["full"] }
-tokio-rustls = { version = "0.26", default-features = false, features = ["tls12"] }
-tokio-util = { version = "0.7", features = ["codec", "compat", "io-util", "rt"] }
-toml_datetime = { version = "0.6", default-features = false, features = ["serde"] }
-toml_edit = { version = "0.22", default-features = false, features = ["display", "parse", "serde"] }
-tracing = { version = "0.1", features = ["log"] }
-tracing-core = { version = "0.1" }
-tungstenite = { version = "0.26", default-features = false, features = ["__rustls-tls", "handshake"] }
-unicode-properties = { version = "0.1" }
-url = { version = "2", features = ["serde"] }
-uuid = { version = "1", features = ["serde", "v4", "v5", "v7"] }
-wasmparser = { version = "0.221" }
-wasmtime = { version = "29", default-features = false, features = ["async", "component-model", "cranelift", "demangle", "gc-drc", "incremental-cache", "parallel-compilation"] }
-wasmtime-cranelift = { version = "29", default-features = false, features = ["component-model", "gc-drc", "incremental-cache"] }
-wasmtime-environ = { version = "29", default-features = false, features = ["compile", "component-model", "demangle", "gc-drc"] }
-
-[build-dependencies]
-ahash = { version = "0.8", features = ["serde"] }
-aho-corasick = { version = "1" }
-anstream = { version = "0.6" }
-arrayvec = { version = "0.7", features = ["serde"] }
-async-compression = { version = "0.4", default-features = false, features = ["deflate", "deflate64", "futures-io", "gzip"] }
-async-std = { version = "1", features = ["attributes", "unstable"] }
-async-tungstenite = { version = "0.29", features = ["tokio-rustls-manual-roots"] }
-aws-config = { version = "1", features = ["behavior-version-latest"] }
-aws-credential-types = { version = "1", default-features = false, features = ["hardcoded-credentials", "test-util"] }
-aws-runtime = { version = "1", default-features = false, features = ["event-stream", "http-02x", "sigv4a"] }
-aws-sigv4 = { version = "1", features = ["http0-compat", "sign-eventstream", "sigv4a"] }
-aws-smithy-async = { version = "1", default-features = false, features = ["rt-tokio"] }
-aws-smithy-http = { version = "0.62", default-features = false, features = ["event-stream"] }
-aws-smithy-runtime = { version = "1", default-features = false, features = ["client", "default-https-client", "rt-tokio", "tls-rustls"] }
-aws-smithy-runtime-api = { version = "1", features = ["client", "http-02x", "http-auth", "test-util"] }
-aws-smithy-types = { version = "1", default-features = false, features = ["byte-stream-poll-next", "http-body-0-4-x", "http-body-1-x", "rt-tokio", "test-util"] }
-base64 = { version = "0.22" }
-base64ct = { version = "1", default-features = false, features = ["std"] }
-bigdecimal = { version = "0.4", features = ["serde"] }
-bit-set = { version = "0.8", default-features = false, features = ["std"] }
-bit-vec = { version = "0.8", default-features = false, features = ["std"] }
-bitflags = { version = "2", default-features = false, features = ["serde", "std"] }
-bstr = { version = "1" }
-bytemuck = { version = "1", default-features = false, features = ["aarch64_simd", "derive", "extern_crate_alloc", "must_cast"] }
-byteorder = { version = "1" }
-bytes = { version = "1", features = ["serde"] }
-cc = { version = "1", default-features = false, features = ["parallel"] }
-chrono = { version = "0.4", features = ["serde"] }
-clap = { version = "4", features = ["cargo", "derive", "string", "wrap_help"] }
-clap_builder = { version = "4", default-features = false, features = ["cargo", "color", "std", "string", "suggestions", "usage", "wrap_help"] }
-concurrent-queue = { version = "2" }
-cranelift-codegen = { version = "0.116", default-features = false, features = ["host-arch", "incremental-cache", "std", "timing", "unwind"] }
-crossbeam-channel = { version = "0.5" }
-crossbeam-epoch = { version = "0.9" }
-crossbeam-utils = { version = "0.8" }
-deranged = { version = "0.4", default-features = false, features = ["powerfmt", "serde", "std"] }
-digest = { version = "0.10", features = ["mac", "oid", "std"] }
-either = { version = "1", features = ["serde", "use_std"] }
-euclid = { version = "0.22" }
-event-listener = { version = "5" }
-event-listener-strategy = { version = "0.5" }
-flate2 = { version = "1", features = ["zlib-rs"] }
-foldhash = { version = "0.1" }
-form_urlencoded = { version = "1" }
-futures = { version = "0.3", features = ["io-compat"] }
-futures-channel = { version = "0.3", features = ["sink"] }
-futures-core = { version = "0.3" }
-futures-executor = { version = "0.3" }
-futures-io = { version = "0.3" }
-futures-sink = { version = "0.3" }
-futures-task = { version = "0.3", default-features = false, features = ["std"] }
-futures-util = { version = "0.3", features = ["channel", "io-compat", "sink"] }
-getrandom-6f8ce4dd05d13bba = { package = "getrandom", version = "0.2", default-features = false, features = ["std"] }
-half = { version = "2", features = ["bytemuck", "num-traits", "rand_distr", "use-intrinsics"] }
-handlebars = { version = "4", features = ["rust-embed"] }
-hashbrown-3575ec1268b04181 = { package = "hashbrown", version = "0.15", features = ["rayon", "serde"] }
-hashbrown-582f2526e08bb6a0 = { package = "hashbrown", version = "0.14", features = ["raw"] }
-heck = { version = "0.4", features = ["unicode"] }
-hmac = { version = "0.12", default-features = false, features = ["reset"] }
-hyper = { version = "0.14", features = ["client", "http1", "http2", "runtime", "server", "stream"] }
-idna = { version = "1" }
-indexmap = { version = "2", features = ["serde"] }
-itertools-594e8ee84c453af0 = { package = "itertools", version = "0.13" }
-itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12" }
-lazy_static = { version = "1", default-features = false, features = ["spin_no_std"] }
-libc = { version = "0.2", features = ["extra_traits"] }
-libsqlite3-sys = { version = "0.30", features = ["bundled", "unlock_notify"] }
-log = { version = "0.4", default-features = false, features = ["kv_unstable_serde"] }
-lyon = { version = "1", default-features = false, features = ["extra"] }
-lyon_path = { version = "1" }
-md-5 = { version = "0.10" }
-memchr = { version = "2" }
-memmap2 = { version = "0.9", default-features = false, features = ["stable_deref_trait"] }
-mime_guess = { version = "2" }
-miniz_oxide = { version = "0.8", features = ["simd"] }
-nom = { version = "7" }
-num-bigint = { version = "0.4" }
-num-integer = { version = "0.1", features = ["i128"] }
-num-iter = { version = "0.1", default-features = false, features = ["i128", "std"] }
-num-rational = { version = "0.4", features = ["num-bigint-std"] }
-num-traits = { version = "0.2", features = ["i128", "libm"] }
-once_cell = { version = "1" }
-percent-encoding = { version = "2" }
-phf = { version = "0.11", features = ["macros"] }
-phf_shared = { version = "0.11" }
-prettyplease = { version = "0.2", default-features = false, features = ["verbatim"] }
-proc-macro2 = { version = "1" }
-prost-274715c4dabd11b0 = { package = "prost", version = "0.9" }
-prost-types = { version = "0.9" }
-quote = { version = "1" }
-rand-c38e5c1d305a1b54 = { package = "rand", version = "0.8", features = ["small_rng"] }
-rand_chacha = { version = "0.3", default-features = false, features = ["std"] }
-rand_core = { version = "0.6", default-features = false, features = ["std"] }
-rand_distr = { version = "0.5" }
-regalloc2 = { version = "0.11", features = ["checker", "enable-serde"] }
-regex = { version = "1" }
-regex-automata = { version = "0.4" }
-regex-syntax = { version = "0.8" }
-reqwest = { version = "0.12", default-features = false, features = ["blocking", "http2", "json", "rustls-tls-native-roots", "stream"] }
-ring = { version = "0.17", features = ["std"] }
-rust_decimal = { version = "1", default-features = false, features = ["maths", "serde", "std"] }
-rustc-hash = { version = "1" }
-rustix-dff4ba8e3ae991db = { package = "rustix", version = "1", features = ["fs", "net"] }
-rustls = { version = "0.23", features = ["ring"] }
-rustls-webpki = { version = "0.103", default-features = false, features = ["aws-lc-rs", "ring", "std"] }
-sea-orm = { version = "1", features = ["runtime-tokio-rustls", "sqlx-postgres", "sqlx-sqlite"] }
-sea-query-binder = { version = "0.7", default-features = false, features = ["postgres-array", "sqlx-postgres", "sqlx-sqlite", "with-bigdecimal", "with-chrono", "with-json", "with-rust_decimal", "with-time", "with-uuid"] }
-semver = { version = "1", features = ["serde"] }
-serde = { version = "1", features = ["alloc", "derive", "rc"] }
-serde_core = { version = "1", default-features = false, features = ["alloc", "rc", "result", "std"] }
-serde_json = { version = "1", features = ["alloc", "preserve_order", "raw_value", "unbounded_depth"] }
-simd-adler32 = { version = "0.3" }
-smallvec = { version = "1", default-features = false, features = ["const_new", "serde", "union"] }
-spin = { version = "0.9" }
-sqlx = { version = "0.8", features = ["bigdecimal", "chrono", "postgres", "runtime-tokio-rustls", "rust_decimal", "sqlite", "time", "uuid"] }
-sqlx-macros = { version = "0.8", features = ["_rt-tokio", "_tls-rustls-ring-webpki", "bigdecimal", "chrono", "derive", "json", "macros", "migrate", "postgres", "rust_decimal", "sqlite", "time", "uuid"] }
-sqlx-macros-core = { version = "0.8", features = ["_rt-tokio", "_tls-rustls-ring-webpki", "bigdecimal", "chrono", "derive", "json", "macros", "migrate", "postgres", "rust_decimal", "sqlite", "time", "uuid"] }
-sqlx-postgres = { version = "0.8", default-features = false, features = ["any", "bigdecimal", "chrono", "json", "migrate", "offline", "rust_decimal", "time", "uuid"] }
-sqlx-sqlite = { version = "0.8", default-features = false, features = ["any", "bundled", "chrono", "json", "migrate", "offline", "time", "uuid"] }
-stable_deref_trait = { version = "1" }
-strum = { version = "0.26", features = ["derive"] }
-subtle = { version = "2" }
-syn-dff4ba8e3ae991db = { package = "syn", version = "1", features = ["extra-traits", "full"] }
-syn-f595c2ba2a3f28df = { package = "syn", version = "2", features = ["extra-traits", "fold", "full", "visit", "visit-mut"] }
-thiserror = { version = "2" }
-time = { version = "0.3", features = ["local-offset", "macros", "serde-well-known"] }
-time-macros = { version = "0.2", default-features = false, features = ["formatting", "parsing", "serde"] }
-tokio = { version = "1", features = ["full"] }
-tokio-rustls = { version = "0.26", default-features = false, features = ["tls12"] }
-tokio-util = { version = "0.7", features = ["codec", "compat", "io-util", "rt"] }
-toml_datetime = { version = "0.6", default-features = false, features = ["serde"] }
-toml_edit = { version = "0.22", default-features = false, features = ["display", "parse", "serde"] }
-tracing = { version = "0.1", features = ["log"] }
-tracing-core = { version = "0.1" }
-tungstenite = { version = "0.26", default-features = false, features = ["__rustls-tls", "handshake"] }
-unicode-properties = { version = "0.1" }
-url = { version = "2", features = ["serde"] }
-uuid = { version = "1", features = ["serde", "v4", "v5", "v7"] }
-wasmparser = { version = "0.221" }
-wasmtime = { version = "29", default-features = false, features = ["async", "component-model", "cranelift", "demangle", "gc-drc", "incremental-cache", "parallel-compilation"] }
-wasmtime-cranelift = { version = "29", default-features = false, features = ["component-model", "gc-drc", "incremental-cache"] }
-wasmtime-environ = { version = "29", default-features = false, features = ["compile", "component-model", "demangle", "gc-drc"] }
-
-[target.x86_64-apple-darwin.dependencies]
-codespan-reporting = { version = "0.12" }
-core-foundation = { version = "0.9" }
-core-foundation-sys = { version = "0.8" }
-getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] }
-gimli = { version = "0.31", default-features = false, features = ["read", "std", "write"] }
-hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] }
-livekit-runtime = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "5f04705ac3f356350ae31534ffbc476abc9ea83d" }
-naga = { version = "25", features = ["msl-out", "wgsl-in"] }
-nix-b73a96c0a5f6a7d9 = { package = "nix", version = "0.29", features = ["fs", "pthread", "signal", "user"] }
-objc2 = { version = "0.6" }
-objc2-core-foundation = { version = "0.3", default-features = false, features = ["CFArray", "CFBase", "CFCGTypes", "CFData", "CFDate", "CFDictionary", "CFError", "CFNumber", "CFPlugInCOM", "CFRunLoop", "CFString", "CFURL", "CFUUID", "objc2", "std"] }
-objc2-foundation = { version = "0.3", default-features = false, features = ["NSArray", "NSAttributedString", "NSBundle", "NSCoder", "NSData", "NSDate", "NSDictionary", "NSEnumerator", "NSError", "NSGeometry", "NSNotification", "NSNull", "NSObjCRuntime", "NSObject", "NSProcessInfo", "NSRange", "NSRunLoop", "NSString", "NSURL", "NSUndoManager", "NSValue", "objc2-core-foundation", "std"] }
-objc2-metal = { version = "0.3" }
-object = { version = "0.36", default-features = false, features = ["archive", "read_core", "unaligned", "write"] }
-prost-5ef9efb8ec2df382 = { package = "prost", version = "0.12", features = ["prost-derive"] }
-rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", features = ["event", "mm", "net", "param", "process"] }
-rustix-dff4ba8e3ae991db = { package = "rustix", version = "1", default-features = false, features = ["event", "pipe", "process", "stdio", "termios", "time"] }
-scopeguard = { version = "1" }
-security-framework = { version = "3", features = ["OSX_10_14"] }
-security-framework-sys = { version = "2", features = ["OSX_10_14"] }
-tokio-rustls = { version = "0.26", default-features = false, features = ["logging", "ring"] }
-tokio-socks = { version = "0.5", features = ["futures-io"] }
-tokio-stream = { version = "0.1", features = ["fs"] }
-tower = { version = "0.5", default-features = false, features = ["timeout", "util"] }
-
-[target.x86_64-apple-darwin.build-dependencies]
-codespan-reporting = { version = "0.12" }
-core-foundation = { version = "0.9" }
-core-foundation-sys = { version = "0.8" }
-getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] }
-gimli = { version = "0.31", default-features = false, features = ["read", "std", "write"] }
-hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] }
-livekit-runtime = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "5f04705ac3f356350ae31534ffbc476abc9ea83d" }
-naga = { version = "25", features = ["msl-out", "wgsl-in"] }
-nix-b73a96c0a5f6a7d9 = { package = "nix", version = "0.29", features = ["fs", "pthread", "signal", "user"] }
-objc2 = { version = "0.6" }
-objc2-core-foundation = { version = "0.3", default-features = false, features = ["CFArray", "CFBase", "CFCGTypes", "CFData", "CFDate", "CFDictionary", "CFError", "CFNumber", "CFPlugInCOM", "CFRunLoop", "CFString", "CFURL", "CFUUID", "objc2", "std"] }
-objc2-foundation = { version = "0.3", default-features = false, features = ["NSArray", "NSAttributedString", "NSBundle", "NSCoder", "NSData", "NSDate", "NSDictionary", "NSEnumerator", "NSError", "NSGeometry", "NSNotification", "NSNull", "NSObjCRuntime", "NSObject", "NSProcessInfo", "NSRange", "NSRunLoop", "NSString", "NSURL", "NSUndoManager", "NSValue", "objc2-core-foundation", "std"] }
-objc2-metal = { version = "0.3" }
-object = { version = "0.36", default-features = false, features = ["archive", "read_core", "unaligned", "write"] }
-proc-macro2 = { version = "1", default-features = false, features = ["span-locations"] }
-prost-5ef9efb8ec2df382 = { package = "prost", version = "0.12", features = ["prost-derive"] }
-rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", features = ["event", "mm", "net", "param", "process"] }
-rustix-dff4ba8e3ae991db = { package = "rustix", version = "1", default-features = false, features = ["event", "pipe", "process", "stdio", "termios", "time"] }
-scopeguard = { version = "1" }
-security-framework = { version = "3", features = ["OSX_10_14"] }
-security-framework-sys = { version = "2", features = ["OSX_10_14"] }
-tokio-rustls = { version = "0.26", default-features = false, features = ["logging", "ring"] }
-tokio-socks = { version = "0.5", features = ["futures-io"] }
-tokio-stream = { version = "0.1", features = ["fs"] }
-tower = { version = "0.5", default-features = false, features = ["timeout", "util"] }
-
-[target.aarch64-apple-darwin.dependencies]
-codespan-reporting = { version = "0.12" }
-core-foundation = { version = "0.9" }
-core-foundation-sys = { version = "0.8" }
-getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] }
-gimli = { version = "0.31", default-features = false, features = ["read", "std", "write"] }
-hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] }
-livekit-runtime = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "5f04705ac3f356350ae31534ffbc476abc9ea83d" }
-naga = { version = "25", features = ["msl-out", "wgsl-in"] }
-nix-b73a96c0a5f6a7d9 = { package = "nix", version = "0.29", features = ["fs", "pthread", "signal", "user"] }
-objc2 = { version = "0.6" }
-objc2-core-foundation = { version = "0.3", default-features = false, features = ["CFArray", "CFBase", "CFCGTypes", "CFData", "CFDate", "CFDictionary", "CFError", "CFNumber", "CFPlugInCOM", "CFRunLoop", "CFString", "CFURL", "CFUUID", "objc2", "std"] }
-objc2-foundation = { version = "0.3", default-features = false, features = ["NSArray", "NSAttributedString", "NSBundle", "NSCoder", "NSData", "NSDate", "NSDictionary", "NSEnumerator", "NSError", "NSGeometry", "NSNotification", "NSNull", "NSObjCRuntime", "NSObject", "NSProcessInfo", "NSRange", "NSRunLoop", "NSString", "NSURL", "NSUndoManager", "NSValue", "objc2-core-foundation", "std"] }
-objc2-metal = { version = "0.3" }
-object = { version = "0.36", default-features = false, features = ["archive", "read_core", "unaligned", "write"] }
-prost-5ef9efb8ec2df382 = { package = "prost", version = "0.12", features = ["prost-derive"] }
-rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", features = ["event", "mm", "net", "param", "process"] }
-rustix-dff4ba8e3ae991db = { package = "rustix", version = "1", default-features = false, features = ["event", "pipe", "process", "stdio", "termios", "time"] }
-scopeguard = { version = "1" }
-security-framework = { version = "3", features = ["OSX_10_14"] }
-security-framework-sys = { version = "2", features = ["OSX_10_14"] }
-tokio-rustls = { version = "0.26", default-features = false, features = ["logging", "ring"] }
-tokio-socks = { version = "0.5", features = ["futures-io"] }
-tokio-stream = { version = "0.1", features = ["fs"] }
-tower = { version = "0.5", default-features = false, features = ["timeout", "util"] }
-
-[target.aarch64-apple-darwin.build-dependencies]
-codespan-reporting = { version = "0.12" }
-core-foundation = { version = "0.9" }
-core-foundation-sys = { version = "0.8" }
-getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] }
-gimli = { version = "0.31", default-features = false, features = ["read", "std", "write"] }
-hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] }
-livekit-runtime = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "5f04705ac3f356350ae31534ffbc476abc9ea83d" }
-naga = { version = "25", features = ["msl-out", "wgsl-in"] }
-nix-b73a96c0a5f6a7d9 = { package = "nix", version = "0.29", features = ["fs", "pthread", "signal", "user"] }
-objc2 = { version = "0.6" }
-objc2-core-foundation = { version = "0.3", default-features = false, features = ["CFArray", "CFBase", "CFCGTypes", "CFData", "CFDate", "CFDictionary", "CFError", "CFNumber", "CFPlugInCOM", "CFRunLoop", "CFString", "CFURL", "CFUUID", "objc2", "std"] }
-objc2-foundation = { version = "0.3", default-features = false, features = ["NSArray", "NSAttributedString", "NSBundle", "NSCoder", "NSData", "NSDate", "NSDictionary", "NSEnumerator", "NSError", "NSGeometry", "NSNotification", "NSNull", "NSObjCRuntime", "NSObject", "NSProcessInfo", "NSRange", "NSRunLoop", "NSString", "NSURL", "NSUndoManager", "NSValue", "objc2-core-foundation", "std"] }
-objc2-metal = { version = "0.3" }
-object = { version = "0.36", default-features = false, features = ["archive", "read_core", "unaligned", "write"] }
-proc-macro2 = { version = "1", default-features = false, features = ["span-locations"] }
-prost-5ef9efb8ec2df382 = { package = "prost", version = "0.12", features = ["prost-derive"] }
-rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", features = ["event", "mm", "net", "param", "process"] }
-rustix-dff4ba8e3ae991db = { package = "rustix", version = "1", default-features = false, features = ["event", "pipe", "process", "stdio", "termios", "time"] }
-scopeguard = { version = "1" }
-security-framework = { version = "3", features = ["OSX_10_14"] }
-security-framework-sys = { version = "2", features = ["OSX_10_14"] }
-tokio-rustls = { version = "0.26", default-features = false, features = ["logging", "ring"] }
-tokio-socks = { version = "0.5", features = ["futures-io"] }
-tokio-stream = { version = "0.1", features = ["fs"] }
-tower = { version = "0.5", default-features = false, features = ["timeout", "util"] }
-
-[target.x86_64-unknown-linux-gnu.dependencies]
-aes = { version = "0.8", default-features = false, features = ["zeroize"] }
-ahash = { version = "0.8", default-features = false, features = ["compile-time-rng"] }
-ashpd = { version = "0.11", default-features = false, features = ["async-std", "wayland"] }
-bytemuck = { version = "1", default-features = false, features = ["min_const_generics"] }
-cipher = { version = "0.4", default-features = false, features = ["block-padding", "rand_core", "zeroize"] }
-codespan-reporting = { version = "0.12" }
-crypto-common = { version = "0.1", default-features = false, features = ["rand_core", "std"] }
-flume = { version = "0.11" }
-getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] }
-getrandom-6f8ce4dd05d13bba = { package = "getrandom", version = "0.2", default-features = false, features = ["js", "rdrand"] }
-gimli = { version = "0.31", default-features = false, features = ["read", "std", "write"] }
-hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] }
-inout = { version = "0.1", default-features = false, features = ["block-padding"] }
-linux-raw-sys-274715c4dabd11b0 = { package = "linux-raw-sys", version = "0.9", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "xdp"] }
-linux-raw-sys-9fbad63c4bcf4a8f = { package = "linux-raw-sys", version = "0.4", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "system", "xdp"] }
-livekit-runtime = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "5f04705ac3f356350ae31534ffbc476abc9ea83d" }
-mio = { version = "1", features = ["net", "os-ext"] }
-naga = { version = "25", features = ["spv-out", "wgsl-in"] }
-nix-1f5adca70f036a62 = { package = "nix", version = "0.28", features = ["fs", "mman", "ptrace", "signal", "term", "user"] }
-nix-b73a96c0a5f6a7d9 = { package = "nix", version = "0.29", features = ["fs", "pthread", "signal", "user"] }
-nix-fa1f6196edfd7249 = { package = "nix", version = "0.30", features = ["fs", "socket", "uio", "user"] }
-num-bigint-dig = { version = "0.8", features = ["i128", "prime", "zeroize"] }
-num-complex = { version = "0.4", features = ["bytemuck"] }
-object = { version = "0.36", default-features = false, features = ["archive", "read_core", "unaligned", "write"] }
-proc-macro2 = { version = "1", features = ["span-locations"] }
-prost-5ef9efb8ec2df382 = { package = "prost", version = "0.12", features = ["prost-derive"] }
-quote = { version = "1" }
-rand-274715c4dabd11b0 = { package = "rand", version = "0.9" }
-rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", features = ["event", "mm", "net", "param", "pipe", "process", "shm", "system"] }
-rustix-dff4ba8e3ae991db = { package = "rustix", version = "1", default-features = false, features = ["event", "pipe", "process", "pty", "stdio", "termios", "time"] }
-scopeguard = { version = "1" }
-smallvec = { version = "1", default-features = false, features = ["write"] }
-syn-f595c2ba2a3f28df = { package = "syn", version = "2", features = ["extra-traits", "fold", "full", "visit", "visit-mut"] }
-tokio-rustls = { version = "0.26", default-features = false, features = ["logging", "ring"] }
-tokio-socks = { version = "0.5", features = ["futures-io"] }
-tokio-stream = { version = "0.1", features = ["fs"] }
-tower = { version = "0.5", default-features = false, features = ["timeout", "util"] }
-wayland-backend = { version = "0.3", default-features = false, features = ["client_system", "dlopen"] }
-wayland-sys = { version = "0.31", default-features = false, features = ["client", "dlopen"] }
-zeroize = { version = "1", features = ["zeroize_derive"] }
-zvariant = { version = "5", features = ["enumflags2", "gvariant", "url"] }
-
-[target.x86_64-unknown-linux-gnu.build-dependencies]
-aes = { version = "0.8", default-features = false, features = ["zeroize"] }
-ahash = { version = "0.8", default-features = false, features = ["compile-time-rng"] }
-ashpd = { version = "0.11", default-features = false, features = ["async-std", "wayland"] }
-bytemuck = { version = "1", default-features = false, features = ["min_const_generics"] }
-cipher = { version = "0.4", default-features = false, features = ["block-padding", "rand_core", "zeroize"] }
-codespan-reporting = { version = "0.12" }
-crypto-common = { version = "0.1", default-features = false, features = ["rand_core", "std"] }
-flume = { version = "0.11" }
-getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] }
-getrandom-6f8ce4dd05d13bba = { package = "getrandom", version = "0.2", default-features = false, features = ["js", "rdrand"] }
-gimli = { version = "0.31", default-features = false, features = ["read", "std", "write"] }
-hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] }
-inout = { version = "0.1", default-features = false, features = ["block-padding"] }
-linux-raw-sys-274715c4dabd11b0 = { package = "linux-raw-sys", version = "0.9", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "xdp"] }
-linux-raw-sys-9fbad63c4bcf4a8f = { package = "linux-raw-sys", version = "0.4", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "system", "xdp"] }
-livekit-runtime = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "5f04705ac3f356350ae31534ffbc476abc9ea83d" }
-mio = { version = "1", features = ["net", "os-ext"] }
-naga = { version = "25", features = ["spv-out", "wgsl-in"] }
-nix-1f5adca70f036a62 = { package = "nix", version = "0.28", features = ["fs", "mman", "ptrace", "signal", "term", "user"] }
-nix-b73a96c0a5f6a7d9 = { package = "nix", version = "0.29", features = ["fs", "pthread", "signal", "user"] }
-nix-fa1f6196edfd7249 = { package = "nix", version = "0.30", features = ["fs", "socket", "uio", "user"] }
-num-bigint-dig = { version = "0.8", features = ["i128", "prime", "zeroize"] }
-num-complex = { version = "0.4", features = ["bytemuck"] }
-object = { version = "0.36", default-features = false, features = ["archive", "read_core", "unaligned", "write"] }
-proc-macro2 = { version = "1", default-features = false, features = ["span-locations"] }
-prost-5ef9efb8ec2df382 = { package = "prost", version = "0.12", features = ["prost-derive"] }
-rand-274715c4dabd11b0 = { package = "rand", version = "0.9" }
-rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", features = ["event", "mm", "net", "param", "pipe", "process", "shm", "system"] }
-rustix-dff4ba8e3ae991db = { package = "rustix", version = "1", default-features = false, features = ["event", "pipe", "process", "pty", "stdio", "termios", "time"] }
-scopeguard = { version = "1" }
-smallvec = { version = "1", default-features = false, features = ["write"] }
-tokio-rustls = { version = "0.26", default-features = false, features = ["logging", "ring"] }
-tokio-socks = { version = "0.5", features = ["futures-io"] }
-tokio-stream = { version = "0.1", features = ["fs"] }
-tower = { version = "0.5", default-features = false, features = ["timeout", "util"] }
-wayland-backend = { version = "0.3", default-features = false, features = ["client_system", "dlopen"] }
-wayland-sys = { version = "0.31", default-features = false, features = ["client", "dlopen"] }
-zbus_macros = { version = "5", features = ["gvariant"] }
-zeroize = { version = "1", features = ["zeroize_derive"] }
-zvariant = { version = "5", features = ["enumflags2", "gvariant", "url"] }
-
-[target.aarch64-unknown-linux-gnu.dependencies]
-aes = { version = "0.8", default-features = false, features = ["zeroize"] }
-ahash = { version = "0.8", default-features = false, features = ["compile-time-rng"] }
-ashpd = { version = "0.11", default-features = false, features = ["async-std", "wayland"] }
-bytemuck = { version = "1", default-features = false, features = ["min_const_generics"] }
-cipher = { version = "0.4", default-features = false, features = ["block-padding", "rand_core", "zeroize"] }
-codespan-reporting = { version = "0.12" }
-crypto-common = { version = "0.1", default-features = false, features = ["rand_core", "std"] }
-flume = { version = "0.11" }
-getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] }
-getrandom-6f8ce4dd05d13bba = { package = "getrandom", version = "0.2", default-features = false, features = ["js", "rdrand"] }
-gimli = { version = "0.31", default-features = false, features = ["read", "std", "write"] }
-hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] }
-inout = { version = "0.1", default-features = false, features = ["block-padding"] }
-linux-raw-sys-274715c4dabd11b0 = { package = "linux-raw-sys", version = "0.9", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "xdp"] }
-linux-raw-sys-9fbad63c4bcf4a8f = { package = "linux-raw-sys", version = "0.4", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "system", "xdp"] }
-livekit-runtime = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "5f04705ac3f356350ae31534ffbc476abc9ea83d" }
-mio = { version = "1", features = ["net", "os-ext"] }
-naga = { version = "25", features = ["spv-out", "wgsl-in"] }
-nix-1f5adca70f036a62 = { package = "nix", version = "0.28", features = ["fs", "mman", "ptrace", "signal", "term", "user"] }
-nix-b73a96c0a5f6a7d9 = { package = "nix", version = "0.29", features = ["fs", "pthread", "signal", "user"] }
-nix-fa1f6196edfd7249 = { package = "nix", version = "0.30", features = ["fs", "socket", "uio", "user"] }
-num-bigint-dig = { version = "0.8", features = ["i128", "prime", "zeroize"] }
-num-complex = { version = "0.4", features = ["bytemuck"] }
-object = { version = "0.36", default-features = false, features = ["archive", "read_core", "unaligned", "write"] }
-proc-macro2 = { version = "1", features = ["span-locations"] }
-prost-5ef9efb8ec2df382 = { package = "prost", version = "0.12", features = ["prost-derive"] }
-quote = { version = "1" }
-rand-274715c4dabd11b0 = { package = "rand", version = "0.9" }
-rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", features = ["event", "mm", "net", "param", "pipe", "process", "shm", "system"] }
-rustix-dff4ba8e3ae991db = { package = "rustix", version = "1", default-features = false, features = ["event", "pipe", "process", "pty", "stdio", "termios", "time"] }
-scopeguard = { version = "1" }
-smallvec = { version = "1", default-features = false, features = ["write"] }
-syn-f595c2ba2a3f28df = { package = "syn", version = "2", features = ["extra-traits", "fold", "full", "visit", "visit-mut"] }
-tokio-rustls = { version = "0.26", default-features = false, features = ["logging", "ring"] }
-tokio-socks = { version = "0.5", features = ["futures-io"] }
-tokio-stream = { version = "0.1", features = ["fs"] }
-tower = { version = "0.5", default-features = false, features = ["timeout", "util"] }
-wayland-backend = { version = "0.3", default-features = false, features = ["client_system", "dlopen"] }
-wayland-sys = { version = "0.31", default-features = false, features = ["client", "dlopen"] }
-zeroize = { version = "1", features = ["zeroize_derive"] }
-zvariant = { version = "5", features = ["enumflags2", "gvariant", "url"] }
-
-[target.aarch64-unknown-linux-gnu.build-dependencies]
-aes = { version = "0.8", default-features = false, features = ["zeroize"] }
-ahash = { version = "0.8", default-features = false, features = ["compile-time-rng"] }
-ashpd = { version = "0.11", default-features = false, features = ["async-std", "wayland"] }
-bytemuck = { version = "1", default-features = false, features = ["min_const_generics"] }
-cipher = { version = "0.4", default-features = false, features = ["block-padding", "rand_core", "zeroize"] }
-codespan-reporting = { version = "0.12" }
-crypto-common = { version = "0.1", default-features = false, features = ["rand_core", "std"] }
-flume = { version = "0.11" }
-getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] }
-getrandom-6f8ce4dd05d13bba = { package = "getrandom", version = "0.2", default-features = false, features = ["js", "rdrand"] }
-gimli = { version = "0.31", default-features = false, features = ["read", "std", "write"] }
-hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] }
-inout = { version = "0.1", default-features = false, features = ["block-padding"] }
-linux-raw-sys-274715c4dabd11b0 = { package = "linux-raw-sys", version = "0.9", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "xdp"] }
-linux-raw-sys-9fbad63c4bcf4a8f = { package = "linux-raw-sys", version = "0.4", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "system", "xdp"] }
-livekit-runtime = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "5f04705ac3f356350ae31534ffbc476abc9ea83d" }
-mio = { version = "1", features = ["net", "os-ext"] }
-naga = { version = "25", features = ["spv-out", "wgsl-in"] }
-nix-1f5adca70f036a62 = { package = "nix", version = "0.28", features = ["fs", "mman", "ptrace", "signal", "term", "user"] }
-nix-b73a96c0a5f6a7d9 = { package = "nix", version = "0.29", features = ["fs", "pthread", "signal", "user"] }
-nix-fa1f6196edfd7249 = { package = "nix", version = "0.30", features = ["fs", "socket", "uio", "user"] }
-num-bigint-dig = { version = "0.8", features = ["i128", "prime", "zeroize"] }
-num-complex = { version = "0.4", features = ["bytemuck"] }
-object = { version = "0.36", default-features = false, features = ["archive", "read_core", "unaligned", "write"] }
-proc-macro2 = { version = "1", default-features = false, features = ["span-locations"] }
-prost-5ef9efb8ec2df382 = { package = "prost", version = "0.12", features = ["prost-derive"] }
-rand-274715c4dabd11b0 = { package = "rand", version = "0.9" }
-rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", features = ["event", "mm", "net", "param", "pipe", "process", "shm", "system"] }
-rustix-dff4ba8e3ae991db = { package = "rustix", version = "1", default-features = false, features = ["event", "pipe", "process", "pty", "stdio", "termios", "time"] }
-scopeguard = { version = "1" }
-smallvec = { version = "1", default-features = false, features = ["write"] }
-tokio-rustls = { version = "0.26", default-features = false, features = ["logging", "ring"] }
-tokio-socks = { version = "0.5", features = ["futures-io"] }
-tokio-stream = { version = "0.1", features = ["fs"] }
-tower = { version = "0.5", default-features = false, features = ["timeout", "util"] }
-wayland-backend = { version = "0.3", default-features = false, features = ["client_system", "dlopen"] }
-wayland-sys = { version = "0.31", default-features = false, features = ["client", "dlopen"] }
-zbus_macros = { version = "5", features = ["gvariant"] }
-zeroize = { version = "1", features = ["zeroize_derive"] }
-zvariant = { version = "5", features = ["enumflags2", "gvariant", "url"] }
-
-[target.x86_64-pc-windows-msvc.dependencies]
-flume = { version = "0.11" }
-getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] }
-getrandom-6f8ce4dd05d13bba = { package = "getrandom", version = "0.2", default-features = false, features = ["js", "rdrand"] }
-hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] }
-livekit-runtime = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "5f04705ac3f356350ae31534ffbc476abc9ea83d" }
-prost-5ef9efb8ec2df382 = { package = "prost", version = "0.12", features = ["prost-derive"] }
-rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", features = ["event", "fs", "net"] }
-scopeguard = { version = "1" }
-tokio-rustls = { version = "0.26", default-features = false, features = ["logging", "ring"] }
-tokio-socks = { version = "0.5", features = ["futures-io"] }
-tokio-stream = { version = "0.1", features = ["fs"] }
-tower = { version = "0.5", default-features = false, features = ["timeout", "util"] }
-winapi = { version = "0.3", default-features = false, features = ["cfg", "commapi", "consoleapi", "evntrace", "fileapi", "handleapi", "impl-debug", "impl-default", "in6addr", "inaddr", "ioapiset", "knownfolders", "minwinbase", "minwindef", "namedpipeapi", "ntsecapi", "objbase", "processenv", "processthreadsapi", "shlobj", "std", "synchapi", "sysinfoapi", "timezoneapi", "winbase", "windef", "winerror", "winioctl", "winnt", "winreg", "winsock2", "winuser"] }

tooling/xtask/Cargo.toml 🔗

@@ -16,4 +16,3 @@ clap = { workspace = true, features = ["derive"] }
 toml.workspace = true
 indoc.workspace = true
 toml_edit.workspace = true
-workspace-hack.workspace = true

tooling/xtask/src/tasks/package_conformity.rs 🔗

@@ -38,11 +38,6 @@ pub fn run_package_conformity(_args: PackageConformityArgs) -> Result<()> {
             continue;
         }
 
-        // Ignore `workspace-hack`, as it produces a lot of false positives.
-        if package.name == "workspace-hack" {
-            continue;
-        }
-
         for dependencies in [
             &cargo_toml.dependencies,
             &cargo_toml.dev_dependencies,