Merge branch 'main' into sumtree-v10000

Piotr Osiewicz created

Change summary

.config/hakari.toml                                                                                         |    3 
.github/ISSUE_TEMPLATE/07_bug_windows_alpha.yml                                                             |   35 
.github/actionlint.yml                                                                                      |    3 
.github/actions/run_tests_windows/action.yml                                                                |  162 
.github/workflows/ci.yml                                                                                    |   45 
.github/workflows/release_nightly.yml                                                                       |   15 
.github/workflows/unit_evals.yml                                                                            |    2 
Cargo.lock                                                                                                  |  416 
Cargo.toml                                                                                                  |   73 
Dockerfile-collab                                                                                           |    2 
assets/fonts/ibm-plex-sans/IBMPlexSans-Bold.ttf                                                             |    0 
assets/fonts/ibm-plex-sans/IBMPlexSans-BoldItalic.ttf                                                       |    0 
assets/fonts/ibm-plex-sans/IBMPlexSans-Italic.ttf                                                           |    0 
assets/fonts/ibm-plex-sans/IBMPlexSans-Regular.ttf                                                          |    0 
assets/fonts/ibm-plex-sans/license.txt                                                                      |    0 
assets/fonts/lilex/Lilex-Bold.ttf                                                                           |    0 
assets/fonts/lilex/Lilex-BoldItalic.ttf                                                                     |    0 
assets/fonts/lilex/Lilex-Italic.ttf                                                                         |    0 
assets/fonts/lilex/Lilex-Regular.ttf                                                                        |    0 
assets/fonts/lilex/OFL.txt                                                                                  |    7 
assets/fonts/plex-mono/ZedPlexMono-Bold.ttf                                                                 |    0 
assets/fonts/plex-mono/ZedPlexMono-BoldItalic.ttf                                                           |    0 
assets/fonts/plex-mono/ZedPlexMono-Italic.ttf                                                               |    0 
assets/fonts/plex-mono/ZedPlexMono-Regular.ttf                                                              |    0 
assets/fonts/plex-sans/ZedPlexSans-Bold.ttf                                                                 |    0 
assets/fonts/plex-sans/ZedPlexSans-BoldItalic.ttf                                                           |    0 
assets/fonts/plex-sans/ZedPlexSans-Italic.ttf                                                               |    0 
assets/fonts/plex-sans/ZedPlexSans-Regular.ttf                                                              |    0 
assets/icons/ai.svg                                                                                         |    2 
assets/icons/arrow_circle.svg                                                                               |    8 
assets/icons/arrow_down.svg                                                                                 |    2 
assets/icons/arrow_down10.svg                                                                               |    2 
assets/icons/arrow_down_from_line.svg                                                                       |    1 
assets/icons/arrow_down_right.svg                                                                           |    5 
assets/icons/arrow_left.svg                                                                                 |    2 
assets/icons/arrow_right.svg                                                                                |    2 
assets/icons/arrow_right_left.svg                                                                           |    7 
assets/icons/arrow_up.svg                                                                                   |    2 
assets/icons/arrow_up_alt.svg                                                                               |    3 
assets/icons/arrow_up_from_line.svg                                                                         |    1 
assets/icons/arrow_up_right.svg                                                                             |    5 
assets/icons/arrow_up_right_alt.svg                                                                         |    3 
assets/icons/audio_off.svg                                                                                  |   10 
assets/icons/audio_on.svg                                                                                   |    6 
assets/icons/backspace.svg                                                                                  |    6 
assets/icons/bell.svg                                                                                       |    4 
assets/icons/bell_dot.svg                                                                                   |    4 
assets/icons/bell_off.svg                                                                                   |    6 
assets/icons/bell_ring.svg                                                                                  |    8 
assets/icons/binary.svg                                                                                     |    2 
assets/icons/blocks.svg                                                                                     |    2 
assets/icons/bolt_outlined.svg                                                                              |    2 
assets/icons/book.svg                                                                                       |    2 
assets/icons/book_copy.svg                                                                                  |    2 
assets/icons/bug_off.svg                                                                                    |    1 
assets/icons/caret_down.svg                                                                                 |    8 
assets/icons/caret_up.svg                                                                                   |    8 
assets/icons/case_sensitive.svg                                                                             |    5 
assets/icons/chat.svg                                                                                       |    4 
assets/icons/check.svg                                                                                      |    2 
assets/icons/check_circle.svg                                                                               |    6 
assets/icons/check_double.svg                                                                               |    5 
assets/icons/chevron_down.svg                                                                               |    4 
assets/icons/chevron_down_small.svg                                                                         |    3 
assets/icons/chevron_left.svg                                                                               |    4 
assets/icons/chevron_right.svg                                                                              |    4 
assets/icons/chevron_up.svg                                                                                 |    4 
assets/icons/chevron_up_down.svg                                                                            |    5 
assets/icons/circle.svg                                                                                     |    4 
assets/icons/circle_check.svg                                                                               |    2 
assets/icons/circle_help.svg                                                                                |    6 
assets/icons/circle_off.svg                                                                                 |    1 
assets/icons/close.svg                                                                                      |    4 
assets/icons/cloud.svg                                                                                      |    1 
assets/icons/cloud_download.svg                                                                             |    2 
assets/icons/code.svg                                                                                       |    2 
assets/icons/cog.svg                                                                                        |    2 
assets/icons/command.svg                                                                                    |    0 
assets/icons/context.svg                                                                                    |    6 
assets/icons/control.svg                                                                                    |    2 
assets/icons/copilot.svg                                                                                    |   16 
assets/icons/copilot_disabled.svg                                                                           |    0 
assets/icons/copilot_error.svg                                                                              |    0 
assets/icons/copilot_init.svg                                                                               |    0 
assets/icons/copy.svg                                                                                       |    5 
assets/icons/countdown_timer.svg                                                                            |    2 
assets/icons/crosshair.svg                                                                                  |   12 
assets/icons/cursor_i_beam.svg                                                                              |    4 
assets/icons/dash.svg                                                                                       |    2 
assets/icons/database_zap.svg                                                                               |    2 
assets/icons/debug.svg                                                                                      |   20 
assets/icons/debug_breakpoint.svg                                                                           |    4 
assets/icons/debug_continue.svg                                                                             |    2 
assets/icons/debug_detach.svg                                                                               |    2 
assets/icons/debug_disabled_breakpoint.svg                                                                  |    4 
assets/icons/debug_disabled_log_breakpoint.svg                                                              |    6 
assets/icons/debug_ignore_breakpoints.svg                                                                   |    4 
assets/icons/debug_log_breakpoint.svg                                                                       |    4 
assets/icons/debug_pause.svg                                                                                |    5 
assets/icons/debug_restart.svg                                                                              |    1 
assets/icons/debug_step_back.svg                                                                            |    2 
assets/icons/debug_step_into.svg                                                                            |    6 
assets/icons/debug_step_out.svg                                                                             |    6 
assets/icons/debug_step_over.svg                                                                            |    6 
assets/icons/debug_stop.svg                                                                                 |    1 
assets/icons/delete.svg                                                                                     |    1 
assets/icons/diff.svg                                                                                       |    2 
assets/icons/disconnected.svg                                                                               |    4 
assets/icons/document_text.svg                                                                              |    3 
assets/icons/download.svg                                                                                   |    2 
assets/icons/ellipsis.svg                                                                                   |    8 
assets/icons/ellipsis_vertical.svg                                                                          |    6 
assets/icons/envelope.svg                                                                                   |    4 
assets/icons/equal.svg                                                                                      |    1 
assets/icons/eraser.svg                                                                                     |    5 
assets/icons/escape.svg                                                                                     |    2 
assets/icons/exit.svg                                                                                       |    6 
assets/icons/expand_down.svg                                                                                |    6 
assets/icons/expand_up.svg                                                                                  |    6 
assets/icons/expand_vertical.svg                                                                            |    2 
assets/icons/external_link.svg                                                                              |    5 
assets/icons/eye.svg                                                                                        |    5 
assets/icons/file.svg                                                                                       |    5 
assets/icons/file_code.svg                                                                                  |    2 
assets/icons/file_create.svg                                                                                |    5 
assets/icons/file_diff.svg                                                                                  |    2 
assets/icons/file_doc.svg                                                                                   |    6 
assets/icons/file_generic.svg                                                                               |    6 
assets/icons/file_git.svg                                                                                   |    8 
assets/icons/file_icons/ai.svg                                                                              |    2 
assets/icons/file_icons/audio.svg                                                                           |   12 
assets/icons/file_icons/book.svg                                                                            |    6 
assets/icons/file_icons/bun.svg                                                                             |    2 
assets/icons/file_icons/chevron_down.svg                                                                    |    2 
assets/icons/file_icons/chevron_left.svg                                                                    |    2 
assets/icons/file_icons/chevron_right.svg                                                                   |    2 
assets/icons/file_icons/chevron_up.svg                                                                      |    2 
assets/icons/file_icons/code.svg                                                                            |    4 
assets/icons/file_icons/coffeescript.svg                                                                    |    2 
assets/icons/file_icons/conversations.svg                                                                   |    2 
assets/icons/file_icons/dart.svg                                                                            |    2 
assets/icons/file_icons/database.svg                                                                        |    6 
assets/icons/file_icons/diff.svg                                                                            |    6 
assets/icons/file_icons/eslint.svg                                                                          |    2 
assets/icons/file_icons/file.svg                                                                            |    6 
assets/icons/file_icons/folder.svg                                                                          |    2 
assets/icons/file_icons/folder_open.svg                                                                     |    4 
assets/icons/file_icons/font.svg                                                                            |    2 
assets/icons/file_icons/git.svg                                                                             |    8 
assets/icons/file_icons/gleam.svg                                                                           |    4 
assets/icons/file_icons/graphql.svg                                                                         |    4 
assets/icons/file_icons/hash.svg                                                                            |    8 
assets/icons/file_icons/heroku.svg                                                                          |    2 
assets/icons/file_icons/html.svg                                                                            |    6 
assets/icons/file_icons/image.svg                                                                           |    6 
assets/icons/file_icons/java.svg                                                                            |   10 
assets/icons/file_icons/lock.svg                                                                            |    2 
assets/icons/file_icons/magnifying_glass.svg                                                                |    2 
assets/icons/file_icons/nix.svg                                                                             |   12 
assets/icons/file_icons/notebook.svg                                                                        |   10 
assets/icons/file_icons/package.svg                                                                         |    2 
assets/icons/file_icons/phoenix.svg                                                                         |    2 
assets/icons/file_icons/plus.svg                                                                            |    2 
assets/icons/file_icons/prettier.svg                                                                        |   20 
assets/icons/file_icons/project.svg                                                                         |    2 
assets/icons/file_icons/python.svg                                                                          |    4 
assets/icons/file_icons/replace.svg                                                                         |    2 
assets/icons/file_icons/replace_next.svg                                                                    |    2 
assets/icons/file_icons/rust.svg                                                                            |    2 
assets/icons/file_icons/scala.svg                                                                           |    2 
assets/icons/file_icons/settings.svg                                                                        |    0 
assets/icons/file_icons/tcl.svg                                                                             |    2 
assets/icons/file_icons/toml.svg                                                                            |    6 
assets/icons/file_icons/video.svg                                                                           |    4 
assets/icons/file_icons/vue.svg                                                                             |    2 
assets/icons/file_lock.svg                                                                                  |    2 
assets/icons/file_markdown.svg                                                                              |    1 
assets/icons/file_rust.svg                                                                                  |    2 
assets/icons/file_search.svg                                                                                |    5 
assets/icons/file_text.svg                                                                                  |    6 
assets/icons/file_text_filled.svg                                                                           |    3 
assets/icons/file_text_outlined.svg                                                                         |    6 
assets/icons/file_toml.svg                                                                                  |    6 
assets/icons/file_tree.svg                                                                                  |    6 
assets/icons/filter.svg                                                                                     |    2 
assets/icons/flame.svg                                                                                      |    2 
assets/icons/folder.svg                                                                                     |    2 
assets/icons/folder_open.svg                                                                                |    4 
assets/icons/folder_search.svg                                                                              |    5 
assets/icons/folder_x.svg                                                                                   |    2 
assets/icons/font.svg                                                                                       |    2 
assets/icons/font_size.svg                                                                                  |    2 
assets/icons/font_weight.svg                                                                                |    2 
assets/icons/forward_arrow.svg                                                                              |    5 
assets/icons/function.svg                                                                                   |    1 
assets/icons/generic_maximize.svg                                                                           |    2 
assets/icons/generic_restore.svg                                                                            |    4 
assets/icons/git_branch.svg                                                                                 |    2 
assets/icons/git_branch_alt.svg                                                                             |    7 
assets/icons/git_branch_small.svg                                                                           |    7 
assets/icons/github.svg                                                                                     |    2 
assets/icons/globe.svg                                                                                      |   12 
assets/icons/hammer.svg                                                                                     |    1 
assets/icons/hash.svg                                                                                       |    7 
assets/icons/history_rerun.svg                                                                              |    6 
assets/icons/image.svg                                                                                      |    2 
assets/icons/info.svg                                                                                       |    4 
assets/icons/inlay_hint.svg                                                                                 |    5 
assets/icons/json.svg                                                                                       |    4 
assets/icons/keyboard.svg                                                                                   |    2 
assets/icons/knockouts/x_fg.svg                                                                             |    2 
assets/icons/layout.svg                                                                                     |    5 
assets/icons/library.svg                                                                                    |    7 
assets/icons/light_bulb.svg                                                                                 |    3 
assets/icons/line_height.svg                                                                                |    7 
assets/icons/link.svg                                                                                       |    1 
assets/icons/list_collapse.svg                                                                              |    2 
assets/icons/list_todo.svg                                                                                  |    2 
assets/icons/list_tree.svg                                                                                  |   10 
assets/icons/list_x.svg                                                                                     |   10 
assets/icons/load_circle.svg                                                                                |    2 
assets/icons/location_edit.svg                                                                              |    2 
assets/icons/lock_outlined.svg                                                                              |    6 
assets/icons/logo_96.svg                                                                                    |    3 
assets/icons/lsp_debug.svg                                                                                  |   12 
assets/icons/lsp_restart.svg                                                                                |    4 
assets/icons/lsp_stop.svg                                                                                   |    4 
assets/icons/magnifying_glass.svg                                                                           |    3 
assets/icons/mail_open.svg                                                                                  |    1 
assets/icons/maximize.svg                                                                                   |    7 
assets/icons/menu.svg                                                                                       |    2 
assets/icons/menu_alt.svg                                                                                   |    6 
assets/icons/menu_alt_temp.svg                                                                              |    3 
assets/icons/mic.svg                                                                                        |    6 
assets/icons/mic_mute.svg                                                                                   |   12 
assets/icons/minimize.svg                                                                                   |    7 
assets/icons/notepad.svg                                                                                    |    1 
assets/icons/option.svg                                                                                     |    3 
assets/icons/panel_left.svg                                                                                 |    1 
assets/icons/panel_right.svg                                                                                |    1 
assets/icons/pencil.svg                                                                                     |    5 
assets/icons/person.svg                                                                                     |    5 
assets/icons/person_circle.svg                                                                              |    1 
assets/icons/phone_incoming.svg                                                                             |    1 
assets/icons/pin.svg                                                                                        |    1 
assets/icons/play_filled.svg                                                                                |    2 
assets/icons/play_outlined.svg                                                                              |    2 
assets/icons/plus.svg                                                                                       |    4 
assets/icons/pocket_knife.svg                                                                               |    1 
assets/icons/power.svg                                                                                      |    2 
assets/icons/public.svg                                                                                     |    4 
assets/icons/pull_request.svg                                                                               |    2 
assets/icons/quote.svg                                                                                      |    2 
assets/icons/reader.svg                                                                                     |    5 
assets/icons/refresh_title.svg                                                                              |    6 
assets/icons/regex.svg                                                                                      |    6 
assets/icons/repl_neutral.svg                                                                               |   15 
assets/icons/repl_off.svg                                                                                   |   29 
assets/icons/repl_pause.svg                                                                                 |   21 
assets/icons/repl_play.svg                                                                                  |   19 
assets/icons/replace.svg                                                                                    |    2 
assets/icons/replace_next.svg                                                                               |    2 
assets/icons/rerun.svg                                                                                      |    8 
assets/icons/return.svg                                                                                     |    5 
assets/icons/rotate_ccw.svg                                                                                 |    2 
assets/icons/rotate_cw.svg                                                                                  |    5 
assets/icons/route.svg                                                                                      |    1 
assets/icons/save.svg                                                                                       |    1 
assets/icons/scissors.svg                                                                                   |    4 
assets/icons/screen.svg                                                                                     |    6 
assets/icons/scroll_text.svg                                                                                |    1 
assets/icons/search_selection.svg                                                                           |    1 
assets/icons/select_all.svg                                                                                 |    2 
assets/icons/send.svg                                                                                       |    5 
assets/icons/server.svg                                                                                     |   20 
assets/icons/settings.svg                                                                                   |    0 
assets/icons/settings_alt.svg                                                                               |    6 
assets/icons/shield_check.svg                                                                               |    4 
assets/icons/shift.svg                                                                                      |    2 
assets/icons/slash.svg                                                                                      |    4 
assets/icons/slash_square.svg                                                                               |    1 
assets/icons/sliders.svg                                                                                    |   12 
assets/icons/sliders_alt.svg                                                                                |    6 
assets/icons/sliders_vertical.svg                                                                           |   11 
assets/icons/snip.svg                                                                                       |    1 
assets/icons/space.svg                                                                                      |    4 
assets/icons/sparkle.svg                                                                                    |    2 
assets/icons/sparkle_alt.svg                                                                                |    3 
assets/icons/sparkle_filled.svg                                                                             |    1 
assets/icons/speaker_loud.svg                                                                               |    4 
assets/icons/split.svg                                                                                      |    8 
assets/icons/split_alt.svg                                                                                  |    2 
assets/icons/square_dot.svg                                                                                 |    5 
assets/icons/square_minus.svg                                                                               |    5 
assets/icons/square_plus.svg                                                                                |    6 
assets/icons/star.svg                                                                                       |    0 
assets/icons/star_filled.svg                                                                                |    0 
assets/icons/stop.svg                                                                                       |    4 
assets/icons/stop_filled.svg                                                                                |    3 
assets/icons/supermaven.svg                                                                                 |   14 
assets/icons/supermaven_disabled.svg                                                                        |   16 
assets/icons/supermaven_error.svg                                                                           |   14 
assets/icons/supermaven_init.svg                                                                            |   14 
assets/icons/swatch_book.svg                                                                                |    2 
assets/icons/tab.svg                                                                                        |    6 
assets/icons/terminal_alt.svg                                                                               |    6 
assets/icons/text_snippet.svg                                                                               |    2 
assets/icons/text_thread.svg                                                                                |   10 
assets/icons/thread.svg                                                                                     |    2 
assets/icons/thread_from_summary.svg                                                                        |    8 
assets/icons/thumbs_down.svg                                                                                |    4 
assets/icons/thumbs_up.svg                                                                                  |    4 
assets/icons/todo_complete.svg                                                                              |    5 
assets/icons/todo_pending.svg                                                                               |   16 
assets/icons/todo_progress.svg                                                                              |   18 
assets/icons/tool_copy.svg                                                                                  |    4 
assets/icons/tool_delete_file.svg                                                                           |    6 
assets/icons/tool_diagnostics.svg                                                                           |    6 
assets/icons/tool_folder.svg                                                                                |    2 
assets/icons/tool_hammer.svg                                                                                |    6 
assets/icons/tool_notification.svg                                                                          |    4 
assets/icons/tool_pencil.svg                                                                                |    4 
assets/icons/tool_read.svg                                                                                  |   10 
assets/icons/tool_regex.svg                                                                                 |    2 
assets/icons/tool_search.svg                                                                                |    4 
assets/icons/tool_terminal.svg                                                                              |    6 
assets/icons/tool_think.svg                                                                                 |    2 
assets/icons/tool_web.svg                                                                                   |    6 
assets/icons/trash.svg                                                                                      |    6 
assets/icons/triangle.svg                                                                                   |    4 
assets/icons/triangle_right.svg                                                                             |    4 
assets/icons/undo.svg                                                                                       |    2 
assets/icons/update.svg                                                                                     |    4 
assets/icons/user_check.svg                                                                                 |    2 
assets/icons/user_group.svg                                                                                 |    6 
assets/icons/user_round_pen.svg                                                                             |    2 
assets/icons/visible.svg                                                                                    |    1 
assets/icons/wand.svg                                                                                       |    1 
assets/icons/warning.svg                                                                                    |    2 
assets/icons/whole_word.svg                                                                                 |    2 
assets/icons/x.svg                                                                                          |    3 
assets/icons/x_circle.svg                                                                                   |    5 
assets/icons/x_circle_filled.svg                                                                            |    3 
assets/icons/zed_agent.svg                                                                                  |   27 
assets/icons/zed_assistant.svg                                                                              |    6 
assets/icons/zed_assistant_filled.svg                                                                       |    5 
assets/icons/zed_burn_mode.svg                                                                              |    4 
assets/icons/zed_burn_mode_on.svg                                                                           |   14 
assets/icons/zed_mcp_custom.svg                                                                             |    2 
assets/icons/zed_mcp_extension.svg                                                                          |    2 
assets/icons/zed_predict.svg                                                                                |    6 
assets/icons/zed_predict_down.svg                                                                           |    6 
assets/icons/zed_predict_error.svg                                                                          |    4 
assets/icons/zed_predict_up.svg                                                                             |    6 
assets/icons/zed_x_copilot.svg                                                                              |    3 
assets/keymaps/default-linux.json                                                                           |   22 
assets/keymaps/default-macos.json                                                                           |   22 
assets/keymaps/linux/cursor.json                                                                            |    4 
assets/keymaps/macos/cursor.json                                                                            |    4 
assets/keymaps/vim.json                                                                                     |    9 
assets/settings/default.json                                                                                |   50 
assets/themes/one/one.json                                                                                  |    6 
crates/acp_thread/Cargo.toml                                                                                |   12 
crates/acp_thread/src/acp_thread.rs                                                                         |  704 
crates/acp_thread/src/connection.rs                                                                         |  429 
crates/acp_thread/src/diff.rs                                                                               |  385 
crates/acp_thread/src/mention.rs                                                                            |  438 
crates/acp_thread/src/terminal.rs                                                                           |   93 
crates/action_log/Cargo.toml                                                                                |   45 
crates/action_log/LICENSE-GPL                                                                               |    0 
crates/action_log/src/action_log.rs                                                                         |   59 
crates/activity_indicator/src/activity_indicator.rs                                                         |  112 
crates/agent/Cargo.toml                                                                                     |    2 
crates/agent/src/agent_profile.rs                                                                           |   10 
crates/agent/src/context.rs                                                                                 |   48 
crates/agent/src/context_server_tool.rs                                                                     |    9 
crates/agent/src/context_store.rs                                                                           |    8 
crates/agent/src/history_store.rs                                                                           |   18 
crates/agent/src/thread.rs                                                                                  |  242 
crates/agent/src/thread_store.rs                                                                            |   84 
crates/agent/src/tool_use.rs                                                                                |   28 
crates/agent2/Cargo.toml                                                                                    |   48 
crates/agent2/src/agent.rs                                                                                  |  916 
crates/agent2/src/agent2.rs                                                                                 |    6 
crates/agent2/src/db.rs                                                                                     |  488 
crates/agent2/src/history_store.rs                                                                          |  362 
crates/agent2/src/native_agent_server.rs                                                                    |   82 
crates/agent2/src/templates.rs                                                                              |    2 
crates/agent2/src/tests/mod.rs                                                                              |  842 
crates/agent2/src/tests/test_tools.rs                                                                       |   42 
crates/agent2/src/thread.rs                                                                                 |  929 
crates/agent2/src/tool_schema.rs                                                                            |   43 
crates/agent2/src/tools.rs                                                                                  |   28 
crates/agent2/src/tools/context_server_registry.rs                                                          |  239 
crates/agent2/src/tools/copy_path_tool.rs                                                                   |  111 
crates/agent2/src/tools/create_directory_tool.rs                                                            |   86 
crates/agent2/src/tools/delete_path_tool.rs                                                                 |  136 
crates/agent2/src/tools/diagnostics_tool.rs                                                                 |  163 
crates/agent2/src/tools/edit_file_tool.rs                                                                   | 1575 
crates/agent2/src/tools/fetch_tool.rs                                                                       |  155 
crates/agent2/src/tools/find_path_tool.rs                                                                   |   81 
crates/agent2/src/tools/grep_tool.rs                                                                        | 1182 
crates/agent2/src/tools/list_directory_tool.rs                                                              |  662 
crates/agent2/src/tools/move_path_tool.rs                                                                   |  120 
crates/agent2/src/tools/now_tool.rs                                                                         |   59 
crates/agent2/src/tools/open_tool.rs                                                                        |  166 
crates/agent2/src/tools/read_file_tool.rs                                                                   |  279 
crates/agent2/src/tools/terminal_tool.rs                                                                    |  468 
crates/agent2/src/tools/thinking_tool.rs                                                                    |    8 
crates/agent2/src/tools/web_search_tool.rs                                                                  |  127 
crates/agent_servers/Cargo.toml                                                                             |   17 
crates/agent_servers/src/acp.rs                                                                             |    4 
crates/agent_servers/src/acp/v0.rs                                                                          |   33 
crates/agent_servers/src/acp/v1.rs                                                                          |  115 
crates/agent_servers/src/agent_servers.rs                                                                   |   17 
crates/agent_servers/src/claude.rs                                                                          |  470 
crates/agent_servers/src/claude/edit_tool.rs                                                                |  178 
crates/agent_servers/src/claude/mcp_server.rs                                                               |  235 
crates/agent_servers/src/claude/permission_tool.rs                                                          |  158 
crates/agent_servers/src/claude/read_tool.rs                                                                |   59 
crates/agent_servers/src/claude/tools.rs                                                                    |   41 
crates/agent_servers/src/claude/write_tool.rs                                                               |   59 
crates/agent_servers/src/e2e_tests.rs                                                                       |  157 
crates/agent_servers/src/gemini.rs                                                                          |   34 
crates/agent_settings/src/agent_profile.rs                                                                  |   14 
crates/agent_settings/src/agent_settings.rs                                                                 |   37 
crates/agent_ui/Cargo.toml                                                                                  |    8 
crates/agent_ui/src/acp.rs                                                                                  |   10 
crates/agent_ui/src/acp/completion_provider.rs                                                              | 1122 
crates/agent_ui/src/acp/entry_view_state.rs                                                                 |  477 
crates/agent_ui/src/acp/message_editor.rs                                                                   | 2464 
crates/agent_ui/src/acp/message_history.rs                                                                  |   92 
crates/agent_ui/src/acp/model_selector.rs                                                                   |  472 
crates/agent_ui/src/acp/model_selector_popover.rs                                                           |   85 
crates/agent_ui/src/acp/thread_history.rs                                                                   |  902 
crates/agent_ui/src/acp/thread_view.rs                                                                      |  913 
crates/agent_ui/src/active_thread.rs                                                                        |  218 
crates/agent_ui/src/agent_configuration.rs                                                                  |   28 
crates/agent_ui/src/agent_configuration/add_llm_provider_modal.rs                                           |  170 
crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs                                   |   39 
crates/agent_ui/src/agent_configuration/manage_profiles_modal.rs                                            |    6 
crates/agent_ui/src/agent_configuration/tool_picker.rs                                                      |    8 
crates/agent_ui/src/agent_diff.rs                                                                           |  169 
crates/agent_ui/src/agent_model_selector.rs                                                                 |    6 
crates/agent_ui/src/agent_panel.rs                                                                          |  690 
crates/agent_ui/src/agent_ui.rs                                                                             |   43 
crates/agent_ui/src/buffer_codegen.rs                                                                       |   86 
crates/agent_ui/src/burn_mode_tooltip.rs                                                                    |   61 
crates/agent_ui/src/context_picker.rs                                                                       |  137 
crates/agent_ui/src/context_picker/completion_provider.rs                                                   |   57 
crates/agent_ui/src/context_picker/fetch_context_picker.rs                                                  |    7 
crates/agent_ui/src/context_picker/file_context_picker.rs                                                   |    8 
crates/agent_ui/src/context_picker/rules_context_picker.rs                                                  |    2 
crates/agent_ui/src/context_picker/symbol_context_picker.rs                                                 |    4 
crates/agent_ui/src/context_picker/thread_context_picker.rs                                                 |   14 
crates/agent_ui/src/context_strip.rs                                                                        |   12 
crates/agent_ui/src/inline_assistant.rs                                                                     |  137 
crates/agent_ui/src/inline_prompt_editor.rs                                                                 |   14 
crates/agent_ui/src/language_model_selector.rs                                                              |   14 
crates/agent_ui/src/message_editor.rs                                                                       |  138 
crates/agent_ui/src/profile_selector.rs                                                                     |   56 
crates/agent_ui/src/slash_command.rs                                                                        |    4 
crates/agent_ui/src/slash_command_picker.rs                                                                 |   18 
crates/agent_ui/src/slash_command_settings.rs                                                               |   11 
crates/agent_ui/src/terminal_codegen.rs                                                                     |    2 
crates/agent_ui/src/terminal_inline_assistant.rs                                                            |   61 
crates/agent_ui/src/text_thread_editor.rs                                                                   |  165 
crates/agent_ui/src/thread_history.rs                                                                       |   10 
crates/agent_ui/src/tool_compatibility.rs                                                                   |   12 
crates/agent_ui/src/ui.rs                                                                                   |    2 
crates/agent_ui/src/ui/burn_mode_tooltip.rs                                                                 |    6 
crates/agent_ui/src/ui/context_pill.rs                                                                      |   10 
crates/agent_ui/src/ui/new_thread_button.rs                                                                 |   75 
crates/agent_ui/src/ui/onboarding_modal.rs                                                                  |    2 
crates/agent_ui/src/ui/preview/usage_callouts.rs                                                            |   14 
crates/ai_onboarding/src/agent_api_keys_onboarding.rs                                                       |    4 
crates/ai_onboarding/src/agent_panel_onboarding_content.rs                                                  |    6 
crates/ai_onboarding/src/ai_onboarding.rs                                                                   |   32 
crates/ai_onboarding/src/young_account_banner.rs                                                            |    2 
crates/askpass/src/askpass.rs                                                                               |    6 
crates/assets/src/assets.rs                                                                                 |    4 
crates/assistant_context/Cargo.toml                                                                         |    3 
crates/assistant_context/src/assistant_context.rs                                                           |  179 
crates/assistant_context/src/assistant_context_tests.rs                                                     |   16 
crates/assistant_context/src/context_store.rs                                                               |   74 
crates/assistant_slash_command/src/assistant_slash_command.rs                                               |   14 
crates/assistant_slash_command/src/extension_slash_command.rs                                               |    2 
crates/assistant_slash_commands/Cargo.toml                                                                  |    1 
crates/assistant_slash_commands/src/assistant_slash_commands.rs                                             |    2 
crates/assistant_slash_commands/src/cargo_workspace_command.rs                                              |    2 
crates/assistant_slash_commands/src/context_server_command.rs                                               |   19 
crates/assistant_slash_commands/src/default_command.rs                                                      |    2 
crates/assistant_slash_commands/src/delta_command.rs                                                        |   71 
crates/assistant_slash_commands/src/diagnostics_command.rs                                                  |   20 
crates/assistant_slash_commands/src/docs_command.rs                                                         |  543 
crates/assistant_slash_commands/src/fetch_command.rs                                                        |    6 
crates/assistant_slash_commands/src/file_command.rs                                                         |   12 
crates/assistant_slash_commands/src/now_command.rs                                                          |    2 
crates/assistant_slash_commands/src/prompt_command.rs                                                       |    4 
crates/assistant_slash_commands/src/symbols_command.rs                                                      |    2 
crates/assistant_slash_commands/src/tab_command.rs                                                          |   18 
crates/assistant_tool/Cargo.toml                                                                            |    5 
crates/assistant_tool/src/assistant_tool.rs                                                                 |    3 
crates/assistant_tool/src/outline.rs                                                                        |    2 
crates/assistant_tool/src/tool_schema.rs                                                                    |   38 
crates/assistant_tool/src/tool_working_set.rs                                                               |    4 
crates/assistant_tools/Cargo.toml                                                                           |    1 
crates/assistant_tools/src/assistant_tools.rs                                                               |   11 
crates/assistant_tools/src/copy_path_tool.rs                                                                |    3 
crates/assistant_tools/src/create_directory_tool.rs                                                         |    3 
crates/assistant_tools/src/delete_path_tool.rs                                                              |    3 
crates/assistant_tools/src/diagnostics_tool.rs                                                              |    9 
crates/assistant_tools/src/edit_agent.rs                                                                    |  122 
crates/assistant_tools/src/edit_agent/create_file_parser.rs                                                 |   13 
crates/assistant_tools/src/edit_agent/evals.rs                                                              |   25 
crates/assistant_tools/src/edit_agent/streaming_fuzzy_matcher.rs                                            |   24 
crates/assistant_tools/src/edit_file_tool.rs                                                                |   41 
crates/assistant_tools/src/fetch_tool.rs                                                                    |    3 
crates/assistant_tools/src/find_path_tool.rs                                                                |    7 
crates/assistant_tools/src/grep_tool.rs                                                                     |   41 
crates/assistant_tools/src/list_directory_tool.rs                                                           |    3 
crates/assistant_tools/src/move_path_tool.rs                                                                |    3 
crates/assistant_tools/src/now_tool.rs                                                                      |    3 
crates/assistant_tools/src/open_tool.rs                                                                     |    3 
crates/assistant_tools/src/project_notifications_tool.rs                                                    |    5 
crates/assistant_tools/src/read_file_tool.rs                                                                |    7 
crates/assistant_tools/src/schema.rs                                                                        |   11 
crates/assistant_tools/src/terminal_tool.rs                                                                 |   27 
crates/assistant_tools/src/thinking_tool.rs                                                                 |    3 
crates/assistant_tools/src/ui/tool_call_card_header.rs                                                      |    9 
crates/assistant_tools/src/web_search_tool.rs                                                               |   12 
crates/audio/Cargo.toml                                                                                     |    7 
crates/audio/src/assets.rs                                                                                  |   54 
crates/audio/src/audio.rs                                                                                   |   76 
crates/audio/src/audio_settings.rs                                                                          |   33 
crates/auto_update/src/auto_update.rs                                                                       |  114 
crates/auto_update_helper/src/auto_update_helper.rs                                                         |   57 
crates/auto_update_helper/src/dialog.rs                                                                     |   14 
crates/auto_update_helper/src/updater.rs                                                                    |   26 
crates/auto_update_ui/src/auto_update_ui.rs                                                                 |    2 
crates/bedrock/src/bedrock.rs                                                                               |    6 
crates/breadcrumbs/src/breadcrumbs.rs                                                                       |   13 
crates/buffer_diff/src/buffer_diff.rs                                                                       |   67 
crates/call/src/call_impl/mod.rs                                                                            |    4 
crates/call/src/call_impl/participant.rs                                                                    |    2 
crates/call/src/call_impl/room.rs                                                                           |  171 
crates/channel/src/channel_buffer.rs                                                                        |   17 
crates/channel/src/channel_chat.rs                                                                          |  129 
crates/channel/src/channel_store.rs                                                                         |   90 
crates/channel/src/channel_store_tests.rs                                                                   |    2 
crates/cli/src/main.rs                                                                                      |   46 
crates/client/Cargo.toml                                                                                    |    1 
crates/client/src/client.rs                                                                                 |  111 
crates/client/src/telemetry.rs                                                                              |   43 
crates/client/src/test.rs                                                                                   |   67 
crates/client/src/user.rs                                                                                   |   89 
crates/client/src/zed_urls.rs                                                                               |    8 
crates/cloud_api_client/src/cloud_api_client.rs                                                             |    6 
crates/cloud_llm_client/src/cloud_llm_client.rs                                                             |    4 
crates/collab/Cargo.toml                                                                                    |    8 
crates/collab/k8s/collab.template.yml                                                                       |    6 
crates/collab/migrations.sqlite/20221109000000_test_schema.sql                                              |   62 
crates/collab/migrations/20250816124707_make_admin_required_on_users.sql                                    |    2 
crates/collab/migrations/20250816133027_add_orb_customer_id_to_billing_customers.sql                        |    2 
crates/collab/migrations/20250816135346_drop_rate_buckets_table.sql                                         |    1 
crates/collab/migrations/20250818192156_add_git_merge_message.sql                                           |    1 
crates/collab/migrations/20250819022421_add_orb_subscription_id_to_billing_subscriptions.sql                |    2 
crates/collab/migrations/20250819225916_make_stripe_fields_optional_on_billing_subscription.sql             |    3 
crates/collab/migrations/20250821133754_add_orb_subscription_status_and_period_to_billing_subscriptions.sql |    4 
crates/collab/src/api.rs                                                                                    |  194 
crates/collab/src/api/billing.rs                                                                            |   59 
crates/collab/src/api/events.rs                                                                             |  328 
crates/collab/src/api/extensions.rs                                                                         |   14 
crates/collab/src/auth.rs                                                                                   |   40 
crates/collab/src/db.rs                                                                                     |    7 
crates/collab/src/db/ids.rs                                                                                 |    3 
crates/collab/src/db/queries.rs                                                                             |    4 
crates/collab/src/db/queries/billing_customers.rs                                                           |  100 
crates/collab/src/db/queries/billing_preferences.rs                                                         |   17 
crates/collab/src/db/queries/billing_subscriptions.rs                                                       |  158 
crates/collab/src/db/queries/extensions.rs                                                                  |   16 
crates/collab/src/db/queries/processed_stripe_events.rs                                                     |   69 
crates/collab/src/db/queries/projects.rs                                                                    |   21 
crates/collab/src/db/queries/rooms.rs                                                                       |    7 
crates/collab/src/db/tables.rs                                                                              |    4 
crates/collab/src/db/tables/billing_customer.rs                                                             |   41 
crates/collab/src/db/tables/billing_preference.rs                                                           |   32 
crates/collab/src/db/tables/billing_subscription.rs                                                         |  176 
crates/collab/src/db/tables/processed_stripe_event.rs                                                       |   16 
crates/collab/src/db/tables/project_repository.rs                                                           |    2 
crates/collab/src/db/tables/user.rs                                                                         |    8 
crates/collab/src/db/tests.rs                                                                               |    1 
crates/collab/src/db/tests/embedding_tests.rs                                                               |    4 
crates/collab/src/db/tests/processed_stripe_event_tests.rs                                                  |   38 
crates/collab/src/lib.rs                                                                                    |   61 
crates/collab/src/llm.rs                                                                                    |   11 
crates/collab/src/llm/db.rs                                                                                 |   74 
crates/collab/src/llm/db/ids.rs                                                                             |   11 
crates/collab/src/llm/db/queries.rs                                                                         |    5 
crates/collab/src/llm/db/queries/providers.rs                                                               |  134 
crates/collab/src/llm/db/queries/subscription_usages.rs                                                     |   38 
crates/collab/src/llm/db/queries/usages.rs                                                                  |   44 
crates/collab/src/llm/db/seed.rs                                                                            |   45 
crates/collab/src/llm/db/tables.rs                                                                          |    6 
crates/collab/src/llm/db/tables/model.rs                                                                    |   48 
crates/collab/src/llm/db/tables/provider.rs                                                                 |   25 
crates/collab/src/llm/db/tables/subscription_usage.rs                                                       |   22 
crates/collab/src/llm/db/tables/subscription_usage_meter.rs                                                 |   55 
crates/collab/src/llm/db/tables/usage.rs                                                                    |   52 
crates/collab/src/llm/db/tables/usage_measure.rs                                                            |   36 
crates/collab/src/llm/db/tests.rs                                                                           |  107 
crates/collab/src/llm/db/tests/provider_tests.rs                                                            |   31 
crates/collab/src/llm/token.rs                                                                              |  146 
crates/collab/src/main.rs                                                                                   |   17 
crates/collab/src/rpc.rs                                                                                    |  534 
crates/collab/src/rpc/connection_pool.rs                                                                    |   14 
crates/collab/src/stripe_billing.rs                                                                         |  156 
crates/collab/src/stripe_client.rs                                                                          |  285 
crates/collab/src/stripe_client/fake_stripe_client.rs                                                       |  247 
crates/collab/src/stripe_client/real_stripe_client.rs                                                       |  612 
crates/collab/src/tests.rs                                                                                  |    2 
crates/collab/src/tests/editor_tests.rs                                                                     |  228 
crates/collab/src/tests/integration_tests.rs                                                                |   26 
crates/collab/src/tests/random_channel_buffer_tests.rs                                                      |    2 
crates/collab/src/tests/random_project_collaboration_tests.rs                                               |    9 
crates/collab/src/tests/randomized_test_helpers.rs                                                          |   30 
crates/collab/src/tests/stripe_billing_tests.rs                                                             |  123 
crates/collab/src/tests/test_server.rs                                                                      |   12 
crates/collab/src/user_backfiller.rs                                                                        |   22 
crates/collab_ui/src/channel_view.rs                                                                        |   82 
crates/collab_ui/src/chat_panel.rs                                                                          |   89 
crates/collab_ui/src/chat_panel/message_editor.rs                                                           |   70 
crates/collab_ui/src/collab_panel.rs                                                                        |  213 
crates/collab_ui/src/collab_panel/channel_modal.rs                                                          |    2 
crates/collab_ui/src/notification_panel.rs                                                                  |   84 
crates/command_palette/src/command_palette.rs                                                               |    2 
crates/component/src/component_layout.rs                                                                    |    2 
crates/context_server/src/client.rs                                                                         |   18 
crates/context_server/src/context_server.rs                                                                 |    2 
crates/context_server/src/listener.rs                                                                       |   31 
crates/context_server/src/types.rs                                                                          |   12 
crates/copilot/src/copilot.rs                                                                               |  225 
crates/copilot/src/copilot_chat.rs                                                                          |    4 
crates/copilot/src/copilot_completion_provider.rs                                                           |    2 
crates/crashes/Cargo.toml                                                                                   |    6 
crates/crashes/src/crashes.rs                                                                               |  197 
crates/credentials_provider/src/credentials_provider.rs                                                     |    2 
crates/dap/src/adapters.rs                                                                                  |    2 
crates/dap/src/client.rs                                                                                    |    2 
crates/dap_adapters/src/codelldb.rs                                                                         |   30 
crates/dap_adapters/src/go.rs                                                                               |    2 
crates/dap_adapters/src/javascript.rs                                                                       |   10 
crates/dap_adapters/src/python.rs                                                                           |   67 
crates/db/src/db.rs                                                                                         |   14 
crates/db/src/kvp.rs                                                                                        |    2 
crates/debugger_tools/src/dap_log.rs                                                                        |   20 
crates/debugger_ui/src/debugger_panel.rs                                                                    |  192 
crates/debugger_ui/src/debugger_ui.rs                                                                       |    6 
crates/debugger_ui/src/dropdown_menus.rs                                                                    |    3 
crates/debugger_ui/src/new_process_modal.rs                                                                 |   23 
crates/debugger_ui/src/onboarding_modal.rs                                                                  |    2 
crates/debugger_ui/src/persistence.rs                                                                       |    4 
crates/debugger_ui/src/session.rs                                                                           |    6 
crates/debugger_ui/src/session/running.rs                                                                   |   70 
crates/debugger_ui/src/session/running/breakpoint_list.rs                                                   |  360 
crates/debugger_ui/src/session/running/console.rs                                                           |   40 
crates/debugger_ui/src/session/running/loaded_source_list.rs                                                |    2 
crates/debugger_ui/src/session/running/memory_view.rs                                                       |   12 
crates/debugger_ui/src/session/running/module_list.rs                                                       |   10 
crates/debugger_ui/src/session/running/stack_frame_list.rs                                                  |   16 
crates/debugger_ui/src/session/running/variable_list.rs                                                     |   17 
crates/debugger_ui/src/tests/attach_modal.rs                                                                |    4 
crates/debugger_ui/src/tests/debugger_panel.rs                                                              |    5 
crates/debugger_ui/src/tests/new_process_modal.rs                                                           |    4 
crates/debugger_ui/src/tests/variable_list.rs                                                               |    7 
crates/diagnostics/src/diagnostic_renderer.rs                                                               |   20 
crates/diagnostics/src/diagnostics.rs                                                                       |   52 
crates/diagnostics/src/diagnostics_tests.rs                                                                 |   12 
crates/diagnostics/src/toolbar_controls.rs                                                                  |    4 
crates/docs_preprocessor/src/main.rs                                                                        |  112 
crates/edit_prediction/src/edit_prediction.rs                                                               |    2 
crates/edit_prediction_button/src/edit_prediction_button.rs                                                 |   14 
crates/editor/src/actions.rs                                                                                |   12 
crates/editor/src/clangd_ext.rs                                                                             |    4 
crates/editor/src/code_completion_tests.rs                                                                  |    4 
crates/editor/src/code_context_menus.rs                                                                     |   10 
crates/editor/src/display_map.rs                                                                            |   38 
crates/editor/src/display_map/block_map.rs                                                                  |  297 
crates/editor/src/display_map/custom_highlights.rs                                                          |    8 
crates/editor/src/display_map/fold_map.rs                                                                   |   94 
crates/editor/src/display_map/inlay_map.rs                                                                  |   37 
crates/editor/src/display_map/invisibles.rs                                                                 |   16 
crates/editor/src/display_map/tab_map.rs                                                                    |   14 
crates/editor/src/display_map/wrap_map.rs                                                                   |  118 
crates/editor/src/edit_prediction_tests.rs                                                                  |   42 
crates/editor/src/editor.rs                                                                                 |  725 
crates/editor/src/editor_settings.rs                                                                        |   14 
crates/editor/src/editor_settings_controls.rs                                                               |    2 
crates/editor/src/editor_tests.rs                                                                           |  428 
crates/editor/src/element.rs                                                                                |  727 
crates/editor/src/git/blame.rs                                                                              |   40 
crates/editor/src/highlight_matching_bracket.rs                                                             |   30 
crates/editor/src/hover_links.rs                                                                            |   62 
crates/editor/src/hover_popover.rs                                                                          |  201 
crates/editor/src/indent_guides.rs                                                                          |   14 
crates/editor/src/inlay_hint_cache.rs                                                                       |  126 
crates/editor/src/items.rs                                                                                  |  111 
crates/editor/src/jsx_tag_auto_close.rs                                                                     |   54 
crates/editor/src/linked_editing_ranges.rs                                                                  |    4 
crates/editor/src/lsp_colors.rs                                                                             |    2 
crates/editor/src/lsp_ext.rs                                                                                |   21 
crates/editor/src/mouse_context_menu.rs                                                                     |   26 
crates/editor/src/movement.rs                                                                               |   64 
crates/editor/src/proposed_changes_editor.rs                                                                |   35 
crates/editor/src/rust_analyzer_ext.rs                                                                      |   22 
crates/editor/src/scroll.rs                                                                                 |   26 
crates/editor/src/scroll/actions.rs                                                                         |    2 
crates/editor/src/scroll/autoscroll.rs                                                                      |   12 
crates/editor/src/scroll/scroll_amount.rs                                                                   |    5 
crates/editor/src/selections_collection.rs                                                                  |   22 
crates/editor/src/signature_help.rs                                                                         |    8 
crates/editor/src/tasks.rs                                                                                  |    2 
crates/editor/src/test.rs                                                                                   |   47 
crates/editor/src/test/editor_lsp_test_context.rs                                                           |    2 
crates/editor/src/test/editor_test_context.rs                                                               |   30 
crates/eval/build.rs                                                                                        |   14 
crates/eval/src/assertions.rs                                                                               |    2 
crates/eval/src/eval.rs                                                                                     |   25 
crates/eval/src/example.rs                                                                                  |    6 
crates/eval/src/examples/add_arg_to_trait_method.rs                                                         |    6 
crates/eval/src/explorer.rs                                                                                 |   28 
crates/eval/src/instance.rs                                                                                 |   46 
crates/extension/src/extension.rs                                                                           |   17 
crates/extension/src/extension_builder.rs                                                                   |   16 
crates/extension/src/extension_events.rs                                                                    |    5 
crates/extension/src/extension_host_proxy.rs                                                                |   34 
crates/extension/src/extension_manifest.rs                                                                  |    7 
crates/extension_api/src/extension_api.rs                                                                   |    6 
crates/extension_cli/src/main.rs                                                                            |    4 
crates/extension_host/benches/extension_compilation_benchmark.rs                                            |    1 
crates/extension_host/src/capability_granter.rs                                                             |    3 
crates/extension_host/src/extension_host.rs                                                                 |  138 
crates/extension_host/src/extension_store_test.rs                                                           |    3 
crates/extension_host/src/headless_host.rs                                                                  |    3 
crates/extension_host/src/wasm_host.rs                                                                      |   19 
crates/extension_host/src/wasm_host/wit/since_v0_6_0.rs                                                     |    2 
crates/extensions_ui/src/components/feature_upsell.rs                                                       |    3 
crates/extensions_ui/src/extensions_ui.rs                                                                   |   28 
crates/feature_flags/src/feature_flags.rs                                                                   |   27 
crates/feedback/src/system_specs.rs                                                                         |    8 
crates/file_finder/src/file_finder.rs                                                                       |  424 
crates/file_finder/src/file_finder_tests.rs                                                                 |    4 
crates/file_finder/src/open_path_prompt.rs                                                                  |   32 
crates/file_icons/src/file_icons.rs                                                                         |   18 
crates/fs/Cargo.toml                                                                                        |    1 
crates/fs/src/fake_git_repo.rs                                                                              |  133 
crates/fs/src/fs.rs                                                                                         |  492 
crates/fs/src/fs_watcher.rs                                                                                 |  200 
crates/fs/src/mac_watcher.rs                                                                                |    5 
crates/fsevent/src/fsevent.rs                                                                               |   65 
crates/git/Cargo.toml                                                                                       |    4 
crates/git/src/blame.rs                                                                                     |   13 
crates/git/src/git.rs                                                                                       |    9 
crates/git/src/repository.rs                                                                                |   84 
crates/git/src/status.rs                                                                                    |   41 
crates/git_hosting_providers/src/git_hosting_providers.rs                                                   |   10 
crates/git_hosting_providers/src/providers/chromium.rs                                                      |    2 
crates/git_hosting_providers/src/providers/github.rs                                                        |    4 
crates/git_ui/src/blame_ui.rs                                                                               |    6 
crates/git_ui/src/branch_picker.rs                                                                          |    6 
crates/git_ui/src/commit_modal.rs                                                                           |   41 
crates/git_ui/src/commit_tooltip.rs                                                                         |    2 
crates/git_ui/src/commit_view.rs                                                                            |    9 
crates/git_ui/src/conflict_view.rs                                                                          |   19 
crates/git_ui/src/file_diff_view.rs                                                                         |   10 
crates/git_ui/src/git_panel.rs                                                                              |  290 
crates/git_ui/src/git_ui.rs                                                                                 |  126 
crates/git_ui/src/onboarding.rs                                                                             |    2 
crates/git_ui/src/picker_prompt.rs                                                                          |    2 
crates/git_ui/src/project_diff.rs                                                                           |   66 
crates/git_ui/src/text_diff_view.rs                                                                         |   10 
crates/go_to_line/src/cursor_position.rs                                                                    |   29 
crates/go_to_line/src/go_to_line.rs                                                                         |   14 
crates/google_ai/src/google_ai.rs                                                                           |   11 
crates/gpui/Cargo.toml                                                                                      |    7 
crates/gpui/build.rs                                                                                        |   19 
crates/gpui/examples/grid_layout.rs                                                                         |   80 
crates/gpui/examples/input.rs                                                                               |   20 
crates/gpui/examples/set_menus.rs                                                                           |    9 
crates/gpui/examples/text.rs                                                                                |    2 
crates/gpui/src/action.rs                                                                                   |   12 
crates/gpui/src/app.rs                                                                                      |   88 
crates/gpui/src/app/async_context.rs                                                                        |    2 
crates/gpui/src/app/context.rs                                                                              |   57 
crates/gpui/src/app/entity_map.rs                                                                           |    7 
crates/gpui/src/app/test_context.rs                                                                         |   25 
crates/gpui/src/arena.rs                                                                                    |    2 
crates/gpui/src/color.rs                                                                                    |    8 
crates/gpui/src/element.rs                                                                                  |    6 
crates/gpui/src/elements/div.rs                                                                             |  250 
crates/gpui/src/elements/image_cache.rs                                                                     |   10 
crates/gpui/src/elements/img.rs                                                                             |   15 
crates/gpui/src/elements/list.rs                                                                            |   81 
crates/gpui/src/elements/text.rs                                                                            |   38 
crates/gpui/src/geometry.rs                                                                                 |   75 
crates/gpui/src/gpui.rs                                                                                     |    6 
crates/gpui/src/inspector.rs                                                                                |    2 
crates/gpui/src/key_dispatch.rs                                                                             |  186 
crates/gpui/src/keymap.rs                                                                                   |   62 
crates/gpui/src/keymap/binding.rs                                                                           |   15 
crates/gpui/src/keymap/context.rs                                                                           |   44 
crates/gpui/src/path_builder.rs                                                                             |    2 
crates/gpui/src/platform.rs                                                                                 |   16 
crates/gpui/src/platform/app_menu.rs                                                                        |   56 
crates/gpui/src/platform/blade/blade_context.rs                                                             |    2 
crates/gpui/src/platform/blade/blade_renderer.rs                                                            |   30 
crates/gpui/src/platform/blade/shaders.wgsl                                                                 |    9 
crates/gpui/src/platform/linux/platform.rs                                                                  |   69 
crates/gpui/src/platform/linux/text_system.rs                                                               |    6 
crates/gpui/src/platform/linux/wayland.rs                                                                   |    2 
crates/gpui/src/platform/linux/wayland/client.rs                                                            |  123 
crates/gpui/src/platform/linux/wayland/cursor.rs                                                            |   13 
crates/gpui/src/platform/linux/wayland/window.rs                                                            |  176 
crates/gpui/src/platform/linux/x11/client.rs                                                                |  137 
crates/gpui/src/platform/linux/x11/clipboard.rs                                                             |   42 
crates/gpui/src/platform/linux/x11/event.rs                                                                 |    6 
crates/gpui/src/platform/linux/x11/window.rs                                                                |   57 
crates/gpui/src/platform/mac/events.rs                                                                      |   11 
crates/gpui/src/platform/mac/metal_renderer.rs                                                              |   27 
crates/gpui/src/platform/mac/open_type.rs                                                                   |   16 
crates/gpui/src/platform/mac/platform.rs                                                                    |  112 
crates/gpui/src/platform/mac/shaders.metal                                                                  |    9 
crates/gpui/src/platform/mac/text_system.rs                                                                 |    8 
crates/gpui/src/platform/mac/window.rs                                                                      |   55 
crates/gpui/src/platform/scap_screen_capture.rs                                                             |    2 
crates/gpui/src/platform/test/dispatcher.rs                                                                 |   14 
crates/gpui/src/platform/test/platform.rs                                                                   |   13 
crates/gpui/src/platform/windows.rs                                                                         |    2 
crates/gpui/src/platform/windows/direct_write.rs                                                            |    8 
crates/gpui/src/platform/windows/directx_renderer.rs                                                        |   51 
crates/gpui/src/platform/windows/events.rs                                                                  |   86 
crates/gpui/src/platform/windows/platform.rs                                                                |  228 
crates/gpui/src/platform/windows/shaders.hlsl                                                               |   11 
crates/gpui/src/platform/windows/util.rs                                                                    |   24 
crates/gpui/src/platform/windows/vsync.rs                                                                   |  174 
crates/gpui/src/platform/windows/window.rs                                                                  |    5 
crates/gpui/src/platform/windows/wrapper.rs                                                                 |   44 
crates/gpui/src/scene.rs                                                                                    |    2 
crates/gpui/src/shared_string.rs                                                                            |    7 
crates/gpui/src/style.rs                                                                                    |   29 
crates/gpui/src/styled.rs                                                                                   |  109 
crates/gpui/src/subscription.rs                                                                             |    6 
crates/gpui/src/tab_stop.rs                                                                                 |   23 
crates/gpui/src/taffy.rs                                                                                    |   69 
crates/gpui/src/text_system.rs                                                                              |   62 
crates/gpui/src/text_system/line.rs                                                                         |   24 
crates/gpui/src/text_system/line_layout.rs                                                                  |    8 
crates/gpui/src/text_system/line_wrapper.rs                                                                 |    4 
crates/gpui/src/util.rs                                                                                     |   81 
crates/gpui/src/view.rs                                                                                     |   31 
crates/gpui/src/window.rs                                                                                   |   95 
crates/gpui_macros/src/derive_inspector_reflection.rs                                                       |   20 
crates/gpui_macros/src/gpui_macros.rs                                                                       |    2 
crates/gpui_macros/src/test.rs                                                                              |  120 
crates/gpui_macros/tests/derive_inspector_reflection.rs                                                     |   18 
crates/html_to_markdown/src/markdown.rs                                                                     |   17 
crates/http_client/src/async_body.rs                                                                        |    2 
crates/http_client/src/github.rs                                                                            |   12 
crates/http_client/src/http_client.rs                                                                       |    3 
crates/icons/README.md                                                                                      |   29 
crates/icons/src/icons.rs                                                                                   |   64 
crates/indexed_docs/Cargo.toml                                                                              |   38 
crates/indexed_docs/src/extension_indexed_docs_provider.rs                                                  |   81 
crates/indexed_docs/src/indexed_docs.rs                                                                     |   16 
crates/indexed_docs/src/providers.rs                                                                        |    1 
crates/indexed_docs/src/providers/rustdoc.rs                                                                |  291 
crates/indexed_docs/src/providers/rustdoc/item.rs                                                           |   82 
crates/indexed_docs/src/providers/rustdoc/popular_crates.txt                                                |  252 
crates/indexed_docs/src/providers/rustdoc/to_markdown.rs                                                    |  618 
crates/indexed_docs/src/registry.rs                                                                         |   62 
crates/indexed_docs/src/store.rs                                                                            |  346 
crates/inspector_ui/src/div_inspector.rs                                                                    |   54 
crates/install_cli/src/install_cli.rs                                                                       |    2 
crates/jj/src/jj_repository.rs                                                                              |    7 
crates/jj/src/jj_store.rs                                                                                   |    2 
crates/journal/src/journal.rs                                                                               |   40 
crates/language/src/buffer.rs                                                                               |  330 
crates/language/src/buffer_tests.rs                                                                         |   10 
crates/language/src/language.rs                                                                             |  146 
crates/language/src/language_registry.rs                                                                    |   25 
crates/language/src/language_settings.rs                                                                    |   26 
crates/language/src/manifest.rs                                                                             |    6 
crates/language/src/proto.rs                                                                                |   12 
crates/language/src/syntax_map.rs                                                                           |  114 
crates/language/src/syntax_map/syntax_map_tests.rs                                                          |   15 
crates/language/src/text_diff.rs                                                                            |   30 
crates/language/src/toolchain.rs                                                                            |   33 
crates/language_extension/src/extension_lsp_adapter.rs                                                      |   10 
crates/language_extension/src/language_extension.rs                                                         |    4 
crates/language_model/src/fake_provider.rs                                                                  |   47 
crates/language_model/src/language_model.rs                                                                 |   20 
crates/language_model/src/model/cloud_model.rs                                                              |   14 
crates/language_model/src/registry.rs                                                                       |   11 
crates/language_model/src/request.rs                                                                        |   53 
crates/language_model/src/role.rs                                                                           |    2 
crates/language_models/src/language_models.rs                                                               |    2 
crates/language_models/src/provider/anthropic.rs                                                            |  108 
crates/language_models/src/provider/bedrock.rs                                                              |   48 
crates/language_models/src/provider/cloud.rs                                                                |   73 
crates/language_models/src/provider/copilot_chat.rs                                                         |    7 
crates/language_models/src/provider/deepseek.rs                                                             |   13 
crates/language_models/src/provider/google.rs                                                               |   70 
crates/language_models/src/provider/lmstudio.rs                                                             |   17 
crates/language_models/src/provider/mistral.rs                                                              |  148 
crates/language_models/src/provider/ollama.rs                                                               |   17 
crates/language_models/src/provider/open_ai.rs                                                              |   51 
crates/language_models/src/provider/open_ai_compatible.rs                                                   |   51 
crates/language_models/src/provider/open_router.rs                                                          |   13 
crates/language_models/src/provider/vercel.rs                                                               |   15 
crates/language_models/src/provider/x_ai.rs                                                                 |   15 
crates/language_models/src/ui/instruction_list_item.rs                                                      |    4 
crates/language_selector/src/active_buffer_language.rs                                                      |    8 
crates/language_tools/src/key_context_view.rs                                                               |   12 
crates/language_tools/src/lsp_log.rs                                                                        |  131 
crates/language_tools/src/lsp_tool.rs                                                                       |    4 
crates/language_tools/src/syntax_tree_view.rs                                                               |   27 
crates/languages/src/bash/overrides.scm                                                                     |    2 
crates/languages/src/c.rs                                                                                   |   29 
crates/languages/src/cpp/outline.scm                                                                        |    6 
crates/languages/src/css.rs                                                                                 |   15 
crates/languages/src/github_download.rs                                                                     |   12 
crates/languages/src/go.rs                                                                                  |  383 
crates/languages/src/go/outline.scm                                                                         |   13 
crates/languages/src/go/runnables.scm                                                                       |  100 
crates/languages/src/javascript/outline.scm                                                                 |    4 
crates/languages/src/json.rs                                                                                |   25 
crates/languages/src/jsonc/overrides.scm                                                                    |    1 
crates/languages/src/lib.rs                                                                                 |   42 
crates/languages/src/python.rs                                                                              |  193 
crates/languages/src/rust.rs                                                                                |   64 
crates/languages/src/tailwind.rs                                                                            |   17 
crates/languages/src/tsx/outline.scm                                                                        |    4 
crates/languages/src/typescript.rs                                                                          |   26 
crates/languages/src/typescript/outline.scm                                                                 |    4 
crates/languages/src/vtsls.rs                                                                               |   14 
crates/languages/src/yaml.rs                                                                                |   19 
crates/languages/src/yaml/overrides.scm                                                                     |    5 
crates/livekit_client/Cargo.toml                                                                            |    4 
crates/livekit_client/examples/test_app.rs                                                                  |    6 
crates/livekit_client/src/lib.rs                                                                            |   67 
crates/livekit_client/src/livekit_client.rs                                                                 |   12 
crates/livekit_client/src/livekit_client/playback.rs                                                        |  126 
crates/livekit_client/src/livekit_client/playback/source.rs                                                 |   67 
crates/livekit_client/src/record.rs                                                                         |   91 
crates/livekit_client/src/test.rs                                                                           |   20 
crates/lsp/src/lsp.rs                                                                                       |   50 
crates/markdown/examples/markdown.rs                                                                        |    6 
crates/markdown/examples/markdown_as_child.rs                                                               |    2 
crates/markdown/src/markdown.rs                                                                             |   87 
crates/markdown/src/parser.rs                                                                               |    2 
crates/markdown_preview/src/markdown_parser.rs                                                              |   34 
crates/markdown_preview/src/markdown_preview_view.rs                                                        |   55 
crates/markdown_preview/src/markdown_renderer.rs                                                            |    9 
crates/migrator/src/migrations/m_2025_01_02/settings.rs                                                     |    4 
crates/migrator/src/migrations/m_2025_01_29/keymap.rs                                                       |   14 
crates/migrator/src/migrations/m_2025_01_29/settings.rs                                                     |    2 
crates/migrator/src/migrations/m_2025_01_30/settings.rs                                                     |    6 
crates/migrator/src/migrations/m_2025_03_29/settings.rs                                                     |    2 
crates/migrator/src/migrations/m_2025_05_05/settings.rs                                                     |    4 
crates/migrator/src/migrations/m_2025_05_29/settings.rs                                                     |    4 
crates/migrator/src/migrations/m_2025_06_16/settings.rs                                                     |   28 
crates/migrator/src/migrations/m_2025_06_25/settings.rs                                                     |   16 
crates/migrator/src/migrations/m_2025_06_27/settings.rs                                                     |   25 
crates/migrator/src/migrator.rs                                                                             |   10 
crates/mistral/src/mistral.rs                                                                               |   50 
crates/multi_buffer/src/anchor.rs                                                                           |  115 
crates/multi_buffer/src/multi_buffer.rs                                                                     |  573 
crates/multi_buffer/src/multi_buffer_tests.rs                                                               |   53 
crates/multi_buffer/src/position.rs                                                                         |    6 
crates/node_runtime/src/node_runtime.rs                                                                     |   34 
crates/notifications/src/notification_store.rs                                                              |   53 
crates/notifications/src/status_toast.rs                                                                    |    2 
crates/onboarding/Cargo.toml                                                                                |    4 
crates/onboarding/src/ai_setup_page.rs                                                                      |  131 
crates/onboarding/src/base_keymap_picker.rs                                                                 |    2 
crates/onboarding/src/basics_page.rs                                                                        |   46 
crates/onboarding/src/editing_page.rs                                                                       |   73 
crates/onboarding/src/multibuffer_hint.rs                                                                   |    2 
crates/onboarding/src/onboarding.rs                                                                         |  165 
crates/onboarding/src/theme_preview.rs                                                                      |   53 
crates/onboarding/src/welcome.rs                                                                            |  115 
crates/open_ai/Cargo.toml                                                                                   |    1 
crates/open_ai/src/open_ai.rs                                                                               |   56 
crates/open_router/src/open_router.rs                                                                       |   10 
crates/outline_panel/src/outline_panel.rs                                                                   |  456 
crates/panel/src/panel.rs                                                                                   |    2 
crates/paths/src/paths.rs                                                                                   |    2 
crates/picker/src/popover_menu.rs                                                                           |    2 
crates/prettier/src/prettier.rs                                                                             |   42 
crates/project/src/buffer_store.rs                                                                          |   82 
1,000 files changed, 32,527 insertions(+), 19,635 deletions(-)

Detailed changes

.config/hakari.toml 🔗

@@ -25,6 +25,8 @@ third-party = [
     { name = "reqwest", version = "0.11.27" },
     # build of remote_server should not include scap / its x11 dependency
     { name = "scap", git = "https://github.com/zed-industries/scap", rev = "808aa5c45b41e8f44729d02e38fd00a2fe2722e7" },
+    # build of remote_server should not need to include on libalsa through rodio
+    { name = "rodio" },
 ]
 
 [final-excludes]
@@ -32,7 +34,6 @@ workspace-members = [
     "zed_extension_api",
 
     # exclude all extensions
-    "zed_emmet",
     "zed_glsl",
     "zed_html",
     "zed_proto",

.github/ISSUE_TEMPLATE/07_bug_windows_alpha.yml 🔗

@@ -0,0 +1,35 @@
+name: Bug Report (Windows Alpha)
+description: Zed Windows Alpha Related Bugs
+type: "Bug"
+labels: ["windows"]
+title: "Windows Alpha: <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/actionlint.yml 🔗

@@ -24,6 +24,9 @@ self-hosted-runner:
     - namespace-profile-8x16-ubuntu-2204
     - namespace-profile-16x32-ubuntu-2204
     - namespace-profile-32x64-ubuntu-2204
+    # Namespace Limited Preview
+    - namespace-profile-8x16-ubuntu-2004-arm-m4
+    - namespace-profile-8x32-ubuntu-2004-arm-m4
     # Self Hosted Runners
     - self-mini-macos
     - self-32vcpu-windows-2022

.github/actions/run_tests_windows/action.yml 🔗

@@ -20,7 +20,167 @@ runs:
       with:
         node-version: "18"
 
+    - name: Configure crash dumps
+      shell: powershell
+      run: |
+        # Record the start time for this CI run
+        $runStartTime = Get-Date
+        $runStartTimeStr = $runStartTime.ToString("yyyy-MM-dd HH:mm:ss")
+        Write-Host "CI run started at: $runStartTimeStr"
+
+        # Save the timestamp for later use
+        echo "CI_RUN_START_TIME=$($runStartTime.Ticks)" >> $env:GITHUB_ENV
+
+        # Create crash dump directory in workspace (non-persistent)
+        $dumpPath = "$env:GITHUB_WORKSPACE\crash_dumps"
+        New-Item -ItemType Directory -Force -Path $dumpPath | Out-Null
+
+        Write-Host "Setting up crash dump detection..."
+        Write-Host "Workspace dump path: $dumpPath"
+
+        # Note: We're NOT modifying registry on stateful runners
+        # Instead, we'll check default Windows crash locations after tests
+
     - name: Run tests
       shell: powershell
       working-directory: ${{ inputs.working-directory }}
-      run: cargo nextest run --workspace --no-fail-fast
+      run: |
+        $env:RUST_BACKTRACE = "full"
+
+        # Enable Windows debugging features
+        $env:_NT_SYMBOL_PATH = "srv*https://msdl.microsoft.com/download/symbols"
+
+        # .NET crash dump environment variables (ephemeral)
+        $env:COMPlus_DbgEnableMiniDump = "1"
+        $env:COMPlus_DbgMiniDumpType = "4"
+        $env:COMPlus_CreateDumpDiagnostics = "1"
+
+        cargo nextest run --workspace --no-fail-fast
+
+    - name: Analyze crash dumps
+      if: always()
+      shell: powershell
+      run: |
+        Write-Host "Checking for crash dumps..."
+
+        # Get the CI run start time from the environment
+        $runStartTime = [DateTime]::new([long]$env:CI_RUN_START_TIME)
+        Write-Host "Only analyzing dumps created after: $($runStartTime.ToString('yyyy-MM-dd HH:mm:ss'))"
+
+        # Check all possible crash dump locations
+        $searchPaths = @(
+            "$env:GITHUB_WORKSPACE\crash_dumps",
+            "$env:LOCALAPPDATA\CrashDumps",
+            "$env:TEMP",
+            "$env:GITHUB_WORKSPACE",
+            "$env:USERPROFILE\AppData\Local\CrashDumps",
+            "C:\Windows\System32\config\systemprofile\AppData\Local\CrashDumps"
+        )
+
+        $dumps = @()
+        foreach ($path in $searchPaths) {
+            if (Test-Path $path) {
+                Write-Host "Searching in: $path"
+                $found = Get-ChildItem "$path\*.dmp" -ErrorAction SilentlyContinue | Where-Object {
+                    $_.CreationTime -gt $runStartTime
+                }
+                if ($found) {
+                    $dumps += $found
+                    Write-Host "  Found $($found.Count) dump(s) from this CI run"
+                }
+            }
+        }
+
+        if ($dumps) {
+          Write-Host "Found $($dumps.Count) crash dump(s)"
+
+          # Install debugging tools if not present
+          $cdbPath = "C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\cdb.exe"
+          if (-not (Test-Path $cdbPath)) {
+            Write-Host "Installing Windows Debugging Tools..."
+            $url = "https://go.microsoft.com/fwlink/?linkid=2237387"
+            Invoke-WebRequest -Uri $url -OutFile winsdksetup.exe
+            Start-Process -Wait winsdksetup.exe -ArgumentList "/features OptionId.WindowsDesktopDebuggers /quiet"
+          }
+
+          foreach ($dump in $dumps) {
+            Write-Host "`n=================================="
+            Write-Host "Analyzing crash dump: $($dump.Name)"
+            Write-Host "Size: $([math]::Round($dump.Length / 1MB, 2)) MB"
+            Write-Host "Time: $($dump.CreationTime)"
+            Write-Host "=================================="
+
+            # Set symbol path
+            $env:_NT_SYMBOL_PATH = "srv*C:\symbols*https://msdl.microsoft.com/download/symbols"
+
+            # Run analysis
+            $analysisOutput = & $cdbPath -z $dump.FullName -c "!analyze -v; ~*k; lm; q" 2>&1 | Out-String
+
+            # Extract key information
+            if ($analysisOutput -match "ExceptionCode:\s*([\w]+)") {
+              Write-Host "Exception Code: $($Matches[1])"
+              if ($Matches[1] -eq "c0000005") {
+                Write-Host "Exception Type: ACCESS VIOLATION"
+              }
+            }
+
+            if ($analysisOutput -match "EXCEPTION_RECORD:\s*(.+)") {
+              Write-Host "Exception Record: $($Matches[1])"
+            }
+
+            if ($analysisOutput -match "FAULTING_IP:\s*\n(.+)") {
+              Write-Host "Faulting Instruction: $($Matches[1])"
+            }
+
+            # Save full analysis
+            $analysisFile = "$($dump.FullName).analysis.txt"
+            $analysisOutput | Out-File -FilePath $analysisFile
+            Write-Host "`nFull analysis saved to: $analysisFile"
+
+            # Print stack trace section
+            Write-Host "`n--- Stack Trace Preview ---"
+            $stackSection = $analysisOutput -split "STACK_TEXT:" | Select-Object -Last 1
+            $stackLines = $stackSection -split "`n" | Select-Object -First 20
+            $stackLines | ForEach-Object { Write-Host $_ }
+            Write-Host "--- End Stack Trace Preview ---"
+          }
+
+          Write-Host "`n⚠️  Crash dumps detected! Download the 'crash-dumps' artifact for detailed analysis."
+
+          # Copy dumps to workspace for artifact upload
+          $artifactPath = "$env:GITHUB_WORKSPACE\crash_dumps_collected"
+          New-Item -ItemType Directory -Force -Path $artifactPath | Out-Null
+
+          foreach ($dump in $dumps) {
+            $destName = "$($dump.Directory.Name)_$($dump.Name)"
+            Copy-Item $dump.FullName -Destination "$artifactPath\$destName"
+            if (Test-Path "$($dump.FullName).analysis.txt") {
+              Copy-Item "$($dump.FullName).analysis.txt" -Destination "$artifactPath\$destName.analysis.txt"
+            }
+          }
+
+          Write-Host "Copied $($dumps.Count) dump(s) to artifact directory"
+        } else {
+          Write-Host "No crash dumps from this CI run found"
+        }
+
+    - name: Upload crash dumps
+      if: always()
+      uses: actions/upload-artifact@v4
+      with:
+        name: crash-dumps-${{ github.run_id }}-${{ github.run_attempt }}
+        path: |
+          crash_dumps_collected/*.dmp
+          crash_dumps_collected/*.txt
+        if-no-files-found: ignore
+        retention-days: 7
+
+    - name: Check test results
+      shell: powershell
+      working-directory: ${{ inputs.working-directory }}
+      run: |
+        # Re-check test results to fail the job if tests failed
+        if ($LASTEXITCODE -ne 0) {
+          Write-Host "Tests failed with exit code: $LASTEXITCODE"
+          exit $LASTEXITCODE
+        }

.github/workflows/ci.yml 🔗

@@ -511,8 +511,8 @@ jobs:
     runs-on:
       - self-mini-macos
     if: |
-      startsWith(github.ref, 'refs/tags/v')
-      || contains(github.event.pull_request.labels.*.name, 'run-bundling')
+      ( startsWith(github.ref, 'refs/tags/v')
+      || contains(github.event.pull_request.labels.*.name, 'run-bundling') )
     needs: [macos_tests]
     env:
       MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
@@ -526,6 +526,11 @@ jobs:
         with:
           node-version: "18"
 
+      - name: Setup Sentry CLI
+        uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b #v2
+        with:
+          token: ${{ SECRETS.SENTRY_AUTH_TOKEN }}
+
       - name: Checkout repo
         uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
         with:
@@ -599,8 +604,8 @@ jobs:
     runs-on:
       - namespace-profile-16x32-ubuntu-2004 # ubuntu 20.04 for minimal glibc
     if: |
-      startsWith(github.ref, 'refs/tags/v')
-      || contains(github.event.pull_request.labels.*.name, 'run-bundling')
+      ( startsWith(github.ref, 'refs/tags/v')
+      || contains(github.event.pull_request.labels.*.name, 'run-bundling') )
     needs: [linux_tests]
     steps:
       - name: Checkout repo
@@ -611,6 +616,11 @@ jobs:
       - name: Install Linux dependencies
         run: ./script/linux && ./script/install-mold 2.34.0
 
+      - name: Setup Sentry CLI
+        uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b #v2
+        with:
+          token: ${{ SECRETS.SENTRY_AUTH_TOKEN }}
+
       - name: Determine version and release channel
         if: startsWith(github.ref, 'refs/tags/v')
         run: |
@@ -650,7 +660,7 @@ jobs:
     timeout-minutes: 60
     name: Linux arm64 release bundle
     runs-on:
-      - namespace-profile-32x64-ubuntu-2004-arm # ubuntu 20.04 for minimal glibc
+      - namespace-profile-8x32-ubuntu-2004-arm-m4 # ubuntu 20.04 for minimal glibc
     if: |
       startsWith(github.ref, 'refs/tags/v')
       || contains(github.event.pull_request.labels.*.name, 'run-bundling')
@@ -664,6 +674,11 @@ jobs:
       - name: Install Linux dependencies
         run: ./script/linux
 
+      - name: Setup Sentry CLI
+        uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b #v2
+        with:
+          token: ${{ SECRETS.SENTRY_AUTH_TOKEN }}
+
       - name: Determine version and release channel
         if: startsWith(github.ref, 'refs/tags/v')
         run: |
@@ -703,10 +718,8 @@ jobs:
     timeout-minutes: 60
     runs-on: github-8vcpu-ubuntu-2404
     if: |
-      false && (
-      startsWith(github.ref, 'refs/tags/v')
-      || contains(github.event.pull_request.labels.*.name, 'run-bundling')
-      )
+      false && ( startsWith(github.ref, 'refs/tags/v')
+      || contains(github.event.pull_request.labels.*.name, 'run-bundling') )
     needs: [linux_tests]
     name: Build Zed on FreeBSD
     steps:
@@ -791,6 +804,11 @@ jobs:
         with:
           clean: false
 
+      - name: Setup Sentry CLI
+        uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b #v2
+        with:
+          token: ${{ SECRETS.SENTRY_AUTH_TOKEN }}
+
       - name: Determine version and release channel
         working-directory: ${{ env.ZED_WORKSPACE }}
         if: ${{ startsWith(github.ref, 'refs/tags/v') }}
@@ -833,3 +851,12 @@ jobs:
         run: gh release edit "$GITHUB_REF_NAME" --draft=false
         env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+      - name: Create Sentry release
+        uses: getsentry/action-release@526942b68292201ac6bbb99b9a0747d4abee354c # v3
+        env:
+          SENTRY_ORG: zed-dev
+          SENTRY_PROJECT: zed
+          SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
+        with:
+          environment: production

.github/workflows/release_nightly.yml 🔗

@@ -168,7 +168,7 @@ jobs:
     name: Create a Linux *.tar.gz bundle for ARM
     if: github.repository_owner == 'zed-industries'
     runs-on:
-      - namespace-profile-32x64-ubuntu-2004-arm # ubuntu 20.04 for minimal glibc
+      - namespace-profile-8x32-ubuntu-2004-arm-m4 # ubuntu 20.04 for minimal glibc
     needs: tests
     steps:
       - name: Checkout repo
@@ -206,9 +206,6 @@ jobs:
     runs-on: github-8vcpu-ubuntu-2404
     needs: tests
     name: Build Zed on FreeBSD
-    # env:
-    #   MYTOKEN : ${{ secrets.MYTOKEN }}
-    #   MYTOKEN2: "value2"
     steps:
       - uses: actions/checkout@v4
       - name: Build FreeBSD remote-server
@@ -243,7 +240,6 @@ jobs:
 
   bundle-nix:
     name: Build and cache Nix package
-    if: false
     needs: tests
     secrets: inherit
     uses: ./.github/workflows/nix.yml
@@ -316,3 +312,12 @@ jobs:
           git config user.email github-actions@github.com
           git tag -f nightly
           git push origin nightly --force
+
+      - name: Create Sentry release
+        uses: getsentry/action-release@526942b68292201ac6bbb99b9a0747d4abee354c # v3
+        env:
+          SENTRY_ORG: zed-dev
+          SENTRY_PROJECT: zed
+          SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
+        with:
+          environment: production

.github/workflows/unit_evals.yml 🔗

@@ -3,7 +3,7 @@ name: Run Unit Evals
 on:
   schedule:
     # GitHub might drop jobs at busy times, so we choose a random time in the middle of the night.
-    - cron: "47 1 * * *"
+    - cron: "47 1 * * 2"
   workflow_dispatch:
 
 concurrency:

Cargo.lock 🔗

@@ -6,12 +6,14 @@ version = 4
 name = "acp_thread"
 version = "0.1.0"
 dependencies = [
+ "action_log",
  "agent-client-protocol",
  "anyhow",
- "assistant_tool",
  "buffer_diff",
+ "collections",
  "editor",
  "env_logger 0.11.8",
+ "file_icons",
  "futures 0.3.31",
  "gpui",
  "indoc",
@@ -21,15 +23,46 @@ dependencies = [
  "markdown",
  "parking_lot",
  "project",
+ "prompt_store",
  "rand 0.8.5",
  "serde",
  "serde_json",
  "settings",
  "smol",
  "tempfile",
+ "terminal",
  "ui",
+ "url",
+ "util",
+ "uuid",
+ "watch",
+ "workspace-hack",
+]
+
+[[package]]
+name = "action_log"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "buffer_diff",
+ "clock",
+ "collections",
+ "ctor",
+ "futures 0.3.31",
+ "gpui",
+ "indoc",
+ "language",
+ "log",
+ "pretty_assertions",
+ "project",
+ "rand 0.8.5",
+ "serde_json",
+ "settings",
+ "text",
  "util",
+ "watch",
  "workspace-hack",
+ "zlog",
 ]
 
 [[package]]
@@ -84,6 +117,7 @@ dependencies = [
 name = "agent"
 version = "0.1.0"
 dependencies = [
+ "action_log",
  "agent_settings",
  "anyhow",
  "assistant_context",
@@ -96,7 +130,6 @@ dependencies = [
  "component",
  "context_server",
  "convert_case 0.8.0",
- "feature_flags",
  "fs",
  "futures 0.3.31",
  "git",
@@ -138,9 +171,9 @@ dependencies = [
 
 [[package]]
 name = "agent-client-protocol"
-version = "0.0.23"
+version = "0.0.30"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3fad72b7b8ee4331b3a4c8d43c107e982a4725564b4ee658ae5c4e79d2b486e8"
+checksum = "5f792e009ba59b137ee1db560bc37e567887ad4b5af6f32181d381fff690e2d4"
 dependencies = [
  "anyhow",
  "futures 0.3.31",
@@ -156,27 +189,44 @@ 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",
+ "chrono",
  "client",
  "clock",
  "cloud_llm_client",
  "collections",
+ "context_server",
  "ctor",
+ "db",
+ "editor",
  "env_logger 0.11.8",
  "fs",
  "futures 0.3.31",
+ "git",
  "gpui",
  "gpui_tokio",
  "handlebars 4.5.0",
+ "html_to_markdown",
+ "http_client",
  "indoc",
  "itertools 0.14.0",
  "language",
  "language_model",
  "language_models",
  "log",
+ "lsp",
+ "open",
+ "parking_lot",
+ "paths",
+ "portable-pty",
  "pretty_assertions",
  "project",
  "prompt_store",
@@ -187,12 +237,25 @@ dependencies = [
  "serde_json",
  "settings",
  "smol",
+ "sqlez",
+ "task",
+ "telemetry",
+ "tempfile",
+ "terminal",
+ "text",
+ "theme",
+ "tree-sitter-rust",
  "ui",
+ "unindent",
  "util",
  "uuid",
  "watch",
+ "web_search",
+ "which 6.0.3",
  "workspace-hack",
  "worktree",
+ "zlog",
+ "zstd",
 ]
 
 [[package]]
@@ -200,24 +263,33 @@ name = "agent_servers"
 version = "0.1.0"
 dependencies = [
  "acp_thread",
+ "action_log",
  "agent-client-protocol",
+ "agent_settings",
  "agentic-coding-protocol",
  "anyhow",
+ "client",
  "collections",
  "context_server",
  "env_logger 0.11.8",
+ "fs",
  "futures 0.3.31",
  "gpui",
+ "gpui_tokio",
  "indoc",
  "itertools 0.14.0",
  "language",
+ "language_model",
+ "language_models",
  "libc",
  "log",
  "nix 0.29.0",
  "paths",
  "project",
  "rand 0.8.5",
+ "reqwest_client",
  "schemars",
+ "semver",
  "serde",
  "serde_json",
  "settings",
@@ -257,6 +329,7 @@ name = "agent_ui"
 version = "0.1.0"
 dependencies = [
  "acp_thread",
+ "action_log",
  "agent",
  "agent-client-protocol",
  "agent2",
@@ -290,7 +363,6 @@ dependencies = [
  "gpui",
  "html_to_markdown",
  "http_client",
- "indexed_docs",
  "indoc",
  "inventory",
  "itertools 0.14.0",
@@ -338,6 +410,7 @@ dependencies = [
  "ui",
  "ui_input",
  "unindent",
+ "url",
  "urlencoding",
  "util",
  "uuid",
@@ -814,7 +887,6 @@ dependencies = [
  "gpui",
  "html_to_markdown",
  "http_client",
- "indexed_docs",
  "language",
  "pretty_assertions",
  "project",
@@ -838,13 +910,13 @@ dependencies = [
 name = "assistant_tool"
 version = "0.1.0"
 dependencies = [
+ "action_log",
  "anyhow",
  "buffer_diff",
  "clock",
  "collections",
  "ctor",
  "derive_more 0.99.19",
- "futures 0.3.31",
  "gpui",
  "icons",
  "indoc",
@@ -861,7 +933,6 @@ dependencies = [
  "settings",
  "text",
  "util",
- "watch",
  "workspace",
  "workspace-hack",
  "zlog",
@@ -871,6 +942,7 @@ dependencies = [
 name = "assistant_tools"
 version = "0.1.0"
 dependencies = [
+ "action_log",
  "agent_settings",
  "anyhow",
  "assistant_tool",
@@ -1204,26 +1276,6 @@ dependencies = [
  "syn 2.0.101",
 ]
 
-[[package]]
-name = "async-stripe"
-version = "0.40.0"
-source = "git+https://github.com/zed-industries/async-stripe?rev=3672dd4efb7181aa597bf580bf5a2f5d23db6735#3672dd4efb7181aa597bf580bf5a2f5d23db6735"
-dependencies = [
- "chrono",
- "futures-util",
- "http-types",
- "hyper 0.14.32",
- "hyper-rustls 0.24.2",
- "serde",
- "serde_json",
- "serde_path_to_error",
- "serde_qs 0.10.1",
- "smart-default 0.6.0",
- "smol_str 0.1.24",
- "thiserror 1.0.69",
- "tokio",
-]
-
 [[package]]
 name = "async-tar"
 version = "0.5.0"
@@ -1246,9 +1298,9 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
 
 [[package]]
 name = "async-trait"
-version = "0.1.88"
+version = "0.1.89"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5"
+checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1327,10 +1379,11 @@ version = "0.1.0"
 dependencies = [
  "anyhow",
  "collections",
- "derive_more 0.99.19",
  "gpui",
- "parking_lot",
  "rodio",
+ "schemars",
+ "serde",
+ "settings",
  "util",
  "workspace-hack",
 ]
@@ -2025,12 +2078,6 @@ version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce"
 
-[[package]]
-name = "base64"
-version = "0.13.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
-
 [[package]]
 name = "base64"
 version = "0.21.7"
@@ -3040,6 +3087,7 @@ dependencies = [
  "schemars",
  "serde",
  "serde_json",
+ "serde_urlencoded",
  "settings",
  "sha2",
  "smol",
@@ -3223,7 +3271,6 @@ dependencies = [
  "anyhow",
  "assistant_context",
  "assistant_slash_command",
- "async-stripe",
  "async-trait",
  "async-tungstenite",
  "audio",
@@ -3239,7 +3286,6 @@ dependencies = [
  "chrono",
  "client",
  "clock",
- "cloud_llm_client",
  "collab_ui",
  "collections",
  "command_palette_hooks",
@@ -3250,7 +3296,6 @@ dependencies = [
  "dap_adapters",
  "dashmap 6.1.0",
  "debugger_ui",
- "derive_more 0.99.19",
  "editor",
  "envy",
  "extension",
@@ -3266,7 +3311,6 @@ dependencies = [
  "http_client",
  "hyper 0.14.32",
  "indoc",
- "jsonwebtoken",
  "language",
  "language_model",
  "livekit_api",
@@ -3312,7 +3356,6 @@ dependencies = [
  "telemetry_events",
  "text",
  "theme",
- "thiserror 2.0.12",
  "time",
  "tokio",
  "toml 0.8.20",
@@ -3814,7 +3857,7 @@ dependencies = [
  "rustc-hash 1.1.0",
  "rustybuzz 0.14.1",
  "self_cell",
- "smol_str 0.2.2",
+ "smol_str",
  "swash",
  "sys-locale",
  "ttf-parser 0.21.1",
@@ -3836,7 +3879,7 @@ dependencies = [
  "jni",
  "js-sys",
  "libc",
- "mach2",
+ "mach2 0.4.2",
  "ndk",
  "ndk-context",
  "num-derive",
@@ -3986,7 +4029,7 @@ checksum = "031ed29858d90cfdf27fe49fae28028a1f20466db97962fa2f4ea34809aeebf3"
 dependencies = [
  "cfg-if",
  "libc",
- "mach2",
+ "mach2 0.4.2",
 ]
 
 [[package]]
@@ -3998,7 +4041,7 @@ dependencies = [
  "cfg-if",
  "crash-context",
  "libc",
- "mach2",
+ "mach2 0.4.2",
  "parking_lot",
 ]
 
@@ -4008,8 +4051,12 @@ version = "0.1.0"
 dependencies = [
  "crash-handler",
  "log",
+ "mach2 0.5.0",
  "minidumper",
  "paths",
+ "release_channel",
+ "serde",
+ "serde_json",
  "smol",
  "workspace-hack",
 ]
@@ -6317,17 +6364,6 @@ dependencies = [
  "windows-targets 0.48.5",
 ]
 
-[[package]]
-name = "getrandom"
-version = "0.1.16"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
-dependencies = [
- "cfg-if",
- "libc",
- "wasi 0.9.0+wasi-snapshot-preview1",
-]
-
 [[package]]
 name = "getrandom"
 version = "0.2.15"
@@ -6392,6 +6428,7 @@ dependencies = [
  "log",
  "parking_lot",
  "pretty_assertions",
+ "rand 0.8.5",
  "regex",
  "rope",
  "schemars",
@@ -7459,6 +7496,7 @@ dependencies = [
  "slotmap",
  "smallvec",
  "smol",
+ "stacksafe",
  "strum 0.27.1",
  "sum_tree",
  "taffy",
@@ -7823,6 +7861,12 @@ dependencies = [
  "windows-sys 0.59.0",
 ]
 
+[[package]]
+name = "hound"
+version = "3.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62adaabb884c94955b19907d60019f4e145d091c75345379e70d1ee696f7854f"
+
 [[package]]
 name = "html5ever"
 version = "0.27.0"
@@ -7924,27 +7968,6 @@ version = "0.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "add0ab9360ddbd88cfeb3bd9574a1d85cfdfa14db10b3e21d3700dbc4328758f"
 
-[[package]]
-name = "http-types"
-version = "2.12.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6e9b187a72d63adbfba487f48095306ac823049cb504ee195541e91c7775f5ad"
-dependencies = [
- "anyhow",
- "async-channel 1.9.0",
- "base64 0.13.1",
- "futures-lite 1.13.0",
- "http 0.2.12",
- "infer",
- "pin-project-lite",
- "rand 0.7.3",
- "serde",
- "serde_json",
- "serde_qs 0.8.5",
- "serde_urlencoded",
- "url",
-]
-
 [[package]]
 name = "http_client"
 version = "0.1.0"
@@ -8378,34 +8401,6 @@ version = "1.11.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d0263a3d970d5c054ed9312c0057b4f3bde9c0b33836d3637361d4a9e6e7a408"
 
-[[package]]
-name = "indexed_docs"
-version = "0.1.0"
-dependencies = [
- "anyhow",
- "async-trait",
- "cargo_metadata",
- "collections",
- "derive_more 0.99.19",
- "extension",
- "fs",
- "futures 0.3.31",
- "fuzzy",
- "gpui",
- "heed",
- "html_to_markdown",
- "http_client",
- "indexmap",
- "indoc",
- "parking_lot",
- "paths",
- "pretty_assertions",
- "serde",
- "strum 0.27.1",
- "util",
- "workspace-hack",
-]
-
 [[package]]
 name = "indexmap"
 version = "2.9.0"
@@ -8423,12 +8418,6 @@ version = "2.0.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd"
 
-[[package]]
-name = "infer"
-version = "0.2.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "64e9829a50b42bb782c1df523f78d332fe371b10c661e78b7a3c34b0198e9fac"
-
 [[package]]
 name = "inherent"
 version = "1.0.12"
@@ -9633,6 +9622,7 @@ version = "0.1.0"
 dependencies = [
  "anyhow",
  "async-trait",
+ "audio",
  "collections",
  "core-foundation 0.10.0",
  "core-video",
@@ -9651,9 +9641,11 @@ dependencies = [
  "objc",
  "parking_lot",
  "postage",
+ "rodio",
  "scap",
  "serde",
  "serde_json",
+ "settings",
  "sha2",
  "simplelog",
  "smallvec",
@@ -9884,6 +9876,15 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "mach2"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a1b95cd5421ec55b445b5ae102f5ea0e768de1f82bd3001e11f426c269c3aea"
+dependencies = [
+ "libc",
+]
+
 [[package]]
 name = "malloc_buf"
 version = "0.0.6"
@@ -10204,7 +10205,7 @@ dependencies = [
  "num-traits",
  "range-map",
  "scroll",
- "smart-default 0.7.1",
+ "smart-default",
 ]
 
 [[package]]
@@ -10220,7 +10221,7 @@ dependencies = [
  "goblin",
  "libc",
  "log",
- "mach2",
+ "mach2 0.4.2",
  "memmap2",
  "memoffset",
  "minidump-common",
@@ -11096,14 +11097,13 @@ dependencies = [
  "ai_onboarding",
  "anyhow",
  "client",
- "command_palette_hooks",
  "component",
  "db",
  "documented",
  "editor",
- "feature_flags",
  "fs",
  "fuzzy",
+ "git",
  "gpui",
  "itertools 0.14.0",
  "language",
@@ -11115,6 +11115,7 @@ dependencies = [
  "schemars",
  "serde",
  "settings",
+ "telemetry",
  "theme",
  "ui",
  "util",
@@ -11190,6 +11191,7 @@ dependencies = [
  "anyhow",
  "futures 0.3.31",
  "http_client",
+ "log",
  "schemars",
  "serde",
  "serde_json",
@@ -13077,19 +13079,6 @@ version = "0.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
 
-[[package]]
-name = "rand"
-version = "0.7.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
-dependencies = [
- "getrandom 0.1.16",
- "libc",
- "rand_chacha 0.2.2",
- "rand_core 0.5.1",
- "rand_hc",
-]
-
 [[package]]
 name = "rand"
 version = "0.8.5"
@@ -13111,16 +13100,6 @@ dependencies = [
  "rand_core 0.9.3",
 ]
 
-[[package]]
-name = "rand_chacha"
-version = "0.2.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402"
-dependencies = [
- "ppv-lite86",
- "rand_core 0.5.1",
-]
-
 [[package]]
 name = "rand_chacha"
 version = "0.3.1"
@@ -13141,15 +13120,6 @@ dependencies = [
  "rand_core 0.9.3",
 ]
 
-[[package]]
-name = "rand_core"
-version = "0.5.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
-dependencies = [
- "getrandom 0.1.16",
-]
-
 [[package]]
 name = "rand_core"
 version = "0.6.4"
@@ -13168,15 +13138,6 @@ dependencies = [
  "getrandom 0.3.2",
 ]
 
-[[package]]
-name = "rand_hc"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
-dependencies = [
- "rand_core 0.5.1",
-]
-
 [[package]]
 name = "range-map"
 version = "0.2.0"
@@ -13518,6 +13479,7 @@ dependencies = [
 name = "remote_server"
 version = "0.1.0"
 dependencies = [
+ "action_log",
  "anyhow",
  "askpass",
  "assistant_tool",
@@ -13910,6 +13872,7 @@ checksum = "e40ecf59e742e03336be6a3d53755e789fd05a059fa22dfa0ed624722319e183"
 dependencies = [
  "cpal",
  "dasp_sample",
+ "hound",
  "num-rational",
  "symphonia",
  "tracing",
@@ -14829,28 +14792,6 @@ dependencies = [
  "serde",
 ]
 
-[[package]]
-name = "serde_qs"
-version = "0.8.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c7715380eec75f029a4ef7de39a9200e0a63823176b759d055b613f5a87df6a6"
-dependencies = [
- "percent-encoding",
- "serde",
- "thiserror 1.0.69",
-]
-
-[[package]]
-name = "serde_qs"
-version = "0.10.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8cac3f1e2ca2fe333923a1ae72caca910b98ed0630bb35ef6f8c8517d6e81afa"
-dependencies = [
- "percent-encoding",
- "serde",
- "thiserror 1.0.69",
-]
-
 [[package]]
 name = "serde_repr"
 version = "0.1.20"
@@ -14992,8 +14933,10 @@ dependencies = [
  "ui",
  "ui_input",
  "util",
+ "vim",
  "workspace",
  "workspace-hack",
+ "zed_actions",
 ]
 
 [[package]]
@@ -15225,17 +15168,6 @@ dependencies = [
  "serde",
 ]
 
-[[package]]
-name = "smart-default"
-version = "0.6.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "133659a15339456eeeb07572eb02a91c91e9815e9cbc89566944d2c8d3efdbf6"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 1.0.109",
-]
-
 [[package]]
 name = "smart-default"
 version = "0.7.1"
@@ -15264,15 +15196,6 @@ dependencies = [
  "futures-lite 2.6.0",
 ]
 
-[[package]]
-name = "smol_str"
-version = "0.1.24"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fad6c857cbab2627dcf01ec85a623ca4e7dcb5691cbaa3d7fb7653671f0d09c9"
-dependencies = [
- "serde",
-]
-
 [[package]]
 name = "smol_str"
 version = "0.2.2"
@@ -15644,6 +15567,40 @@ version = "1.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
 
+[[package]]
+name = "stacker"
+version = "0.1.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cddb07e32ddb770749da91081d8d0ac3a16f1a569a18b20348cd371f5dead06b"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "libc",
+ "psm",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "stacksafe"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d9c1172965d317e87ddb6d364a040d958b40a1db82b6ef97da26253a8b3d090"
+dependencies = [
+ "stacker",
+ "stacksafe-macro",
+]
+
+[[package]]
+name = "stacksafe-macro"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "172175341049678163e979d9107ca3508046d4d2a7c6682bee46ac541b17db69"
+dependencies = [
+ "proc-macro-error2",
+ "quote",
+ "syn 2.0.101",
+]
+
 [[package]]
 name = "static_assertions"
 version = "1.1.0"
@@ -17974,6 +17931,7 @@ dependencies = [
  "command_palette_hooks",
  "db",
  "editor",
+ "env_logger 0.11.8",
  "futures 0.3.31",
  "git_ui",
  "gpui",
@@ -18120,12 +18078,6 @@ dependencies = [
  "tracing",
 ]
 
-[[package]]
-name = "wasi"
-version = "0.9.0+wasi-snapshot-preview1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
-
 [[package]]
 name = "wasi"
 version = "0.11.0+wasi-snapshot-preview1"
@@ -18359,7 +18311,7 @@ dependencies = [
  "indexmap",
  "libc",
  "log",
- "mach2",
+ "mach2 0.4.2",
  "memfd",
  "object",
  "once_cell",
@@ -18827,33 +18779,6 @@ version = "0.1.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082"
 
-[[package]]
-name = "welcome"
-version = "0.1.0"
-dependencies = [
- "anyhow",
- "client",
- "component",
- "db",
- "documented",
- "editor",
- "fuzzy",
- "gpui",
- "install_cli",
- "language",
- "picker",
- "project",
- "serde",
- "settings",
- "telemetry",
- "ui",
- "util",
- "vim_mode_setting",
- "workspace",
- "workspace-hack",
- "zed_actions",
-]
-
 [[package]]
 name = "which"
 version = "4.4.2"
@@ -20239,7 +20164,7 @@ dependencies = [
 [[package]]
 name = "xim"
 version = "0.4.0"
-source = "git+https://github.com/XDeme1/xim-rs?rev=d50d461764c2213655cd9cf65a0ea94c70d3c4fd#d50d461764c2213655cd9cf65a0ea94c70d3c4fd"
+source = "git+https://github.com/zed-industries/xim-rs?rev=c0a70c1bd2ce197364216e5e818a2cb3adb99a8d#c0a70c1bd2ce197364216e5e818a2cb3adb99a8d"
 dependencies = [
  "ahash 0.8.11",
  "hashbrown 0.14.5",
@@ -20252,7 +20177,7 @@ dependencies = [
 [[package]]
 name = "xim-ctext"
 version = "0.3.0"
-source = "git+https://github.com/XDeme1/xim-rs?rev=d50d461764c2213655cd9cf65a0ea94c70d3c4fd#d50d461764c2213655cd9cf65a0ea94c70d3c4fd"
+source = "git+https://github.com/zed-industries/xim-rs?rev=c0a70c1bd2ce197364216e5e818a2cb3adb99a8d#c0a70c1bd2ce197364216e5e818a2cb3adb99a8d"
 dependencies = [
  "encoding_rs",
 ]
@@ -20260,7 +20185,7 @@ dependencies = [
 [[package]]
 name = "xim-parser"
 version = "0.2.1"
-source = "git+https://github.com/XDeme1/xim-rs?rev=d50d461764c2213655cd9cf65a0ea94c70d3c4fd#d50d461764c2213655cd9cf65a0ea94c70d3c4fd"
+source = "git+https://github.com/zed-industries/xim-rs?rev=c0a70c1bd2ce197364216e5e818a2cb3adb99a8d#c0a70c1bd2ce197364216e5e818a2cb3adb99a8d"
 dependencies = [
  "bitflags 2.9.0",
 ]
@@ -20336,8 +20261,9 @@ checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
 
 [[package]]
 name = "yawc"
-version = "0.2.4"
-source = "git+https://github.com/deviant-forks/yawc?rev=1899688f3e69ace4545aceb97b2a13881cf26142#1899688f3e69ace4545aceb97b2a13881cf26142"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19a5d82922135b4ae73a079a4ffb5501e9aadb4d785b8c660eaa0a8b899028c5"
 dependencies = [
  "base64 0.22.1",
  "bytes 1.10.1",
@@ -20468,7 +20394,7 @@ dependencies = [
 
 [[package]]
 name = "zed"
-version = "0.200.0"
+version = "0.202.0"
 dependencies = [
  "activity_indicator",
  "agent",
@@ -20538,6 +20464,7 @@ dependencies = [
  "language_tools",
  "languages",
  "libc",
+ "livekit_client",
  "log",
  "markdown",
  "markdown_preview",
@@ -20608,7 +20535,6 @@ dependencies = [
  "watch",
  "web_search",
  "web_search_providers",
- "welcome",
  "windows 0.61.1",
  "winresource",
  "workspace",
@@ -20630,13 +20556,6 @@ dependencies = [
  "workspace-hack",
 ]
 
-[[package]]
-name = "zed_emmet"
-version = "0.0.4"
-dependencies = [
- "zed_extension_api 0.1.0",
-]
-
 [[package]]
 name = "zed_extension_api"
 version = "0.1.0"
@@ -20871,6 +20790,7 @@ dependencies = [
  "menu",
  "postage",
  "project",
+ "rand 0.8.5",
  "regex",
  "release_channel",
  "reqwest_client",

Cargo.toml 🔗

@@ -2,6 +2,7 @@
 resolver = "2"
 members = [
     "crates/acp_thread",
+    "crates/action_log",
     "crates/activity_indicator",
     "crates/agent",
     "crates/agent2",
@@ -80,7 +81,6 @@ members = [
     "crates/http_client_tls",
     "crates/icons",
     "crates/image_viewer",
-    "crates/indexed_docs",
     "crates/edit_prediction",
     "crates/edit_prediction_button",
     "crates/inspector_ui",
@@ -184,7 +184,6 @@ members = [
     "crates/watch",
     "crates/web_search",
     "crates/web_search_providers",
-    "crates/welcome",
     "crates/workspace",
     "crates/worktree",
     "crates/x_ai",
@@ -199,7 +198,6 @@ members = [
     # Extensions
     #
 
-    "extensions/emmet",
     "extensions/glsl",
     "extensions/html",
     "extensions/proto",
@@ -229,6 +227,7 @@ edition = "2024"
 #
 
 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" }
@@ -305,7 +304,6 @@ http_client = { path = "crates/http_client" }
 http_client_tls = { path = "crates/http_client_tls" }
 icons = { path = "crates/icons" }
 image_viewer = { path = "crates/image_viewer" }
-indexed_docs = { path = "crates/indexed_docs" }
 edit_prediction = { path = "crates/edit_prediction" }
 edit_prediction_button = { path = "crates/edit_prediction_button" }
 inspector_ui = { path = "crates/inspector_ui" }
@@ -362,6 +360,7 @@ remote_server = { path = "crates/remote_server" }
 repl = { path = "crates/repl" }
 reqwest_client = { path = "crates/reqwest_client" }
 rich_text = { path = "crates/rich_text" }
+rodio = { version = "0.21.1", default-features = false }
 rope = { path = "crates/rope" }
 rpc = { path = "crates/rpc" }
 rules_library = { path = "crates/rules_library" }
@@ -410,7 +409,6 @@ vim_mode_setting = { path = "crates/vim_mode_setting" }
 watch = { path = "crates/watch" }
 web_search = { path = "crates/web_search" }
 web_search_providers = { path = "crates/web_search_providers" }
-welcome = { path = "crates/welcome" }
 workspace = { path = "crates/workspace" }
 worktree = { path = "crates/worktree" }
 x_ai = { path = "crates/x_ai" }
@@ -425,7 +423,7 @@ zlog_settings = { path = "crates/zlog_settings" }
 #
 
 agentic-coding-protocol = "0.0.10"
-agent-client-protocol = { version = "0.0.23" }
+agent-client-protocol = "0.0.30"
 aho-corasick = "1.1"
 alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" }
 any_vec = "0.14"
@@ -517,6 +515,7 @@ libsqlite3-sys = { version = "0.30.1", features = ["bundled"] }
 linkify = "0.10.0"
 log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] }
 lsp-types = { git = "https://github.com/zed-industries/lsp-types", rev = "39f629bdd03d59abd786ed9fc27e8bca02c0c0ec" }
+mach2 = "0.5"
 markup5ever_rcdom = "0.3.0"
 metal = "0.29"
 minidumper = "0.8"
@@ -584,6 +583,7 @@ serde_json_lenient = { version = "0.2", features = [
     "raw_value",
 ] }
 serde_repr = "0.1"
+serde_urlencoded = "0.7"
 sha2 = "0.10"
 shellexpand = "2.1.0"
 shlex = "1.3.0"
@@ -591,6 +591,7 @@ simplelog = "0.12.2"
 smallvec = { version = "1.6", features = ["union"] }
 smol = "2.0"
 sqlformat = "0.2"
+stacksafe = "0.1"
 streaming-iterator = "0.1"
 strsim = "0.11"
 strum = { version = "0.27.0", features = ["derive"] }
@@ -661,25 +662,9 @@ which = "6.0.0"
 windows-core = "0.61"
 wit-component = "0.221"
 workspace-hack = "0.1.0"
-# We can switch back to the published version once https://github.com/infinitefield/yawc/pull/16 is merged and a new
-# version is released.
-yawc = { git = "https://github.com/deviant-forks/yawc", rev = "1899688f3e69ace4545aceb97b2a13881cf26142" }
+yawc = "0.2.5"
 zstd = "0.11"
 
-[workspace.dependencies.async-stripe]
-git = "https://github.com/zed-industries/async-stripe"
-rev = "3672dd4efb7181aa597bf580bf5a2f5d23db6735"
-default-features = false
-features = [
-    "runtime-tokio-hyper-rustls",
-    "billing",
-    "checkout",
-    "events",
-    # The features below are only enabled to get the `events` feature to build.
-    "chrono",
-    "connect",
-]
-
 [workspace.dependencies.windows]
 version = "0.61"
 features = [
@@ -712,6 +697,7 @@ features = [
     "Win32_System_LibraryLoader",
     "Win32_System_Memory",
     "Win32_System_Ole",
+    "Win32_System_Performance",
     "Win32_System_Pipes",
     "Win32_System_SystemInformation",
     "Win32_System_SystemServices",
@@ -816,38 +802,33 @@ unexpected_cfgs = { level = "allow" }
 dbg_macro = "deny"
 todo = "deny"
 
-# Motivation: We use `vec![a..b]` a lot when dealing with ranges in text, so
-# warning on this rule produces a lot of noise.
-single_range_in_vec_init = "allow"
+# This is not a style lint, see https://github.com/rust-lang/rust-clippy/pull/15454
+# Remove when the lint gets promoted to `suspicious`.
+declare_interior_mutable_const = "deny"
 
-# These are all of the rules that currently have violations in the Zed
-# codebase.
+redundant_clone = "deny"
+
+# We currently do not restrict any style rules
+# as it slows down shipping code to Zed.
 #
-# We'll want to drive this list down by either:
-# 1. fixing violations of the rule and begin enforcing it
-# 2. deciding we want to allow the rule permanently, at which point
-#    we should codify that separately above.
+# Running ./script/clippy can take several minutes, and so it's
+# common to skip that step and let CI do it. Any unexpected failures
+# (which also take minutes to discover) thus require switching back
+# to an old branch, manual fixing, and re-pushing.
 #
-# This list shouldn't be added to; it should only get shorter.
-# =============================================================================
-
-# There are a bunch of rules currently failing in the `style` group, so
-# allow all of those, for now.
+# In the future we could improve this by either making sure
+# Zed can surface clippy errors in diagnostics (in addition to the
+# rust-analyzer errors), or by having CI fix style nits automatically.
 style = { level = "allow", priority = -1 }
 
-# Temporary list of style lints that we've fixed so far.
-module_inception = { level = "deny" }
-question_mark = { level = "deny" }
-redundant_closure = { level = "deny" }
 # Individual rules that have violations in the codebase:
 type_complexity = "allow"
-# We often return trait objects from `new` functions.
-new_ret_no_self = { level = "allow" }
-# We have a few `next` functions that differ in lifetimes
-# compared to Iterator::next. Yet, clippy complains about those.
-should_implement_trait = { level = "allow" }
 let_underscore_future = "allow"
 
+# Motivation: We use `vec![a..b]` a lot when dealing with ranges in text, so
+# warning on this rule produces a lot of noise.
+single_range_in_vec_init = "allow"
+
 # in Rust it can be very tedious to reduce argument count without
 # running afoul of the borrow checker.
 too_many_arguments = "allow"

Dockerfile-collab 🔗

@@ -1,6 +1,6 @@
 # syntax = docker/dockerfile:1.2
 
-FROM rust:1.88-bookworm as builder
+FROM rust:1.89-bookworm as builder
 WORKDIR app
 COPY . .
 

assets/fonts/plex-sans/license.txt → assets/fonts/lilex/OFL.txt 🔗

@@ -1,8 +1,9 @@
-Copyright © 2017 IBM Corp. with Reserved Font Name "Plex"
+Copyright 2019 The Lilex Project Authors (https://github.com/mishamyrt/Lilex)
 
 This Font Software is licensed under the SIL Open Font License, Version 1.1.
 This license is copied below, and is also available with a FAQ at:
-http://scripts.sil.org/OFL
+https://scripts.sil.org/OFL
+
 
 -----------------------------------------------------------
 SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
@@ -89,4 +90,4 @@ COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
 DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
-OTHER DEALINGS IN THE FONT SOFTWARE.
+OTHER DEALINGS IN THE FONT SOFTWARE.

assets/icons/ai.svg 🔗

@@ -1,5 +1,5 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-    <path d="M10.5 8.75V10.5C8.43097 10.5 7.56903 10.5 5.5 10.5V10L10.5 6V5.5H5.5V7.25" stroke="black" stroke-width="1.5"/>
+    <path d="M10.5 8.75V10.5C8.43097 10.5 7.56903 10.5 5.5 10.5V10L10.5 6V5.5H5.5V7.25" stroke="black" stroke-width="1.2"/>
     <path d="M1.5 8.5C1.77614 8.5 2 8.27614 2 8C2 7.72386 1.77614 7.5 1.5 7.5C1.22386 7.5 1 7.72386 1 8C1 8.27614 1.22386 8.5 1.5 8.5Z" fill="black"/>
     <path d="M2.49976 6.33002C2.7759 6.33002 2.99976 6.10616 2.99976 5.83002C2.99976 5.55387 2.7759 5.33002 2.49976 5.33002C2.22361 5.33002 1.99976 5.55387 1.99976 5.83002C1.99976 6.10616 2.22361 6.33002 2.49976 6.33002Z" fill="black"/>
     <path d="M2.49976 10.66C2.7759 10.66 2.99976 10.4361 2.99976 10.16C2.99976 9.88383 2.7759 9.65997 2.49976 9.65997C2.22361 9.65997 1.99976 9.88383 1.99976 10.16C1.99976 10.4361 2.22361 10.66 2.49976 10.66Z" fill="black"/>

assets/icons/arrow_circle.svg 🔗

@@ -1,6 +1,6 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M3 8C3 6.67392 3.52678 5.40215 4.46446 4.46447C5.40214 3.52679 6.67391 3.00001 7.99999 3.00001C9.39779 3.00527 10.7394 3.55069 11.7444 4.52223L13 5.77778" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M13 3.00001V5.77778H10.2222" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M13 8C13 9.32608 12.4732 10.5978 11.5355 11.5355C10.5978 12.4732 9.32607 13 7.99999 13C6.60219 12.9947 5.26054 12.4493 4.25555 11.4778L3 10.2222" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M5.77777 10.2222H3V13" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M12.8989 5.77778L11.6434 4.52222C10.6384 3.55068 9.29673 3.00526 7.89893 3C6.57285 3 5.30103 3.52678 4.36343 4.46447C3.78887 5.03901 3.36856 5.73897 3.12921 6.5" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M12.8989 3V5.77778H10.1211" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M3.1012 10.2222L4.3568 11.4778C5.3618 12.4493 6.70342 12.9947 8.10122 13C9.42731 13 10.6991 12.4732 11.6368 11.5355C12.2163 10.956 12.6389 10.2487 12.8772 9.47994" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M5.87891 10.2222H3.10111V13" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/arrow_down.svg 🔗

@@ -1,3 +1,3 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M8.00001 12L3.5 7.50001M8.00001 12L12.5 7.50001M8.00001 12L8.00001 3.00001" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M8 13L12.5 8.5M8 13L3.5 8.5M8 13V3" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/arrow_down10.svg 🔗

@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-down10-icon lucide-arrow-down-1-0"><path d="m3 16 4 4 4-4"/><path d="M7 20V4"/><path d="M17 10V4h-2"/><path d="M15 10h4"/><rect x="15" y="14" width="4" height="6" ry="2"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="m2.5 10.667 2.667 2.666 2.666-2.666M5.167 13.333V2.667M11.833 6.667v-4H10.5M10.5 6.667h2.667M13.167 10.667a1.333 1.333 0 0 0-2.667 0V12a1.333 1.333 0 0 0 2.667 0v-1.333Z"/></svg>

assets/icons/arrow_down_from_line.svg 🔗

@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-down-from-line"><path d="M19 3H5"/><path d="M12 21V7"/><path d="m6 15 6 6 6-6"/></svg>

assets/icons/arrow_down_right.svg 🔗

@@ -1 +1,4 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-down-right-icon lucide-arrow-down-right"><path d="m7 7 10 10"/><path d="M17 7v10H7"/></svg>
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M4.25 4.25L11.125 11.125" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M11.75 4.25006V11.7501H4.25" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

assets/icons/arrow_left.svg 🔗

@@ -1,3 +1,3 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M3.5 7.50001L8 3M3.5 7.50001L8 12M3.5 7.50001H12.5" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M3.5 7.50001L8 3M3.5 7.50001L8 12M3.5 7.50001H12.5" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/arrow_right.svg 🔗

@@ -1,3 +1,3 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M12.5 7.5L8 12M12.5 7.5L8 3M12.5 7.5L3.5 7.5" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M12.5 8L8 12.5M12.5 8L8 3.5M12.5 8H3.5" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/arrow_right_left.svg 🔗

@@ -1 +1,6 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-right-left"><path d="m16 3 4 4-4 4"/><path d="M20 7H4"/><path d="m8 21-4-4 4-4"/><path d="M4 17h16"/></svg>
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M11 2L13 4.5L11 7" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M12.5 4.5H2.5" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M5 14L3 11.5L5 9" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M3 11.5H13" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

assets/icons/arrow_up.svg 🔗

@@ -1,3 +1,3 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M7.99999 3.00001L12.5 7.50001M7.99999 3.00001L3.49999 7.50001M7.99999 3.00001L7.99999 12" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M8 3L12.5 7.5M8 3L3.5 7.5M8 3V13" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/arrow_up_alt.svg 🔗

@@ -1,3 +0,0 @@
-<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M8 3.5L12.5 8M8 3.5L3.5 8M8 3.5V12.5" stroke="black" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
-</svg>

assets/icons/arrow_up_from_line.svg 🔗

@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-up-from-line"><path d="m18 9-6-6-6 6"/><path d="M12 3v14"/><path d="M5 21h14"/></svg>

assets/icons/arrow_up_right.svg 🔗

@@ -1 +1,4 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-up-right-icon lucide-arrow-up-right"><path d="M7 7h10v10"/><path d="M7 17 17 7"/></svg>
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M4.5 11.5L11.5 4.5" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M4.5 4.5L11.5 4.5L11.5 11.5" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

assets/icons/arrow_up_right_alt.svg 🔗

@@ -1,3 +0,0 @@
-<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M5.4 2.6H5.75C5.75 2.50717 5.71312 2.41815 5.64749 2.35251C5.58185 2.28688 5.49283 2.25 5.4 2.25V2.6ZM2.6 2.25C2.4067 2.25 2.25 2.4067 2.25 2.6C2.25 2.7933 2.4067 2.95 2.6 2.95V2.25ZM5.05 5.4C5.05 5.5933 5.2067 5.75 5.4 5.75C5.5933 5.75 5.75 5.5933 5.75 5.4H5.05ZM2.35252 5.15251C2.21583 5.2892 2.21583 5.5108 2.35252 5.64748C2.4892 5.78417 2.7108 5.78417 2.84749 5.64748L2.35252 5.15251ZM5.4 2.25H2.6V2.95H5.4V2.25ZM5.05 2.6V5.4H5.75V2.6H5.05ZM5.15252 2.35251L2.35252 5.15251L2.84749 5.64748L5.64749 2.84748L5.15252 2.35251Z" fill="black"/>
-</svg>

assets/icons/audio_off.svg 🔗

@@ -1,7 +1,7 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M10.6667 6C11.003 6.44823 11.2208 6.97398 11.3001 7.52867" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M12.9094 3.75732C13.7621 4.6095 14.3383 5.69876 14.5629 6.88315C14.7875 8.06754 14.6502 9.29213 14.1688 10.3973" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M2.66675 2L13.6667 13" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M5.33333 4.66669L4.942 5.05802C4.85494 5.1456 4.75136 5.21504 4.63726 5.2623C4.52317 5.30957 4.40083 5.33372 4.27733 5.33335H2.66667C2.48986 5.33335 2.32029 5.40359 2.19526 5.52862C2.07024 5.65364 2 5.82321 2 6.00002V10C2 10.1768 2.07024 10.3464 2.19526 10.4714C2.32029 10.5964 2.48986 10.6667 2.66667 10.6667H4.27733C4.40083 10.6663 4.52317 10.6905 4.63726 10.7377C4.75136 10.785 4.85494 10.8544 4.942 10.942L7.19733 13.198C7.26307 13.2639 7.34687 13.3088 7.43813 13.3269C7.52939 13.3451 7.62399 13.3358 7.70995 13.3002C7.79591 13.2646 7.86936 13.2042 7.921 13.1268C7.97263 13.0494 8.00013 12.9584 8 12.8654V7.33335" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M7.21875 2.78136C7.28267 2.71719 7.36421 2.67345 7.45303 2.65568C7.54184 2.63791 7.63393 2.64691 7.71762 2.68154C7.80132 2.71618 7.87284 2.77488 7.92312 2.85022C7.97341 2.92555 8.0002 3.01412 8.00008 3.10469V3.56202" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M10.6667 6C11.003 6.44823 11.2208 6.97398 11.3001 7.52867" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M12.9094 3.75732C13.7621 4.6095 14.3383 5.69876 14.5629 6.88315C14.7875 8.06754 14.6502 9.29213 14.1688 10.3973" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M2.66675 2L13.6667 13" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M5.33333 4.66669L4.942 5.05802C4.85494 5.1456 4.75136 5.21504 4.63726 5.2623C4.52317 5.30957 4.40083 5.33372 4.27733 5.33335H2.66667C2.48986 5.33335 2.32029 5.40359 2.19526 5.52862C2.07024 5.65364 2 5.82321 2 6.00002V10C2 10.1768 2.07024 10.3464 2.19526 10.4714C2.32029 10.5964 2.48986 10.6667 2.66667 10.6667H4.27733C4.40083 10.6663 4.52317 10.6905 4.63726 10.7377C4.75136 10.785 4.85494 10.8544 4.942 10.942L7.19733 13.198C7.26307 13.2639 7.34687 13.3088 7.43813 13.3269C7.52939 13.3451 7.62399 13.3358 7.70995 13.3002C7.79591 13.2646 7.86936 13.2042 7.921 13.1268C7.97263 13.0494 8.00013 12.9584 8 12.8654V7.33335" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M7.21875 2.78136C7.28267 2.71719 7.36421 2.67345 7.45303 2.65568C7.54184 2.63791 7.63393 2.64691 7.71762 2.68154C7.80132 2.71618 7.87284 2.77488 7.92312 2.85022C7.97341 2.92555 8.0002 3.01412 8.00008 3.10469V3.56202" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/audio_on.svg 🔗

@@ -1,5 +1,5 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M8 3.13467C7.99987 3.04181 7.97223 2.95107 7.92057 2.8739C7.86892 2.79674 7.79557 2.7366 7.70977 2.70108C7.62397 2.66557 7.52958 2.65626 7.43849 2.67434C7.34741 2.69242 7.26373 2.73707 7.198 2.80266L4.942 5.058C4.85494 5.14558 4.75136 5.21502 4.63726 5.26228C4.52317 5.30954 4.40083 5.33369 4.27733 5.33333H2.66667C2.48986 5.33333 2.32029 5.40357 2.19526 5.52859C2.07024 5.65362 2 5.82319 2 6V10C2 10.1768 2.07024 10.3464 2.19526 10.4714C2.32029 10.5964 2.48986 10.6667 2.66667 10.6667H4.27733C4.40083 10.6663 4.52317 10.6905 4.63726 10.7377C4.75136 10.785 4.85494 10.8544 4.942 10.942L7.19733 13.198C7.26307 13.2639 7.34687 13.3087 7.43813 13.3269C7.52939 13.3451 7.62399 13.3358 7.70995 13.3002C7.79591 13.2645 7.86936 13.2042 7.921 13.1268C7.97263 13.0494 8.00013 12.9584 8 12.8653V3.13467Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M10.6667 6C11.0995 6.57699 11.3334 7.27877 11.3334 8C11.3334 8.72123 11.0995 9.42301 10.6667 10" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M12.9094 12.2427C13.4666 11.6855 13.9085 11.0241 14.2101 10.2961C14.5116 9.56815 14.6668 8.78793 14.6668 7.99999C14.6668 7.21205 14.5116 6.43183 14.2101 5.70387C13.9085 4.97591 13.4666 4.31448 12.9094 3.75732" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M8 3.13467C7.99987 3.04181 7.97223 2.95107 7.92057 2.8739C7.86892 2.79674 7.79557 2.7366 7.70977 2.70108C7.62397 2.66557 7.52958 2.65626 7.43849 2.67434C7.34741 2.69242 7.26373 2.73707 7.198 2.80266L4.942 5.058C4.85494 5.14558 4.75136 5.21502 4.63726 5.26228C4.52317 5.30954 4.40083 5.33369 4.27733 5.33333H2.66667C2.48986 5.33333 2.32029 5.40357 2.19526 5.52859C2.07024 5.65362 2 5.82319 2 6V10C2 10.1768 2.07024 10.3464 2.19526 10.4714C2.32029 10.5964 2.48986 10.6667 2.66667 10.6667H4.27733C4.40083 10.6663 4.52317 10.6905 4.63726 10.7377C4.75136 10.785 4.85494 10.8544 4.942 10.942L7.19733 13.198C7.26307 13.2639 7.34687 13.3087 7.43813 13.3269C7.52939 13.3451 7.62399 13.3358 7.70995 13.3002C7.79591 13.2645 7.86936 13.2042 7.921 13.1268C7.97263 13.0494 8.00013 12.9584 8 12.8653V3.13467Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M10.6667 6C11.0995 6.57699 11.3334 7.27877 11.3334 8C11.3334 8.72123 11.0995 9.42301 10.6667 10" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M12.9094 12.2427C13.4666 11.6855 13.9085 11.0241 14.2101 10.2961C14.5116 9.56815 14.6668 8.78793 14.6668 7.99999C14.6668 7.21205 14.5116 6.43183 14.2101 5.70387C13.9085 4.97591 13.4666 4.31448 12.9094 3.75732" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/backspace.svg 🔗

@@ -1,3 +1,5 @@
-<svg width="15" height="11" viewBox="0 0 15 11" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M5.24432 11L0.183239 5.90909L5.24432 0.818182H14.75V11H5.24432ZM5.68679 9.90625H13.6761V1.91193H5.68679L1.70952 5.90909L5.68679 9.90625ZM11.7223 8.15625L10.9964 8.89205L5.75639 3.66193L6.48224 2.92614L11.7223 8.15625ZM6.48224 8.89205L5.75639 8.15625L10.9964 2.92614L11.7223 3.66193L6.48224 8.89205Z" fill="black"/>
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M6.79998 4C6.50183 4.00002 6.21436 4.10574 5.99358 4.29657L2.19677 7.57657C2.1348 7.63013 2.08528 7.69545 2.05139 7.76832C2.01751 7.8412 2 7.92001 2 7.99971C2 8.07941 2.01751 8.15823 2.05139 8.23111C2.08528 8.30398 2.1348 8.36929 2.19677 8.42286L5.99358 11.7034C6.21436 11.8943 6.50183 12 6.79998 12H12.8C13.1183 12 13.4235 11.8796 13.6485 11.6653C13.8736 11.4509 14 11.1602 14 10.8571V5.14286C14 4.83975 13.8736 4.54906 13.6485 4.33474C13.4235 4.12041 13.1183 4 12.8 4H6.79998Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M8.5 6.5L11.5 9.5" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M11.5 6.5L8.5 9.5" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/bell.svg 🔗

@@ -1,4 +1,4 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-    <path d="M5.33333 5.8C5.33333 5.05739 5.61429 4.3452 6.11438 3.8201C6.61448 3.295 7.29276 3 8 3C8.70724 3 9.38552 3.295 9.88562 3.8201C10.3857 4.3452 10.6667 5.05739 10.6667 5.8C10.6667 9.06667 12 10 12 10H4C4 10 5.33333 9.06667 5.33333 5.8Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-    <path d="M6 12.5C6.19692 12.8028 6.48641 13.0554 6.83822 13.2313C7.19004 13.4072 7.59127 13.5 8 13.5C8.40873 13.5 8.80996 13.4072 9.16178 13.2313C9.51359 13.0554 9.80308 12.8028 10 12.5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+    <path d="M5.33333 5.8C5.33333 5.05739 5.61429 4.3452 6.11438 3.8201C6.61448 3.295 7.29276 3 8 3C8.70724 3 9.38552 3.295 9.88562 3.8201C10.3857 4.3452 10.6667 5.05739 10.6667 5.8C10.6667 9.06667 12 10 12 10H4C4 10 5.33333 9.06667 5.33333 5.8Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+    <path d="M6 12.5C6.19692 12.8028 6.48641 13.0554 6.83822 13.2313C7.19004 13.4072 7.59127 13.5 8 13.5C8.40873 13.5 8.80996 13.4072 9.16178 13.2313C9.51359 13.0554 9.80308 12.8028 10 12.5" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/bell_dot.svg 🔗

@@ -1,5 +1,5 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-    <path d="M4.86142 8.6961C4.47786 9.66547 4 9.99997 4 9.99997H12C12 9.99997 10.6667 9.06664 10.6667 5.79997C10.6667 5.05737 10.3857 4.34518 9.88562 3.82007C9.52389 3.44026 9.06893 3.18083 8.57722 3.06635" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-    <path d="M6 12.5C6.19692 12.8028 6.48641 13.0554 6.83822 13.2313C7.19004 13.4072 7.59127 13.5 8 13.5C8.40873 13.5 8.80996 13.4072 9.16178 13.2313C9.51359 13.0554 9.80308 12.8028 10 12.5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+    <path d="M4.86142 8.6961C4.47786 9.66547 4 9.99997 4 9.99997H12C12 9.99997 10.6667 9.06664 10.6667 5.79997C10.6667 5.05737 10.3857 4.34518 9.88562 3.82007C9.52389 3.44026 9.06893 3.18083 8.57722 3.06635" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+    <path d="M6 12.5C6.19692 12.8028 6.48641 13.0554 6.83822 13.2313C7.19004 13.4072 7.59127 13.5 8 13.5C8.40873 13.5 8.80996 13.4072 9.16178 13.2313C9.51359 13.0554 9.80308 12.8028 10 12.5" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
     <circle cx="4.5" cy="4.5" r="2.5" fill="black"/>
 </svg>

assets/icons/bell_off.svg 🔗

@@ -1,5 +1,5 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-    <path d="M10.6667 5.8C10.6667 5.05739 10.3857 4.3452 9.88562 3.8201C9.38552 3.295 8.70724 3 8 3C7.29276 3 6.61448 3.295 6.11438 3.8201C5.61428 4.3452 5.33333 5.05739 5.33333 5.8C5.33333 9.06667 4 10 4 10H7.375" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-    <path d="M6 12.5C6.19692 12.8028 6.48641 13.0554 6.83822 13.2313C7.19004 13.4072 7.59127 13.5 8 13.5C8.40873 13.5 8.80996 13.4072 9.16178 13.2313C9.51359 13.0554 9.80308 12.8028 10 12.5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-    <path d="M4 3L12.5 11.5" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
+    <path d="M10.6667 5.8C10.6667 5.05739 10.3857 4.3452 9.88562 3.8201C9.38552 3.295 8.70724 3 8 3C7.29276 3 6.61448 3.295 6.11438 3.8201C5.61428 4.3452 5.33333 5.05739 5.33333 5.8C5.33333 9.06667 4 10 4 10H7.375" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+    <path d="M6 12.5C6.19692 12.8028 6.48641 13.0554 6.83822 13.2313C7.19004 13.4072 7.59127 13.5 8 13.5C8.40873 13.5 8.80996 13.4072 9.16178 13.2313C9.51359 13.0554 9.80308 12.8028 10 12.5" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+    <path d="M4 3L12.5 11.5" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
 </svg>

assets/icons/bell_ring.svg 🔗

@@ -1,6 +1,6 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-    <path d="M5.33333 5.8C5.33333 5.05739 5.61429 4.3452 6.11438 3.8201C6.61448 3.295 7.29276 3 8 3C8.70724 3 9.38552 3.295 9.88562 3.8201C10.3857 4.3452 10.6667 5.05739 10.6667 5.8C10.6667 9.06667 12 10 12 10H4C4 10 5.33333 9.06667 5.33333 5.8Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-    <path d="M6 12.5C6.19692 12.8028 6.48641 13.0554 6.83822 13.2313C7.19004 13.4072 7.59127 13.5 8 13.5C8.40873 13.5 8.80996 13.4072 9.16178 13.2313C9.51359 13.0554 9.80308 12.8028 10 12.5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-    <path d="M12 2.02081C12.617 2.89491 13.0754 3.88797 13.2528 5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-    <path d="M4 2.02081C3.38299 2.89491 2.92461 3.88797 2.74719 5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+    <path d="M5.33333 5.8C5.33333 5.05739 5.61429 4.3452 6.11438 3.8201C6.61448 3.295 7.29276 3 8 3C8.70724 3 9.38552 3.295 9.88562 3.8201C10.3857 4.3452 10.6667 5.05739 10.6667 5.8C10.6667 9.06667 12 10 12 10H4C4 10 5.33333 9.06667 5.33333 5.8Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+    <path d="M6 12.5C6.19692 12.8028 6.48641 13.0554 6.83822 13.2313C7.19004 13.4072 7.59127 13.5 8 13.5C8.40873 13.5 8.80996 13.4072 9.16178 13.2313C9.51359 13.0554 9.80308 12.8028 10 12.5" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+    <path d="M12 2.02081C12.617 2.89491 13.0754 3.88797 13.2528 5" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+    <path d="M4 2.02081C3.38299 2.89491 2.92461 3.88797 2.74719 5" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/binary.svg 🔗

@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-binary-icon lucide-binary"><rect x="14" y="14" width="4" height="6" rx="2"/><rect x="6" y="4" width="4" height="6" rx="2"/><path d="M6 20h4"/><path d="M14 10h4"/><path d="M6 14h2v6"/><path d="M14 4h2v6"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M12 10.667a1.333 1.333 0 1 0-2.667 0V12A1.333 1.333 0 1 0 12 12v-1.333ZM6.667 4A1.333 1.333 0 0 0 4 4v1.333a1.333 1.333 0 1 0 2.667 0V4ZM4 13.333h2.667M9.333 6.667H12M4 9.333h1.333v4M9.333 2.667h1.334v4"/></svg>

assets/icons/blocks.svg 🔗

@@ -1 +1,3 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-blocks-icon lucide-blocks"><rect width="7" height="7" x="14" y="3" rx="1"/><path d="M10 21V8a1 1 0 0 0-1-1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-5a1 1 0 0 0-1-1H3"/></svg>
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">

assets/icons/bolt_outlined.svg 🔗

@@ -1,3 +1,3 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path fill-rule="evenodd" clip-rule="evenodd" d="M9.29787 2.8462C9.41607 2.90046 9.51178 2.98975 9.5701 3.10016C9.62841 3.21057 9.64605 3.3359 9.62028 3.45666L8.96749 6.51117H12.2195C12.334 6.51115 12.446 6.54191 12.5423 6.59976C12.6386 6.65762 12.7151 6.74013 12.7627 6.83748C12.8102 6.93482 12.8269 7.04291 12.8106 7.14885C12.7943 7.2548 12.7458 7.35413 12.6709 7.43504L7.49631 13.0184C7.40998 13.1115 7.29318 13.1752 7.16411 13.1997C7.03504 13.2241 6.90092 13.2081 6.78264 13.1539C6.66437 13.0997 6.56859 13.0104 6.5102 12.9C6.4518 12.7896 6.43408 12.6643 6.45979 12.5435L7.11259 9.48899H3.86054C3.74609 9.489 3.63405 9.45825 3.53776 9.40039C3.44147 9.34254 3.36498 9.26003 3.31742 9.16268C3.26986 9.06534 3.25322 8.95725 3.26949 8.85131C3.28576 8.74536 3.33423 8.64603 3.40916 8.56513L8.58377 2.98169C8.67012 2.88856 8.78699 2.82478 8.91616 2.80028C9.04533 2.77576 9.17953 2.79192 9.29787 2.8462Z" fill="black" fill-opacity="0.15" stroke="black" stroke-width="1.5"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M9.29787 2.8462C9.41607 2.90046 9.51178 2.98975 9.5701 3.10016C9.62841 3.21057 9.64605 3.3359 9.62028 3.45666L8.96749 6.51117H12.2195C12.334 6.51115 12.446 6.54191 12.5423 6.59976C12.6386 6.65762 12.7151 6.74013 12.7627 6.83748C12.8102 6.93482 12.8269 7.04291 12.8106 7.14885C12.7943 7.2548 12.7458 7.35413 12.6709 7.43504L7.49631 13.0184C7.40998 13.1115 7.29318 13.1752 7.16411 13.1997C7.03504 13.2241 6.90092 13.2081 6.78264 13.1539C6.66437 13.0997 6.56859 13.0104 6.5102 12.9C6.4518 12.7896 6.43408 12.6643 6.45979 12.5435L7.11259 9.48899H3.86054C3.74609 9.489 3.63405 9.45825 3.53776 9.40039C3.44147 9.34254 3.36498 9.26003 3.31742 9.16268C3.26986 9.06534 3.25322 8.95725 3.26949 8.85131C3.28576 8.74536 3.33423 8.64603 3.40916 8.56513L8.58377 2.98169C8.67012 2.88856 8.78699 2.82478 8.91616 2.80028C9.04533 2.77576 9.17953 2.79192 9.29787 2.8462Z" fill="black" fill-opacity="0.15" stroke="black" stroke-width="1.2"/>
 </svg>

assets/icons/book.svg 🔗

@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-book"><path d="M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H20v20H6.5a2.5 2.5 0 0 1 0-5H20"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M4 12.125v-8.25c0-.365.132-.714.366-.972.235-.258.552-.403.884-.403H12v11H5.25c-.332 0-.65-.145-.884-.403A1.448 1.448 0 0 1 4 12.125Zm0 0c0-.365.132-.714.366-.972.235-.258.552-.403.884-.403H12"/></svg>

assets/icons/book_copy.svg 🔗

@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-book-copy"><path d="M2 16V4a2 2 0 0 1 2-2h11"/><path d="M5 14H4a2 2 0 1 0 0 4h1"/><path d="M22 18H11a2 2 0 1 0 0 4h11V6H11a2 2 0 0 0-2 2v12"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M2.5 10.5V3.643c0-.303.113-.594.315-.808.202-.215.476-.335.762-.335H9.5"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M4.5 9.5h-.667c-.353 0-.692.105-.942.293-.25.187-.391.442-.391.707 0 .265.14.52.39.707.25.188.59.293.943.293H4.5M13.5 11.25H7.577c-.286 0-.56.118-.762.33a1.151 1.151 0 0 0-.315.795m0 0c0 .298.113.585.315.796.202.21.476.329.762.329H13.5v-9H7.577c-.286 0-.56.119-.762.33a1.151 1.151 0 0 0-.315.795v6.75Z"/></svg>

assets/icons/bug_off.svg 🔗

@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-bug-off-icon lucide-bug-off"><path d="M15 7.13V6a3 3 0 0 0-5.14-2.1L8 2"/><path d="M14.12 3.88 16 2"/><path d="M22 13h-4v-2a4 4 0 0 0-4-4h-1.3"/><path d="M20.97 5c0 2.1-1.6 3.8-3.5 4"/><path d="m2 2 20 20"/><path d="M7.7 7.7A4 4 0 0 0 6 11v3a6 6 0 0 0 11.13 3.13"/><path d="M12 20v-8"/><path d="M6 13H2"/><path d="M3 21c0-2.1 1.7-3.9 3.8-4"/></svg>

assets/icons/caret_down.svg 🔗

@@ -1,8 +0,0 @@
-<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
-  <path
-    fill-rule="evenodd"
-    clip-rule="evenodd"
-    d="M4.18179 6.18181C4.35753 6.00608 4.64245 6.00608 4.81819 6.18181L7.49999 8.86362L10.1818 6.18181C10.3575 6.00608 10.6424 6.00608 10.8182 6.18181C10.9939 6.35755 10.9939 6.64247 10.8182 6.81821L7.81819 9.81821C7.73379 9.9026 7.61934 9.95001 7.49999 9.95001C7.38064 9.95001 7.26618 9.9026 7.18179 9.81821L4.18179 6.81821C4.00605 6.64247 4.00605 6.35755 4.18179 6.18181Z"
-    fill="currentColor"
-  />
-</svg>

assets/icons/caret_up.svg 🔗

@@ -1,8 +0,0 @@
-<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
-  <path
-    fill-rule="evenodd"
-    clip-rule="evenodd"
-    d="M4.18179 8.81819C4.00605 8.64245 4.00605 8.35753 4.18179 8.18179L7.18179 5.18179C7.26618 5.0974 7.38064 5.04999 7.49999 5.04999C7.61933 5.04999 7.73379 5.0974 7.81819 5.18179L10.8182 8.18179C10.9939 8.35753 10.9939 8.64245 10.8182 8.81819C10.6424 8.99392 10.3575 8.99392 10.1818 8.81819L7.49999 6.13638L4.81819 8.81819C4.64245 8.99392 4.35753 8.99392 4.18179 8.81819Z"
-    fill="currentColor"
-  />
-</svg>

assets/icons/case_sensitive.svg 🔗

@@ -1,8 +1 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="12px" height="12px" viewBox="0 0 12 12" version="1.1">
-<g id="surface1">
-<path style=" stroke:none;fill-rule:nonzero;fill:rgb(47.058824%,49.019608%,52.941176%);fill-opacity:1;" d="M 2.976562 2.746094 L 4.226562 2.746094 L 6.105469 9.296875 L 5.285156 9.296875 L 4.804688 7.640625 L 2.386719 7.640625 L 1.914062 9.296875 L 1.097656 9.296875 Z M 4.621094 6.917969 L 3.640625 3.449219 L 3.5625 3.449219 L 2.582031 6.917969 Z M 4.621094 6.917969 "/>
-<path style=" stroke:none;fill-rule:evenodd;fill:rgb(47.058824%,49.019608%,52.941176%);fill-opacity:1;" d="M 2.878906 2.617188 L 4.324219 2.617188 L 6.277344 9.425781 L 5.191406 9.425781 L 4.707031 7.769531 L 2.484375 7.769531 L 2.011719 9.425781 L 0.925781 9.425781 Z M 3.601562 3.785156 L 2.75 6.789062 L 4.453125 6.789062 Z M 3.601562 3.785156 "/>

assets/icons/chat.svg 🔗

@@ -1,4 +1,4 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M4.17279 8.26346C4.87566 8.62402 5.68419 8.72168 6.4527 8.53885C7.2212 8.35601 7.89913 7.90471 8.36433 7.26626C8.82953 6.62781 9.0514 5.8442 8.98996 5.05664C8.92852 4.26908 8.58781 3.52936 8.02922 2.97078C7.47064 2.41219 6.73092 2.07148 5.94336 2.01004C5.1558 1.9486 4.37219 2.17047 3.73374 2.63567C3.09529 3.10087 2.64399 3.7788 2.46115 4.5473C2.27832 5.31581 2.37598 6.12435 2.73654 6.82721L2 9L4.17279 8.26346Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M7.07168 11C7.16761 11.4537 7.35843 11.8857 7.63567 12.2662C8.10087 12.9047 8.7788 13.356 9.5473 13.5388C10.3158 13.7217 11.1243 13.624 11.8272 13.2634L14 14L13.2635 11.8272C13.624 11.1243 13.7217 10.3158 13.5388 9.54728C13.356 8.77877 12.9047 8.10084 12.2663 7.63564C11.8858 7.3584 11.4537 7.16759 11 7.07166" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M4.17279 8.26346C4.87566 8.62402 5.68419 8.72168 6.4527 8.53885C7.2212 8.35601 7.89913 7.90471 8.36433 7.26626C8.82953 6.62781 9.0514 5.8442 8.98996 5.05664C8.92852 4.26908 8.58781 3.52936 8.02922 2.97078C7.47064 2.41219 6.73092 2.07148 5.94336 2.01004C5.1558 1.9486 4.37219 2.17047 3.73374 2.63567C3.09529 3.10087 2.64399 3.7788 2.46115 4.5473C2.27832 5.31581 2.37598 6.12435 2.73654 6.82721L2 9L4.17279 8.26346Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M7.07168 11C7.16761 11.4537 7.35843 11.8857 7.63567 12.2662C8.10087 12.9047 8.7788 13.356 9.5473 13.5388C10.3158 13.7217 11.1243 13.624 11.8272 13.2634L14 14L13.2635 11.8272C13.624 11.1243 13.7217 10.3158 13.5388 9.54728C13.356 8.77877 12.9047 8.10084 12.2663 7.63564C11.8858 7.3584 11.4537 7.16759 11 7.07166" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/check.svg 🔗

@@ -1,3 +1,3 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-    <path d="M4.625 8.98121L7.03402 10.7714L11.3437 4.75989" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M4.625 8.98121L7.03402 10.7714L11.3437 4.75989" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/check_circle.svg 🔗

@@ -1,4 +1,4 @@
-<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M5 8L6.5 9L9 5.5" stroke="#11181C" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
-<circle cx="7" cy="7" r="4.875" stroke="#11181C" stroke-width="1.25"/>
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M5.94873 9.02564L7.48722 10.0513L10.0514 6.46149" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M8 14C11.3137 14 14 11.3137 14 8C14 4.68629 11.3137 2 8 2C4.68629 2 2 4.68629 2 8C2 11.3137 4.68629 14 8 14Z" stroke="black" stroke-width="1.2"/>
 </svg>

assets/icons/check_double.svg 🔗

@@ -1 +1,4 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-check-check-icon lucide-check-check"><path d="M18 6 7 17l-5-5"/><path d="m22 10-7.5 7.5L13 16"/></svg>
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M11.5999 4.38336L4.99996 10.9833L2 7.98332" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M14 6.78339L9.50009 11.2833L8.6001 10.3833" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

assets/icons/chevron_down.svg 🔗

@@ -1,3 +1,3 @@
-<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M3.63281 5.66406L6.99344 8.89844L10.3672 5.66406" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M4.15186 6.47321L7.99258 10.1696L11.8483 6.47321" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/chevron_down_small.svg 🔗

@@ -1,3 +0,0 @@
-<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M3.49574 4.74787L5.99574 7.25214L8.49574 4.74787" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
-</svg>

assets/icons/chevron_left.svg 🔗

@@ -1,3 +1,3 @@
-<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M8.35938 3.63281L5.125 6.99344L8.35938 10.3672" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M9.55361 4.15179L5.85718 7.99251L9.55361 11.8482" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/chevron_right.svg 🔗

@@ -1,3 +1,3 @@
-<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M5.64062 3.64062L8.89062 7.00125L5.64062 10.375" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M6.44653 4.16071L10.1608 8.00143L6.44653 11.8571" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/chevron_up.svg 🔗

@@ -1,3 +1,3 @@
-<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M3.63281 8.36719L6.99344 5.13281L10.3672 8.36719" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M4.15186 9.56252L7.99258 5.86609L11.8483 9.56252" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/chevron_up_down.svg 🔗

@@ -1 +1,4 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevrons-up-down"><path d="m7 15 5 5 5-5"/><path d="m7 9 5-5 5 5"/></svg>
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M4.66675 10L8.00008 13.3333L11.3334 10" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M4.66675 6.00002L8.00008 2.66669L11.3334 6.00002" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

assets/icons/circle.svg 🔗

@@ -1 +1,3 @@
-<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="7.25" cy="7.25" r="3" fill="currentColor"></circle></svg>
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M8 11C9.65685 11 11 9.65685 11 8C11 6.34315 9.65685 5 8 5C6.34315 5 5 6.34315 5 8C5 9.65685 6.34315 11 8 11Z" fill="black"/>
+</svg>

assets/icons/circle_check.svg 🔗

@@ -1,3 +1,3 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path fill-rule="evenodd" clip-rule="evenodd" d="M8 15C11.866 15 15 11.866 15 8C15 4.13401 11.866 1 8 1C4.13401 1 1 4.13401 1 8C1 11.866 4.13401 15 8 15ZM12.1187 5.99372L10.8813 4.75628L6.6875 8.95006L5.11872 7.38128L3.88128 8.61872L6.6875 11.4249L12.1187 5.99372Z" fill="white"/>
+<path d="M8 2.5C11.0376 2.5 13.5 4.96243 13.5 8C13.5 11.0376 11.0376 13.5 8 13.5C4.96243 13.5 2.5 11.0376 2.5 8C2.5 4.96243 4.96243 2.5 8 2.5ZM10.6699 5.37598C10.1196 5.06178 9.40923 5.20902 9.0332 5.73535L7.17188 8.33887L6.6416 7.98633L6.62207 7.97461L6.55566 7.93457L6.54395 7.92676L6.53125 7.91992C6.00582 7.64262 5.35445 7.7754 4.97949 8.23633L4.9082 8.33301C4.55035 8.87107 4.66306 9.58687 5.15234 9.99023L5.16211 9.99805L5.17188 10.0049L5.2334 10.0508L5.25488 10.0664L6.79297 11.0918C7.35476 11.4663 8.11112 11.3257 8.50293 10.7783H8.50391L11.0684 7.18848V7.1875C11.4684 6.62649 11.3395 5.84687 10.7783 5.44531V5.44434L10.6699 5.37598Z" fill="black" stroke="black"/>
 </svg>

assets/icons/circle_help.svg 🔗

@@ -1 +1,5 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-help-icon lucide-circle-help"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><path d="M12 17h.01"/></svg>
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M8 14C11.3137 14 14 11.3137 14 8C14 4.68629 11.3137 2 8 2C4.68629 2 2 4.68629 2 8C2 11.3137 4.68629 14 8 14Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M6.54492 6.5C6.66248 6.16582 6.8945 5.88404 7.19991 5.70455C7.50532 5.52506 7.86439 5.45945 8.21354 5.51934C8.56268 5.57922 8.87937 5.76075 9.1075 6.03175C9.33564 6.30276 9.4605 6.64576 9.45997 7C9.45997 8.00002 7.95994 8.50003 7.95994 8.50003" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M8 10.5H8.005" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

assets/icons/circle_off.svg 🔗

@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-off-icon lucide-circle-off"><path d="m2 2 20 20"/><path d="M8.35 2.69A10 10 0 0 1 21.3 15.65"/><path d="M19.08 19.08A10 10 0 1 1 4.92 4.92"/></svg>

assets/icons/close.svg 🔗

@@ -1,3 +1,3 @@
-<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M9.82843 4.17157L4.17157 9.82842M9.82843 9.82842L4.17157 4.17157" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M4.70581 4.5L11.294 11.5M11.294 4.5L4.70581 11.5" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
 </svg>

assets/icons/cloud.svg 🔗

@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-cloud"><path d="M17.5 19H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z"/></svg>

assets/icons/cloud_download.svg 🔗

@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-cloud-download-icon lucide-cloud-download"><path d="M12 13v8l-4-4"/><path d="m12 21 4-4"/><path d="M4.393 15.269A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.436 8.284"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M8.001 9v4l-2-2M8.001 13l2-2"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M3.436 10.389a4.215 4.215 0 0 1-1.424-3.484 4.227 4.227 0 0 1 1.92-3.236 4.19 4.19 0 0 1 5.335.665 4.22 4.22 0 0 1 .96 1.677H11.3c.584 0 1.151.19 1.618.54a2.71 2.71 0 0 1 .913 3.116A2.709 2.709 0 0 1 12.762 11"/></svg>

assets/icons/code.svg 🔗

@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-code-xml"><path d="m18 16 4-4-4-4"/><path d="m6 8-4 4 4 4"/><path d="m14.5 4-5 16"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="m11.75 10.5 2.5-2.5-2.5-2.5M4.25 5.5 1.75 8l2.5 2.5M9.563 3 6.437 13"/></svg>

assets/icons/cog.svg 🔗

@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-cog"><path d="M12 20a8 8 0 1 0 0-16 8 8 0 0 0 0 16Z"/><path d="M12 14a2 2 0 1 0 0-4 2 2 0 0 0 0 4Z"/><path d="M12 2v2"/><path d="M12 22v-2"/><path d="m17 20.66-1-1.73"/><path d="M11 10.27 7 3.34"/><path d="m20.66 17-1.73-1"/><path d="m3.34 7 1.73 1"/><path d="M14 12h8"/><path d="M2 12h2"/><path d="m20.66 7-1.73 1"/><path d="m3.34 17 1.73-1"/><path d="m17 3.34-1 1.73"/><path d="m11 13.73-4 6.93"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M8 12.8a4.8 4.8 0 1 0 0-9.6 4.8 4.8 0 0 0 0 9.6Z"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M8 9.2a1.2 1.2 0 1 0 0-2.4 1.2 1.2 0 0 0 0 2.4ZM8 2v1.2M8 14v-1.2M11 13.196l-.6-1.038M7.4 6.962 5 2.804M13.196 11l-1.038-.6M2.804 5l1.038.6M9.2 8H14M2 8h1.2M13.196 5l-1.038.6M2.804 11l1.038-.6M11 2.804l-.6 1.038M7.4 9.038 5 13.196"/></svg>

assets/icons/command.svg 🔗

@@ -1,3 +1,3 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">

assets/icons/context.svg 🔗

@@ -1,6 +0,0 @@
-<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M12.6667 2H3.33333C2.59695 2 2 2.59695 2 3.33333V12.6667C2 13.403 2.59695 14 3.33333 14H12.6667C13.403 14 14 13.403 14 12.6667V3.33333C14 2.59695 13.403 2 12.6667 2Z" stroke="#888888" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M9 5H5" stroke="#888888" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M10.5 8H5" stroke="#888888" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M9 10.9502H5" stroke="#888888" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
-</svg>

assets/icons/control.svg 🔗

@@ -1,3 +1,3 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M3.5 6.12488L7.64656 1.97853C7.84183 1.78328 8.1584 1.78329 8.35366 1.97854L12.5 6.12488" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
+<path d="M3.5 6.12487L7.64656 1.97852C7.84183 1.78327 8.1584 1.78328 8.35366 1.97853L12.5 6.12487" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
 </svg>

assets/icons/copilot.svg 🔗

@@ -1,9 +1,9 @@
-<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path fill-rule="evenodd" clip-rule="evenodd" d="M5.64063 7.67017C5.97718 7.67017 6.25 7.94437 6.25 8.28263V9.60963C6.25 9.94786 5.97718 10.2221 5.64063 10.2221C5.30408 10.2221 5.03125 9.94786 5.03125 9.60963V8.28263C5.03125 7.94437 5.30408 7.67017 5.64063 7.67017Z" fill="black"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M8.37537 7.67017C8.71192 7.67017 8.98474 7.94437 8.98474 8.28263V9.60963C8.98474 9.94786 8.71192 10.2221 8.37537 10.2221C8.03882 10.2221 7.76599 9.94786 7.76599 9.60963V8.28263C7.76599 7.94437 8.03882 7.67017 8.37537 7.67017Z" fill="black"/>
-<path d="M7 3.65625C7 5.84375 5.10754 6.3718 3.76562 6.3718C2.42371 6.3718 2.1405 5.3854 2.1405 4.16861C2.1405 2.95182 3.22834 1.96542 4.57025 1.96542C5.91216 1.96542 7 2.43946 7 3.65625Z" fill="black" fill-opacity="0.5" stroke="black" stroke-width="1.25"/>
-<path d="M7 3.65625C7 5.84375 8.89246 6.3718 10.2344 6.3718C11.5763 6.3718 11.8595 5.3854 11.8595 4.16861C11.8595 2.95182 10.7717 1.96542 9.42975 1.96542C8.08784 1.96542 7 2.43946 7 3.65625Z" fill="black" fill-opacity="0.5" stroke="black" stroke-width="1.25"/>
-<path d="M11.0156 6.01562C11.0156 6.01562 11.6735 6.43636 12 7.07348C12.3265 7.7106 12.3281 9.18621 12 9.7181C11.6719 10.25 11.2813 10.625 10.2931 11.16C9.30501 11.695 8 12.0156 8 12.0156H6C6 12.0156 4.70312 11.7344 3.70687 11.16C2.71061 10.5856 2.23437 10.2188 2 9.7181C1.76562 9.21746 1.6875 7.75 2 7.07348C2.31249 6.39695 3 6.01562 3 6.01562" stroke="black" stroke-width="1.25" stroke-linejoin="round"/>
-<path d="M10.4454 11.0264V6.41934L12.1671 6.99323V9.5598L10.4454 11.0264Z" fill="black" fill-opacity="0.75"/>
-<path d="M3.51556 11.0264V6.41934L1.79388 6.99323V9.5598L3.51556 11.0264Z" fill="black" fill-opacity="0.75"/>
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M6.44643 8.76593C6.83106 8.76593 7.14286 9.0793 7.14286 9.46588V10.9825C7.14286 11.369 6.83106 11.6824 6.44643 11.6824C6.06181 11.6824 5.75 11.369 5.75 10.9825V9.46588C5.75 9.0793 6.06181 8.76593 6.44643 8.76593Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M9.57168 8.76593C9.95631 8.76593 10.2681 9.0793 10.2681 9.46588V10.9825C10.2681 11.369 9.95631 11.6824 9.57168 11.6824C9.18705 11.6824 8.87524 11.369 8.87524 10.9825V9.46588C8.87524 9.0793 9.18705 8.76593 9.57168 8.76593Z" fill="black"/>
+<path d="M7.99976 4.17853C7.99976 6.67853 5.83695 7.28202 4.30332 7.28202C2.76971 7.28202 2.44604 6.1547 2.44604 4.76409C2.44604 3.37347 3.68929 2.24615 5.2229 2.24615C6.75651 2.24615 7.99976 2.78791 7.99976 4.17853Z" fill="black" fill-opacity="0.5" stroke="black" stroke-width="1.2"/>
+<path d="M8 4.17853C8 6.67853 10.1628 7.28202 11.6965 7.28202C13.2301 7.28202 13.5537 6.1547 13.5537 4.76409C13.5537 3.37347 12.3105 2.24615 10.7769 2.24615C9.24325 2.24615 8 2.78791 8 4.17853Z" fill="black" fill-opacity="0.5" stroke="black" stroke-width="1.2"/>
+<path d="M12.5894 6.875C12.5894 6.875 13.3413 7.35585 13.7144 8.08398C14.0876 8.81212 14.0894 10.4985 13.7144 11.1064C13.3395 11.7143 12.8931 12.1429 11.7637 12.7543C10.6344 13.3657 9.143 13.7321 9.143 13.7321H6.85728C6.85728 13.7321 5.37513 13.4107 4.23656 12.7543C3.09798 12.0978 2.55371 11.6786 2.28585 11.1064C2.01799 10.5342 1.92871 8.85715 2.28585 8.08398C2.64299 7.31081 3.42871 6.875 3.42871 6.875" stroke="black" stroke-width="1.2" stroke-linejoin="round"/>
+<path d="M11.9375 12.6016V7.33636L13.9052 7.99224V10.9255L11.9375 12.6016Z" fill="black" fill-opacity="0.75"/>
+<path d="M4.01793 12.6016V7.33636L2.05029 7.99224V10.9255L4.01793 12.6016Z" fill="black" fill-opacity="0.75"/>
 </svg>

assets/icons/copy.svg 🔗

@@ -1,4 +1 @@
-<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M18.4286 9H10.5714C9.70355 9 9 9.70355 9 10.5714V18.4286C9 19.2964 9.70355 20 10.5714 20H18.4286C19.2964 20 20 19.2964 20 18.4286V10.5714C20 9.70355 19.2964 9 18.4286 9Z" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M5.57143 15C4.70714 15 4 14.2929 4 13.4286V5.57143C4 4.70714 4.70714 4 5.57143 4H13.4286C14.2929 4 15 4.70714 15 5.57143" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
-</svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M12.286 6H7.048C6.469 6 6 6.469 6 7.048v5.238c0 .578.469 1.047 1.048 1.047h5.238c.578 0 1.047-.469 1.047-1.047V7.048c0-.579-.469-1.048-1.047-1.048Z"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M3.714 10a1.05 1.05 0 0 1-1.047-1.048V3.714a1.05 1.05 0 0 1 1.047-1.047h5.238A1.05 1.05 0 0 1 10 3.714"/></svg>

assets/icons/countdown_timer.svg 🔗

@@ -1 +1 @@
-<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M13.15 7.49998C13.15 4.66458 10.9402 1.84998 7.50002 1.84998C4.7217 1.84998 3.34851 3.90636 2.76336 4.99997H4.5C4.77614 4.99997 5 5.22383 5 5.49997C5 5.77611 4.77614 5.99997 4.5 5.99997H1.5C1.22386 5.99997 1 5.77611 1 5.49997V2.49997C1 2.22383 1.22386 1.99997 1.5 1.99997C1.77614 1.99997 2 2.22383 2 2.49997V4.31318C2.70453 3.07126 4.33406 0.849976 7.50002 0.849976C11.5628 0.849976 14.15 4.18537 14.15 7.49998C14.15 10.8146 11.5628 14.15 7.50002 14.15C5.55618 14.15 3.93778 13.3808 2.78548 12.2084C2.16852 11.5806 1.68668 10.839 1.35816 10.0407C1.25306 9.78536 1.37488 9.49315 1.63024 9.38806C1.8856 9.28296 2.17781 9.40478 2.2829 9.66014C2.56374 10.3425 2.97495 10.9745 3.4987 11.5074C4.47052 12.4963 5.83496 13.15 7.50002 13.15C10.9402 13.15 13.15 10.3354 13.15 7.49998ZM7 10V5.00001H8V10H7Z" fill="currentColor" fill-rule="evenodd" clip-rule="evenodd"></path></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M6.8 2h2.4M8 9.2l1.8-1.8M8 14a4.8 4.8 0 1 0 0-9.6A4.8 4.8 0 0 0 8 14Z"/></svg>

assets/icons/crosshair.svg 🔗

@@ -1,7 +1,7 @@
-<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M7 12C9.76142 12 12 9.76142 12 7C12 4.23858 9.76142 2 7 2C4.23858 2 2 4.23858 2 7C2 9.76142 4.23858 12 7 12Z" stroke="black" stroke-width="1.33" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M11.5 7H9" stroke="black" stroke-width="1.33" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M4.5 7H2" stroke="black" stroke-width="1.33" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M7 4.5V2" stroke="black" stroke-width="1.33" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M7 11.5V9" stroke="black" stroke-width="1.33" stroke-linecap="round" stroke-linejoin="round"/>
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M8 14C11.3137 14 14 11.3137 14 8C14 4.6863 11.3137 2 8 2C4.6863 2 2 4.6863 2 8C2 11.3137 4.6863 14 8 14Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M14 8L11 8" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M5 8H2" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M8 5V2" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M8 14V11" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/cursor_i_beam.svg 🔗

@@ -1,4 +1,4 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M11 13H10.4C9.76346 13 9.15302 12.7893 8.70296 12.4142C8.25284 12.0391 8 11.5304 8 11V5C8 4.46957 8.25284 3.96086 8.70296 3.58579C9.15302 3.21071 9.76346 3 10.4 3H11" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M5 13H5.6C6.23654 13 6.84698 12.7893 7.29704 12.4142C7.74716 12.0391 8 11.5304 8 11V5C8 4.46957 7.74716 3.96086 7.29704 3.58579C6.84698 3.21071 6.23654 3 5.6 3H5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M11 13H10.4C9.76346 13 9.15302 12.7893 8.70296 12.4142C8.25284 12.0391 8 11.5304 8 11V5C8 4.46957 8.25284 3.96086 8.70296 3.58579C9.15302 3.21071 9.76346 3 10.4 3H11" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M5 13H5.6C6.23654 13 6.84698 12.7893 7.29704 12.4142C7.74716 12.0391 8 11.5304 8 11V5C8 4.46957 7.74716 3.96086 7.29704 3.58579C6.84698 3.21071 6.23654 3 5.6 3H5" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/dash.svg 🔗

@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-minus"><path d="M5 12h14"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M3.333 8h9.334"/></svg>

assets/icons/database_zap.svg 🔗

@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-database-zap"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M3 5V19A9 3 0 0 0 15 21.84"/><path d="M21 5V8"/><path d="M21 12L18 17H22L19 22"/><path d="M3 12A9 3 0 0 0 14.59 14.87"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M8.017 5.625c2.974 0 5.385-.804 5.385-1.795 0-.991-2.41-1.795-5.385-1.795-2.973 0-5.384.804-5.384 1.795 0 .991 2.41 1.795 5.384 1.795Z"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M2.633 3.83v8.376c-.003.288.201.571.596.827.394.256.967.477 1.671.643a13.12 13.12 0 0 0 2.373.314c.854.04 1.725.01 2.54-.085M13.402 3.83v1.795M13.402 8.018l-1.795 2.991H14l-1.795 2.992"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M2.633 8.018c0 .28.198.556.575.805.378.25.925.467 1.599.634.673.167 1.454.279 2.28.327.827.048 1.676.032 2.48-.049"/></svg>

assets/icons/debug.svg 🔗

@@ -1,12 +1,12 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M5.49219 2.29071L6.41455 3.1933" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M9.61816 3.1933L10.508 2.29071" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M5.7042 5.89221V5.15749C5.69033 4.85975 5.73943 4.56239 5.84856 4.28336C5.95768 4.00434 6.12456 3.74943 6.33913 3.53402C6.55369 3.31862 6.81149 3.14718 7.09697 3.03005C7.38245 2.91292 7.68969 2.85254 8.00014 2.85254C8.3106 2.85254 8.61784 2.91292 8.90332 3.03005C9.18879 3.14718 9.44659 3.31862 9.66116 3.53402C9.87572 3.74943 10.0426 4.00434 10.1517 4.28336C10.2609 4.56239 10.31 4.85975 10.2961 5.15749V5.89221" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M8.00006 13.0426C6.13263 13.0426 4.60474 11.6005 4.60474 9.83792V8.23558C4.60474 7.66895 4.84322 7.12554 5.26772 6.72487C5.69221 6.32421 6.26796 6.09912 6.86829 6.09912H9.13184C9.73217 6.09912 10.3079 6.32421 10.7324 6.72487C11.1569 7.12554 11.3954 7.66895 11.3954 8.23558V9.83792C11.3954 11.6005 9.86749 13.0426 8.00006 13.0426Z" fill="black" fill-opacity="0.15" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M4.60452 6.25196C3.51235 6.13878 2.60693 5.17677 2.60693 3.9884" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M4.60462 8.81659H2.34106" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M2.4541 13.3186C2.4541 12.1302 3.41611 11.1116 4.60448 11.0551" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M13.0761 3.9884C13.0761 5.17677 12.1706 6.13878 11.0955 6.25196" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M13.6591 8.81659H11.3955" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M11.3955 11.0551C12.5839 11.1116 13.5459 12.1302 13.5459 13.3186" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M5.44727 2.19177L6.38617 3.11055" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M9.64722 3.11055L10.553 2.19177" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M5.66298 6.07369L5.66298 5.10997C5.64886 4.80689 5.69884 4.50419 5.80993 4.22016C5.92101 3.93613 6.09088 3.67665 6.3093 3.45738C6.52771 3.23811 6.79013 3.0636 7.08074 2.94437C7.37134 2.82514 7.68409 2.76367 8.00011 2.76367C8.31614 2.76367 8.62889 2.82514 8.91949 2.94437C9.21008 3.0636 9.4725 3.23811 9.69092 3.45738C9.90933 3.67665 10.0792 3.93613 10.1903 4.22016C10.3014 4.50419 10.3514 4.80689 10.3373 5.10997V6.07369" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M8.00017 13.1366C6.09924 13.1366 4.54395 11.6686 4.54395 9.87441V8.24333C4.54395 7.66653 4.7867 7.11337 5.21882 6.70552C5.65092 6.29767 6.237 6.06854 6.8481 6.06854H9.15225C9.76335 6.06854 10.3494 6.29767 10.7815 6.70552C11.2136 7.11337 11.4564 7.66653 11.4564 8.24333V9.87441C11.4564 11.6686 9.9011 13.1366 8.00017 13.1366Z" fill="black" fill-opacity="0.15" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M4.54343 6.22415C3.43167 6.10894 2.51001 5.12967 2.51001 3.91998" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M4.54367 8.83472H2.2395" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M2.35449 13.4175C2.35449 12.2078 3.33376 11.1709 4.54345 11.1134" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M13.1673 3.91998C13.1673 5.12967 12.2455 6.10894 11.1511 6.22415" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M13.7605 8.83472H11.4563" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M11.4563 11.1134C12.666 11.1709 13.6453 12.2078 13.6453 13.4175" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/debug_breakpoint.svg 🔗

@@ -1 +1,3 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle"><circle cx="12" cy="12" r="10"/></svg>
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M8 14C11.3137 14 14 11.3137 14 8C14 4.68629 11.3137 2 8 2C4.68629 2 2 4.68629 2 8C2 11.3137 4.68629 14 8 14Z" fill="black" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

assets/icons/debug_continue.svg 🔗

@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-step-forward"><line x1="6" x2="6" y1="4" y2="20"/><polygon points="10,4 20,12 10,20"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M4.167 3v10M7.167 3l6 5-6 5V3Z"/></svg>

assets/icons/debug_detach.svg 🔗

@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-unplug"><path d="m19 5 3-3"/><path d="m2 22 3-3"/><path d="M6.3 20.3a2.4 2.4 0 0 0 3.4 0L12 18l-6-6-2.3 2.3a2.4 2.4 0 0 0 0 3.4Z"/><path d="M7.5 13.5 10 11"/><path d="M10.5 16.5 13 14"/><path d="m12 6 6 6 2.3-2.3a2.4 2.4 0 0 0 0-3.4l-2.6-2.6a2.4 2.4 0 0 0-3.4 0Z"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><g stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" clip-path="url(#a)"><path d="m13 3 2-2M1 15l2-2M4.202 13.53a1.598 1.598 0 0 0 2.266 0L8 11.997 4.003 8 2.47 9.532a1.6 1.6 0 0 0 0 2.265l1.732 1.733ZM5 9l1.5-1.5M7 11l1.5-1.5M8 4.003 11.997 8l1.533-1.532a1.599 1.599 0 0 0 0-2.266L11.798 2.47a1.598 1.598 0 0 0-2.266 0L8 4.003Z"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

assets/icons/debug_disabled_breakpoint.svg 🔗

@@ -1 +1,3 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" stroke="currentColor" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle"><circle cx="12" cy="12" r="10"/></svg>
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M8 14C11.3137 14 14 11.3137 14 8C14 4.68629 11.3137 2 8 2C4.68629 2 2 4.68629 2 8C2 11.3137 4.68629 14 8 14Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

assets/icons/debug_disabled_log_breakpoint.svg 🔗

@@ -1 +1,5 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-message-circle"><path d="M7.9 20A9 9 0 1 0 4 16.1L2 22Z"/></svg>
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M11.8889 2H4.11111C3.49746 2 3 2.59695 3 3.33333V12.6667C3 13.403 3.49746 14 4.11111 14H11.8889C12.5025 14 13 13.403 13 12.6667V3.33333C13 2.59695 12.5025 2 11.8889 2Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M9 6H6" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M10 10H6" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

assets/icons/debug_ignore_breakpoints.svg 🔗

@@ -1 +1,3 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-off"><path d="m2 2 20 20"/><path d="M8.35 2.69A10 10 0 0 1 21.3 15.65"/><path d="M19.08 19.08A10 10 0 1 1 4.92 4.92"/></svg>
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M2 2L14 14M5.81044 2.41392C6.89676 1.98976 8.08314 1.89138 9.22449 2.13079C10.3658 2.37021 11.4127 2.93705 12.237 3.76199C13.0613 4.58693 13.6273 5.6342 13.8658 6.77573C14.1044 7.91727 14.0051 9.10357 13.5801 10.1896M12.2484 12.2484C11.1176 13.3558 9.59562 13.9724 8.01292 13.9642C6.43021 13.956 4.91467 13.3236 3.79552 12.2045C2.67636 11.0853 2.044 9.56979 2.03578 7.98708C2.02757 6.40438 2.64417 4.88236 3.75165 3.75165" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

assets/icons/debug_log_breakpoint.svg 🔗

@@ -1 +1,3 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-message-circle"><path d="M7.9 20A9 9 0 1 0 4 16.1L2 22Z"/></svg>
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M11.8887 1.25C13.0386 1.25 13.7498 2.31634 13.75 3.33301V12.667C13.7499 13.6836 13.0386 14.75 11.8887 14.75H4.11133C2.96134 14.75 2.25014 13.6836 2.25 12.667V3.33301C2.25015 2.31635 2.96136 1.25 4.11133 1.25H11.8887ZM6 9.25C5.58579 9.25 5.25 9.58579 5.25 10C5.25 10.4142 5.58579 10.75 6 10.75H10C10.4142 10.75 10.75 10.4142 10.75 10C10.75 9.58579 10.4142 9.25 10 9.25H6ZM6 5.25C5.58579 5.25 5.25 5.58579 5.25 6C5.25 6.41421 5.58579 6.75 6 6.75H9C9.41421 6.75 9.75 6.41421 9.75 6C9.75 5.58579 9.41421 5.25 9 5.25H6Z" fill="black"/>
+</svg>

assets/icons/debug_pause.svg 🔗

@@ -1 +1,4 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-pause"><rect x="14" y="4" width="4" height="16" rx="1"/><rect x="6" y="4" width="4" height="16" rx="1"/></svg>
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M10.5 4H9.5C9.22386 4 9 4.22386 9 4.5V11.5001C9 11.7762 9.22386 12.0001 9.5 12.0001H10.5C10.7762 12.0001 11 11.7762 11 11.5001V4.5C11 4.22386 10.7762 4 10.5 4Z" fill="black" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M6.50001 4H5.5C5.22386 4 5 4.22386 5 4.5V11.5001C5 11.7762 5.22386 12.0001 5.5 12.0001H6.50001C6.77616 12.0001 7.00002 11.7762 7.00002 11.5001V4.5C7.00002 4.22386 6.77616 4 6.50001 4Z" fill="black" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

assets/icons/debug_restart.svg 🔗

@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-rotate-ccw"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></svg>

assets/icons/debug_step_back.svg 🔗

@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-undo-dot"><path d="M21 17a9 9 0 0 0-15-6.7L3 13"/><path d="M3 7v6h6"/><circle cx="12" cy="17" r="1"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M14 11.333A6 6 0 0 0 4 6.867l-1 .9"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.333" d="M2 4.667v4h4"/><path fill="#000" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M8 12a.667.667 0 1 0 0-1.333A.667.667 0 0 0 8 12Z"/></svg>

assets/icons/debug_step_into.svg 🔗

@@ -1,5 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-up-from-dot">
-  <path d="m5 15 7 7 7-7"/>
-  <path d="M12 8v14"/>
-  <circle cx="12" cy="3" r="1"/>
-</svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M3.333 10 8 14.667 12.667 10M8 5.333v9.334"/><path fill="#000" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M8 2.667a.667.667 0 1 0 0-1.334.667.667 0 0 0 0 1.334Z"/></svg>

assets/icons/debug_step_out.svg 🔗

@@ -1,5 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-up-from-dot">
-  <path d="m3 10 9-8 9 8"/>
-  <path d="M12 17V2"/>
-  <circle cx="12" cy="21" r="1"/>
-</svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M3.333 6 8 1.333 12.667 6M8 10.667V1.333"/><path fill="#000" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M8 13.333a.667.667 0 1 1 0 1.334.667.667 0 0 1 0-1.334Z"/></svg>

assets/icons/debug_step_over.svg 🔗

@@ -1,5 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-redo-dot">
-  <circle cx="12" cy="17" r="1"/>
-  <path d="M21 7v6h-6"/>
-  <path d="M3 17a9 9 0 0 1 9-9 9 9 0 0 1 6 2.3l3 2.7"/>
-</svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M2 11.333a6 6 0 0 1 10-4.466l1 .9"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.333" d="M14 4.667v4h-4"/><path fill="#000" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M8 12a.667.667 0 1 1 0-1.333A.667.667 0 0 1 8 12Z"/></svg>

assets/icons/debug_stop.svg 🔗

@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-square"><rect width="18" height="18" x="3" y="3" rx="2"/></svg>

assets/icons/delete.svg 🔗

@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-delete"><path d="M20 5H9l-7 7 7 7h11a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2Z"/><line x1="18" x2="12" y1="9" y2="15"/><line x1="12" x2="18" y1="9" y2="15"/></svg>

assets/icons/diff.svg 🔗

@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-diff"><path d="M12 3v14"/><path d="M5 10h14"/><path d="M5 21h14"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M8 3v8M4 7h8M4 13h8"/></svg>

assets/icons/disconnected.svg 🔗

@@ -1,3 +1 @@
-<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M2.59892 2.76222C3.14641 2.17067 3.93013 1.80018 4.80011 1.80018C5.91195 1.80018 6.8813 2.40485 7.40066 3.30389C7.68565 3.09577 8.03064 3.00015 8.4 3.00015C9.39372 3.00015 10.1999 3.7895 10.1999 4.80009C10.1999 5.02884 10.1568 5.24633 10.08 5.44882C11.1749 5.67007 11.9999 6.63941 11.9999 7.80001C11.9999 8.48623 11.7112 9.10497 11.233 9.52683L11.8274 9.99557C12.0224 10.1493 12.058 10.4324 11.9043 10.6274C11.7505 10.8224 11.4674 10.858 11.2724 10.7043L0.172556 2.00436C-0.0231882 1.85099 -0.0574997 1.56825 0.0958708 1.37251C0.249241 1.17676 0.531795 1.14245 0.727671 1.29582L2.59868 2.76184L2.59892 2.76222ZM1.82213 4.43635L9.13873 10.1999H2.70017C1.20903 10.1999 0.000248596 8.99059 0.000248596 7.50001C0.000248596 6.32255 0.753414 5.32133 1.80395 4.95196C1.80151 4.90134 1.8002 4.85072 1.8002 4.80009C1.8002 4.67635 1.8077 4.55448 1.82213 4.43635Z" fill="white"/>
-</svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="m2 2 12 12M4.269 4.27a4.2 4.2 0 0 0 1.93 7.93h5.1c.267 0 .53-.039.785-.116M13.72 10.7a2.699 2.699 0 0 0-2.42-3.9h-1.074A4.204 4.204 0 0 0 6.8 3.842"/></svg>

assets/icons/document_text.svg 🔗

@@ -1,3 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-5">
-  <path fill-rule="evenodd" d="M4.5 2A1.5 1.5 0 0 0 3 3.5v13A1.5 1.5 0 0 0 4.5 18h11a1.5 1.5 0 0 0 1.5-1.5V7.621a1.5 1.5 0 0 0-.44-1.06l-4.12-4.122A1.5 1.5 0 0 0 11.378 2H4.5Zm2.25 8.5a.75.75 0 0 0 0 1.5h6.5a.75.75 0 0 0 0-1.5h-6.5Zm0 3a.75.75 0 0 0 0 1.5h6.5a.75.75 0 0 0 0-1.5h-6.5Z" clip-rule="evenodd" />
-</svg>

assets/icons/download.svg 🔗

@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-download"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" x2="12" y1="15" y2="3"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M13 9.667v2.222A1.111 1.111 0 0 1 11.889 13H4.11A1.111 1.111 0 0 1 3 11.889V9.667M5.222 6.889 8 9.667l2.778-2.778M8 9.667V3"/></svg>

assets/icons/ellipsis.svg 🔗

@@ -1,5 +1,5 @@
-<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
-<circle cx="7" cy="7" r="1" fill="black"/>
-<circle cx="11" cy="7" r="1" fill="black"/>
-<circle cx="3" cy="7" r="1" fill="black"/>
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<circle cx="3" cy="8" r="1" fill="black" stroke="black" stroke-width="0.5"/>
+<circle cx="8" cy="8" r="1" fill="black" stroke="black" stroke-width="0.5"/>
+<circle cx="13" cy="8" r="1" fill="black" stroke="black" stroke-width="0.5"/>
 </svg>

assets/icons/ellipsis_vertical.svg 🔗

@@ -1 +1,5 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-ellipsis-vertical"><circle cx="12" cy="12" r="1"/><circle cx="12" cy="5" r="1"/><circle cx="12" cy="19" r="1"/></svg>
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<circle cx="8" cy="3" r="1" fill="black" stroke="black" stroke-width="0.5"/>
+<circle cx="8" cy="8" r="1" fill="black" stroke="black" stroke-width="0.5"/>
+<circle cx="8" cy="13" r="1" fill="black" stroke="black" stroke-width="0.5"/>
+</svg>

assets/icons/envelope.svg 🔗

@@ -1,4 +1,4 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-    <path d="M13 4C13.5523 4 14 4.44772 14 5V11C14 11.5523 13.5523 12 13 12H3C2.44772 12 2 11.5523 2 11V5C2 4.44772 2.44772 4 3 4H13Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-    <path d="M13.5 5L7.9999 8.5L2.5 5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+    <path d="M13 4C13.5523 4 14 4.44772 14 5V11C14 11.5523 13.5523 12 13 12H3C2.44772 12 2 11.5523 2 11V5C2 4.44772 2.44772 4 3 4H13Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+    <path d="M13.5 5L7.9999 8.5L2.5 5" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/equal.svg 🔗

@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-equal-icon lucide-equal"><line x1="5" x2="19" y1="9" y2="9"/><line x1="5" x2="19" y1="15" y2="15"/></svg>

assets/icons/eraser.svg 🔗

@@ -1,4 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-eraser">
-    <path d="m7 21-4.3-4.3c-1-1-1-2.5 0-3.4l9.6-9.6c1-1 2.5-1 3.4 0l5.6 5.6c1 1 1 2.5 0 3.4L13 21"/>
-    <path d="M22 21H7"/><path d="m5 11 9 9"/>
-</svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="m5.015 12.983-2.567-2.382c-.597-.554-.597-1.385 0-1.884L8.179 3.4c.597-.554 1.493-.554 2.03 0L13.552 6.5c.597.554.597 1.385 0 1.884l-4.955 4.598M14 12.983H5M4.5 7.483l5 5"/></svg>

assets/icons/escape.svg 🔗

@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-up-left-from-circle"><path d="M2 8V2h6"/><path d="m2 2 10 10"/><path d="M12 2A10 10 0 1 1 2 12"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M3 6V3h3M3 3l5 5M8 3a5 5 0 1 1-5 5"/></svg>

assets/icons/exit.svg 🔗

@@ -1,5 +1,5 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M10.437 11.0461L13.4831 8L10.437 4.95392" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M13 8L8 8" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M6.6553 13.4659H4.21843C3.89528 13.4659 3.58537 13.3375 3.35687 13.109C3.12837 12.8805 3 12.5706 3 12.2475V3.71843C3 3.39528 3.12837 3.08537 3.35687 2.85687C3.58537 2.62837 3.89528 2.5 4.21843 2.5H6.6553" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M10.437 11.0461L13.4831 8L10.437 4.95392" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M13 8L8 8" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M6.6553 13.4659H4.21843C3.89528 13.4659 3.58537 13.3375 3.35687 13.109C3.12837 12.8805 3 12.5706 3 12.2475V3.71843C3 3.39528 3.12837 3.08537 3.35687 2.85687C3.58537 2.62837 3.89528 2.5 4.21843 2.5H6.6553" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/expand_down.svg 🔗

@@ -1,4 +1,4 @@
-<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M10.5 8.5L7.5 11.5M7.5 11.5L4.5 8.5M7.5 11.5L7.5 5.5" stroke="black" stroke-linecap="square"/>
-<path d="M5 3.5L10 3.5" stroke="black"/>
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M11.1998 9.60002L7.9998 12.8M7.9998 12.8L4.7998 9.60002M7.9998 12.8V6.40002" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M5.33325 3.73334H10.6666" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
 </svg>

assets/icons/expand_up.svg 🔗

@@ -1,4 +1,4 @@
-<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M4.5 6.5L7.5 3.5M7.5 3.5L10.5 6.5M7.5 3.5V9.5" stroke="black" stroke-linecap="square"/>
-<path d="M5 11.5H10" stroke="black"/>
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M4.80005 6.93334L8.00005 3.73334M8.00005 3.73334L11.2 6.93334M8.00005 3.73334V10.1333" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M5.3335 12.8H10.6668" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
 </svg>

assets/icons/expand_vertical.svg 🔗

@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-unfold-vertical"><path d="M12 22v-6"/><path d="M12 8V2"/><path d="M4 12H2"/><path d="M10 12H8"/><path d="M16 12h-2"/><path d="M22 12h-2"/><path d="m15 19-3 3-3-3"/><path d="m15 5-3-3-3 3"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><g stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" clip-path="url(#a)"><path d="M8 14.667v-4M8 5.333v-4M2.667 8H1.333M6.667 8H5.333M10.667 8H9.333M14.667 8h-1.334M10 12.667l-2 2-2-2M10 3.333l-2-2-2 2"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

assets/icons/external_link.svg 🔗

@@ -1,5 +0,0 @@
-<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M5.5 1H7.5H8.75C8.88807 1 9 1.11193 9 1.25V4.5" stroke="#838994" stroke-linecap="round"/>
-<path d="M3.64645 5.64645C3.45118 5.84171 3.45118 6.15829 3.64645 6.35355C3.84171 6.54882 4.15829 6.54882 4.35355 6.35355L3.64645 5.64645ZM8.64645 0.646447L3.64645 5.64645L4.35355 6.35355L9.35355 1.35355L8.64645 0.646447Z" fill="#838994"/>
-<path d="M7.5 6.5V9C7.5 9.27614 7.27614 9.5 7 9.5H1C0.723858 9.5 0.5 9.27614 0.5 9V3C0.5 2.72386 0.723858 2.5 1 2.5H3.5" stroke="#838994" stroke-linecap="round"/>
-</svg>

assets/icons/eye.svg 🔗

@@ -1 +1,4 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-eye"><path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0"/><circle cx="12" cy="12" r="3"/></svg>
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M2.0375 8.2088C1.9875 8.07409 1.9875 7.92592 2.0375 7.79122C2.5245 6.61039 3.35114 5.60076 4.41264 4.89031C5.47414 4.17986 6.72268 3.8006 7.99999 3.8006C9.2773 3.8006 10.5258 4.17986 11.5873 4.89031C12.6488 5.60076 13.4755 6.61039 13.9625 7.79122C14.0125 7.92592 14.0125 8.07409 13.9625 8.2088C13.4755 9.38962 12.6488 10.3993 11.5873 11.1097C10.5258 11.8202 9.2773 12.1994 7.99999 12.1994C6.72268 12.1994 5.47414 11.8202 4.41264 11.1097C3.35114 10.3993 2.5245 9.38962 2.0375 8.2088Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M8.0001 9.79988C8.99416 9.79988 9.80001 8.99404 9.80001 7.99998C9.80001 7.00592 8.99416 6.20007 8.0001 6.20007C7.00604 6.20007 6.2002 7.00592 6.2002 7.99998C6.2002 8.99404 7.00604 9.79988 8.0001 9.79988Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

assets/icons/file.svg 🔗

@@ -1,4 +1 @@
-<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M10.0001 1.33334H4.00008C3.64646 1.33334 3.30732 1.47382 3.05727 1.72387C2.80722 1.97392 2.66675 2.31305 2.66675 2.66668V13.3333C2.66675 13.687 2.80722 14.0261 3.05727 14.2762C3.30732 14.5262 3.64646 14.6667 4.00008 14.6667H12.0001C12.3537 14.6667 12.6928 14.5262 12.9429 14.2762C13.1929 14.0261 13.3334 13.687 13.3334 13.3333V4.66668L10.0001 1.33334Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M9.33325 1.33334V4.00001C9.33325 4.35363 9.47373 4.69277 9.72378 4.94282C9.97383 5.19287 10.313 5.33334 10.6666 5.33334H13.3333" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-</svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M9.875 2H4.25c-.332 0-.65.126-.884.351-.234.226-.366.53-.366.849v9.6c0 .318.132.623.366.849.235.225.552.351.884.351h7.5c.332 0 .65-.127.884-.351.234-.225.366-.53.366-.85V5L9.875 2Z"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M9 2v2.667A1.333 1.333 0 0 0 10.333 6H13"/></svg>

assets/icons/file_code.svg 🔗

@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-file-code"><path d="M10 12.5 8 15l2 2.5"/><path d="m14 12.5 2 2.5-2 2.5"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7z"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M6.8 8.3 5.6 9.8l1.2 1.5M9.2 8.3l1.2 1.5-1.2 1.5M9.2 2v2.4a1.2 1.2 0 0 0 1.2 1.2h2.4"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M9.8 2H4.4a1.2 1.2 0 0 0-1.2 1.2v9.6A1.2 1.2 0 0 0 4.4 14h7.2a1.2 1.2 0 0 0 1.2-1.2V5l-3-3Z"/></svg>

assets/icons/file_create.svg 🔗

@@ -1,5 +0,0 @@
-<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M10.0001 1.33334H4.00008C3.64646 1.33334 3.30732 1.47382 3.05727 1.72387C2.80722 1.97392 2.66675 2.31305 2.66675 2.66668V13.3333C2.66675 13.687 2.80722 14.0261 3.05727 14.2762C3.30732 14.5262 3.64646 14.6667 4.00008 14.6667H12.0001C12.3537 14.6667 12.6928 14.5262 12.9429 14.2762C13.1929 14.0261 13.3334 13.687 13.3334 13.3333V4.66668L10.0001 1.33334Z" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M6 8H10" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M8 10V6" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
-</svg>

assets/icons/file_diff.svg 🔗

@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-file-diff"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M9 10h6"/><path d="M12 13V7"/><path d="M9 17h6"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M9.8 2H4.4a1.2 1.2 0 0 0-1.2 1.2v9.6A1.2 1.2 0 0 0 4.4 14h7.2a1.2 1.2 0 0 0 1.2-1.2V5l-3-3ZM6.2 6.8h3.6M8 8.6V5M6.2 11h3.6"/></svg>

assets/icons/file_doc.svg 🔗

@@ -1,6 +1,6 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M13 11V11.8374C13 11.9431 12.9665 12.046 12.9044 12.1315L12.1498 13.1691C12.0557 13.2985 11.9054 13.375 11.7454 13.375H4.25461C4.09464 13.375 3.94433 13.2985 3.85024 13.1691L3.09563 12.1315C3.03348 12.046 3 11.9431 3 11.8374V3" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
+<path d="M13 11V11.8374C13 11.9431 12.9665 12.046 12.9044 12.1315L12.1498 13.1691C12.0557 13.2985 11.9054 13.375 11.7454 13.375H4.25461C4.09464 13.375 3.94433 13.2985 3.85024 13.1691L3.09563 12.1315C3.03348 12.046 3 11.9431 3 11.8374V3" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
 <path d="M3 13V11L8 12H13V13H3Z" fill="black"/>
-<path d="M6.63246 3.04418C7.44914 3.31641 8 4.08069 8 4.94155V11.7306C8 12.0924 7.62757 12.3345 7.29693 12.1875L3.79693 10.632C3.61637 10.5518 3.5 10.3727 3.5 10.1751V2.69374C3.5 2.35246 3.83435 2.11148 4.15811 2.2194L6.63246 3.04418Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M9.5 3C8.67157 3 8 3.67157 8 4.5V13C8 12.1954 11.2366 12.0382 12.5017 12.0075C12.7778 12.0008 13 11.7761 13 11.5V3.5C13 3.22386 12.7761 3 12.5 3H9.5Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M6.63246 3.04418C7.44914 3.31641 8 4.08069 8 4.94155V11.7306C8 12.0924 7.62757 12.3345 7.29693 12.1875L3.79693 10.632C3.61637 10.5518 3.5 10.3727 3.5 10.1751V2.69374C3.5 2.35246 3.83435 2.11148 4.15811 2.2194L6.63246 3.04418Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M9.5 3C8.67157 3 8 3.67157 8 4.5V13C8 12.1954 11.2366 12.0382 12.5017 12.0075C12.7778 12.0008 13 11.7761 13 11.5V3.5C13 3.22386 12.7761 3 12.5 3H9.5Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/file_generic.svg 🔗

@@ -1,5 +1,5 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M3 5H11" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
-<path d="M3 8H13" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
-<path d="M3 11H9" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
+<path d="M3 5H11" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
+<path d="M3 8H13" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
+<path d="M3 11H9" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
 </svg>

assets/icons/file_git.svg 🔗

@@ -1,6 +1,6 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M5 13C6.10457 13 7 12.1046 7 11C7 9.89543 6.10457 9 5 9C3.89543 9 3 9.89543 3 11C3 12.1046 3.89543 13 5 13Z" stroke="black" stroke-width="1.5"/>
-<path d="M11 7C12.1046 7 13 6.10457 13 5C13 3.89543 12.1046 3 11 3C9.89543 3 9 3.89543 9 5C9 6.10457 9.89543 7 11 7Z" fill="black" stroke="black" stroke-width="1.5"/>
-<path d="M4.625 3.625V8.375" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
-<path d="M11 7C11 9.20914 9.20914 11 7 11" stroke="black" stroke-width="1.5"/>
+<path d="M5 13C6.10457 13 7 12.1046 7 11C7 9.89543 6.10457 9 5 9C3.89543 9 3 9.89543 3 11C3 12.1046 3.89543 13 5 13Z" stroke="black" stroke-width="1.2"/>
+<path d="M11 7C12.1046 7 13 6.10457 13 5C13 3.89543 12.1046 3 11 3C9.89543 3 9 3.89543 9 5C9 6.10457 9.89543 7 11 7Z" fill="black" stroke="black" stroke-width="1.2"/>
+<path d="M4.625 3.625V8.375" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
+<path d="M11 7C11 9.20914 9.20914 11 7 11" stroke="black" stroke-width="1.2"/>
 </svg>

assets/icons/file_icons/ai.svg 🔗

@@ -1,5 +1,5 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-    <path d="M10.5 8.75V10.5C8.43097 10.5 7.56903 10.5 5.5 10.5V10L10.5 6V5.5H5.5V7.25" stroke="black" stroke-width="1.5"/>
+    <path d="M10.5 8.75V10.5C8.43097 10.5 7.56903 10.5 5.5 10.5V10L10.5 6V5.5H5.5V7.25" stroke="black" stroke-width="1.2"/>
     <path d="M1.5 8.5C1.77614 8.5 2 8.27614 2 8C2 7.72386 1.77614 7.5 1.5 7.5C1.22386 7.5 1 7.72386 1 8C1 8.27614 1.22386 8.5 1.5 8.5Z" fill="black"/>
     <path d="M2.49976 6.33002C2.7759 6.33002 2.99976 6.10616 2.99976 5.83002C2.99976 5.55387 2.7759 5.33002 2.49976 5.33002C2.22361 5.33002 1.99976 5.55387 1.99976 5.83002C1.99976 6.10616 2.22361 6.33002 2.49976 6.33002Z" fill="black"/>
     <path d="M2.49976 10.66C2.7759 10.66 2.99976 10.4361 2.99976 10.16C2.99976 9.88383 2.7759 9.65997 2.49976 9.65997C2.22361 9.65997 1.99976 9.88383 1.99976 10.16C1.99976 10.4361 2.22361 10.66 2.49976 10.66Z" fill="black"/>

assets/icons/file_icons/audio.svg 🔗

@@ -1,8 +1,8 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M3.5 6.66666V8.66666" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M5.5 5V11" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M7.5 3V13" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M9.5 5.33334V10" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M11.5 4V12" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M13.5 6.66666V8.66666" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M3.5 6.66666V8.66666" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M5.5 5V11" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M7.5 3V13" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M9.5 5.33334V10" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M11.5 4V12" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M13.5 6.66666V8.66666" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/file_icons/book.svg 🔗

@@ -1,6 +1,6 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M13 11V11.8374C13 11.9431 12.9665 12.046 12.9044 12.1315L12.1498 13.1691C12.0557 13.2985 11.9054 13.375 11.7454 13.375H4.25461C4.09464 13.375 3.94433 13.2985 3.85024 13.1691L3.09563 12.1315C3.03348 12.046 3 11.9431 3 11.8374V3" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
+<path d="M13 11V11.8374C13 11.9431 12.9665 12.046 12.9044 12.1315L12.1498 13.1691C12.0557 13.2985 11.9054 13.375 11.7454 13.375H4.25461C4.09464 13.375 3.94433 13.2985 3.85024 13.1691L3.09563 12.1315C3.03348 12.046 3 11.9431 3 11.8374V3" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
 <path d="M3 13V11L8 12H13V13H3Z" fill="black"/>
-<path d="M6.63246 3.04418C7.44914 3.31641 8 4.08069 8 4.94155V11.7306C8 12.0924 7.62757 12.3345 7.29693 12.1875L3.79693 10.632C3.61637 10.5518 3.5 10.3727 3.5 10.1751V2.69374C3.5 2.35246 3.83435 2.11148 4.15811 2.2194L6.63246 3.04418Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M9.5 3C8.67157 3 8 3.67157 8 4.5V13C8 12.1954 11.2366 12.0382 12.5017 12.0075C12.7778 12.0008 13 11.7761 13 11.5V3.5C13 3.22386 12.7761 3 12.5 3H9.5Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M6.63246 3.04418C7.44914 3.31641 8 4.08069 8 4.94155V11.7306C8 12.0924 7.62757 12.3345 7.29693 12.1875L3.79693 10.632C3.61637 10.5518 3.5 10.3727 3.5 10.1751V2.69374C3.5 2.35246 3.83435 2.11148 4.15811 2.2194L6.63246 3.04418Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M9.5 3C8.67157 3 8 3.67157 8 4.5V13C8 12.1954 11.2366 12.0382 12.5017 12.0075C12.7778 12.0008 13 11.7761 13 11.5V3.5C13 3.22386 12.7761 3 12.5 3H9.5Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/file_icons/bun.svg 🔗

@@ -1,5 +1,5 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-    <path d="M11.0426 5.32305L11.0426 5.32306L11.0457 5.32471C12.4534 6.05668 13.25 7.21804 13.25 8.42984C13.25 9.40862 12.7315 10.3471 11.7886 11.0652C10.845 11.7839 9.50819 12.25 8 12.25C6.49181 12.25 5.155 11.7839 4.21141 11.0652C3.2685 10.3471 2.75 9.40862 2.75 8.42984C2.75 7.21804 3.54655 6.05668 4.95426 5.32471L4.95427 5.32473L4.95849 5.3225C5.44976 5.06306 5.93128 4.79038 6.4063 4.50125C6.82126 4.25139 7.14467 4.05839 7.42857 3.92422C7.71398 3.78934 7.88783 3.75 8 3.75C8.28571 3.75 8.57685 3.89469 9.43489 4.41073L9.43488 4.41075L9.43944 4.41345C9.47377 4.43377 9.50881 4.45453 9.54456 4.47572C9.94472 4.71289 10.4345 5.00316 11.0426 5.32305Z" stroke="black" stroke-width="1.5"/>
+    <path d="M11.0426 5.32305L11.0426 5.32306L11.0457 5.32471C12.4534 6.05668 13.25 7.21804 13.25 8.42984C13.25 9.40862 12.7315 10.3471 11.7886 11.0652C10.845 11.7839 9.50819 12.25 8 12.25C6.49181 12.25 5.155 11.7839 4.21141 11.0652C3.2685 10.3471 2.75 9.40862 2.75 8.42984C2.75 7.21804 3.54655 6.05668 4.95426 5.32471L4.95427 5.32473L4.95849 5.3225C5.44976 5.06306 5.93128 4.79038 6.4063 4.50125C6.82126 4.25139 7.14467 4.05839 7.42857 3.92422C7.71398 3.78934 7.88783 3.75 8 3.75C8.28571 3.75 8.57685 3.89469 9.43489 4.41073L9.43488 4.41075L9.43944 4.41345C9.47377 4.43377 9.50881 4.45453 9.54456 4.47572C9.94472 4.71289 10.4345 5.00316 11.0426 5.32305Z" stroke="black" stroke-width="1.2"/>
     <path d="M13 7C15.5 12.5 7.92993 15 3.92993 11" stroke="black" stroke-width="1.25"/>
     <circle cx="6" cy="7.75" r="1" fill="black"/>
     <circle cx="10" cy="7.75" r="1" fill="black"/>

assets/icons/file_icons/chevron_down.svg 🔗

@@ -1,3 +1,3 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M4.63281 6.66406L7.99344 9.89844L11.3672 6.66406" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M4.63281 6.66406L7.99344 9.89844L11.3672 6.66406" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/file_icons/chevron_left.svg 🔗

@@ -1,3 +1,3 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M9.35938 4.63281L6.125 7.99344L9.35938 11.3672" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M9.35938 4.63281L6.125 7.99344L9.35938 11.3672" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/file_icons/chevron_right.svg 🔗

@@ -1,3 +1,3 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M6.64062 4.64062L9.89062 8.00125L6.64062 11.375" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M6.64062 4.64062L9.89062 8.00125L6.64062 11.375" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/file_icons/chevron_up.svg 🔗

@@ -1,3 +1,3 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M4.63281 9.36719L7.99344 6.13281L11.3672 9.36719" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M4.63281 9.36719L7.99344 6.13281L11.3672 9.36719" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/file_icons/code.svg 🔗

@@ -1,4 +1,4 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M5.78125 3C3.90625 3 3.90625 4.5 3.90625 5.5C3.90625 6.5 3.40625 7.50106 2.40625 8C3.40625 8.50106 3.90625 9.5 3.90625 10.5C3.90625 11.5 3.90625 13 5.78125 13" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M10.2422 3C12.1172 3 12.1172 4.5 12.1172 5.5C12.1172 6.5 12.6172 7.50106 13.6172 8C12.6172 8.50106 12.1172 9.5 12.1172 10.5C12.1172 11.5 12.1172 13 10.2422 13" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M5.78125 3C3.90625 3 3.90625 4.5 3.90625 5.5C3.90625 6.5 3.40625 7.50106 2.40625 8C3.40625 8.50106 3.90625 9.5 3.90625 10.5C3.90625 11.5 3.90625 13 5.78125 13" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M10.2422 3C12.1172 3 12.1172 4.5 12.1172 5.5C12.1172 6.5 12.6172 7.50106 13.6172 8C12.6172 8.50106 12.1172 9.5 12.1172 10.5C12.1172 11.5 12.1172 13 10.2422 13" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/file_icons/coffeescript.svg 🔗

@@ -1,5 +1,5 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
     <path d="M13.3061 6.5778C11.2118 7.65229 5.59818 7.64305 3.55456 6.49718C3.34826 6.38151 3.00857 6.53238 3.02549 6.76829C3.25878 10.0209 5.09256 13 8.49998 13C11.8648 13 13.6714 10.1058 13.9591 6.91373C13.9819 6.66029 13.5325 6.46164 13.3061 6.5778Z" fill="black"/>
     <path d="M10.0555 5.53646C10.4444 6.0547 11.9998 6.57297 12 4.49998C12.0002 2.42709 9.66679 2.94528 8.50013 4.49998C7.33348 6.05467 4.99991 6.57294 5 4.5C5.00009 2.42706 6.55548 2.94528 6.94443 3.46352" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
-    <circle cx="4" cy="10.5" r="1.75" stroke="black" stroke-width="1.5"/>
+    <circle cx="4" cy="10.5" r="1.75" stroke="black" stroke-width="1.2"/>
 </svg>

assets/icons/file_icons/conversations.svg 🔗

@@ -1,4 +1,4 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
 <path d="M6.46115 9.43419C8.30678 9.43419 9.92229 8.43411 9.92229 6.21171C9.92229 3.98933 8.30678 2.98926 6.46115 2.98926C4.61553 2.98926 3 3.98933 3 6.21171C3 7.028 3.21794 7.67935 3.58519 8.17685C3.7184 8.35732 3.69033 8.77795 3.58387 8.97539C3.32908 9.44793 3.81048 9.9657 4.33372 9.84571C4.72539 9.75597 5.13621 9.63447 5.49574 9.4715C5.62736 9.41181 5.7727 9.38777 5.91631 9.40402C6.09471 9.42416 6.27678 9.43419 6.46115 9.43419Z" fill="black" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M12.3385 7.24835C12.7049 7.74561 12.9224 8.39641 12.9224 9.2117C12.9224 10.028 12.7044 10.6793 12.3372 11.1768C12.204 11.3573 12.232 11.7779 12.3385 11.9754C12.5933 12.4479 12.1119 12.9657 11.5886 12.8457C11.197 12.756 10.7862 12.6345 10.4266 12.4715C10.295 12.4118 10.1497 12.3878 10.0061 12.404C9.82765 12.4242 9.64558 12.4342 9.46121 12.4342C8.61469 12.4342 7.81658 12.2238 7.20055 11.7816" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M12.3385 7.24835C12.7049 7.74561 12.9224 8.39641 12.9224 9.2117C12.9224 10.028 12.7044 10.6793 12.3372 11.1768C12.204 11.3573 12.232 11.7779 12.3385 11.9754C12.5933 12.4479 12.1119 12.9657 11.5886 12.8457C11.197 12.756 10.7862 12.6345 10.4266 12.4715C10.295 12.4118 10.1497 12.3878 10.0061 12.404C9.82765 12.4242 9.64558 12.4342 9.46121 12.4342C8.61469 12.4342 7.81658 12.2238 7.20055 11.7816" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/file_icons/dart.svg 🔗

@@ -1,5 +1,5 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-    <path d="M12 11.25H11.25V12V13.25H7.31066L2.35809 8.29743L3.60151 4.56717L7.81937 2.88003L13.25 8.31066V11.25H12Z" stroke="black" stroke-width="1.5"/>
+    <path d="M12 11.25H11.25V12V13.25H7.31066L2.35809 8.29743L3.60151 4.56717L7.81937 2.88003L13.25 8.31066V11.25H12Z" stroke="black" stroke-width="1.2"/>
     <path d="M10.928 11.4328L3.5 4.5H9.89645C9.96275 4.5 10.0263 4.52634 10.0732 4.57322L13.4268 7.92678C13.4737 7.97366 13.5 8.03725 13.5 8.10355V11.25C13.5 11.3881 13.3881 11.5 13.25 11.5H11.0985C11.0352 11.5 10.9743 11.476 10.928 11.4328Z" fill="black"/>
     <path d="M4 11L4.5 5C3.97221 4.7361 3.33305 5.00085 3.14645 5.56066L2.19544 8.41368C2.07566 8.77302 2.16918 9.16918 2.43702 9.43702L4 11Z" fill="black"/>
 </svg>

assets/icons/file_icons/database.svg 🔗

@@ -1,5 +1,5 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M7.99993 6.85713C11.1558 6.85713 13.7142 5.83379 13.7142 4.57142C13.7142 3.30905 11.1558 2.28571 7.99993 2.28571C4.84402 2.28571 2.28564 3.30905 2.28564 4.57142C2.28564 5.83379 4.84402 6.85713 7.99993 6.85713Z" fill="black" stroke="black" stroke-width="1.5"/>
-<path d="M13.7142 4.57141V11.4286C13.7142 12.691 11.1558 13.7143 7.99993 13.7143C4.84402 13.7143 2.28564 12.691 2.28564 11.4286V4.57141" stroke="black" stroke-width="1.5"/>
-<path d="M13.7142 8C13.7142 9.26237 11.1558 10.2857 7.99993 10.2857C4.84402 10.2857 2.28564 9.26237 2.28564 8" stroke="black" stroke-width="1.5"/>
+<path d="M7.99993 6.85713C11.1558 6.85713 13.7142 5.83379 13.7142 4.57142C13.7142 3.30905 11.1558 2.28571 7.99993 2.28571C4.84402 2.28571 2.28564 3.30905 2.28564 4.57142C2.28564 5.83379 4.84402 6.85713 7.99993 6.85713Z" fill="black" stroke="black" stroke-width="1.2"/>
+<path d="M13.7142 4.57141V11.4286C13.7142 12.691 11.1558 13.7143 7.99993 13.7143C4.84402 13.7143 2.28564 12.691 2.28564 11.4286V4.57141" stroke="black" stroke-width="1.2"/>
+<path d="M13.7142 8C13.7142 9.26237 11.1558 10.2857 7.99993 10.2857C4.84402 10.2857 2.28564 9.26237 2.28564 8" stroke="black" stroke-width="1.2"/>
 </svg>

assets/icons/file_icons/diff.svg 🔗

@@ -1,5 +1,5 @@
 <svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M8.5 3L8.5 10" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M5 6.5H12" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M5 13H12" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M8.5 3L8.5 10" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M5 6.5H12" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M5 13H12" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/file_icons/eslint.svg 🔗

@@ -1,4 +1,4 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-    <path d="M13.5413 8.31248C13.6529 8.11911 13.6529 7.88086 13.5413 7.68748L11.0413 3.35736C10.9296 3.16398 10.7233 3.04486 10.5 3.04486H5.5C5.27671 3.04486 5.07038 3.16398 4.95873 3.35736L2.45873 7.68748C2.34709 7.88086 2.34709 8.11911 2.45873 8.31248L4.95873 12.6426C5.07038 12.836 5.27671 12.9551 5.5 12.9551H10.5C10.7233 12.9551 10.9296 12.836 11.0413 12.6426L13.5413 8.31248Z" stroke="black" stroke-width="1.5" stroke-linejoin="round"/>
+    <path d="M13.5413 8.31248C13.6529 8.11911 13.6529 7.88086 13.5413 7.68748L11.0413 3.35736C10.9296 3.16398 10.7233 3.04486 10.5 3.04486H5.5C5.27671 3.04486 5.07038 3.16398 4.95873 3.35736L2.45873 7.68748C2.34709 7.88086 2.34709 8.11911 2.45873 8.31248L4.95873 12.6426C5.07038 12.836 5.27671 12.9551 5.5 12.9551H10.5C10.7233 12.9551 10.9296 12.836 11.0413 12.6426L13.5413 8.31248Z" stroke="black" stroke-width="1.2" stroke-linejoin="round"/>
     <path d="M7.74994 5.14432C7.90464 5.055 8.09524 5.055 8.24994 5.14432L10.348 6.35564C10.5027 6.44496 10.598 6.61002 10.598 6.78866V9.2113C10.598 9.38994 10.5027 9.555 10.348 9.64432L8.24994 10.8556C8.09524 10.945 7.90464 10.945 7.74994 10.8556L5.65186 9.64432C5.49716 9.555 5.40186 9.38994 5.40186 9.2113V6.78866C5.40186 6.61002 5.49716 6.44496 5.65186 6.35564L7.74994 5.14432Z" fill="black"/>
 </svg>

assets/icons/file_icons/file.svg 🔗

@@ -1,5 +1,5 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M3 5H11" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
-<path d="M3 8H13" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
-<path d="M3 11H9" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
+<path d="M3 5H11" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
+<path d="M3 8H13" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
+<path d="M3 11H9" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
 </svg>

assets/icons/file_icons/folder.svg 🔗

@@ -1,3 +1,3 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M8.26046 3.97337C8.3527 4.17617 8.4795 4.47151 8.57375 4.69341C8.65258 4.87898 8.83437 4.99999 9.03599 4.99999H12.5C12.7761 4.99999 13 5.22385 13 5.49999V12.125C13 12.4011 12.7761 12.625 12.5 12.625H3.5C3.22386 12.625 3 12.4011 3 12.125V3.86932C3 3.59318 3.22386 3.36932 3.5 3.36932H7.34219C7.74141 3.36932 8.09483 3.60924 8.26046 3.97337Z" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
+<path d="M8.26046 3.97337C8.3527 4.17617 8.4795 4.47151 8.57375 4.69341C8.65258 4.87898 8.83437 4.99999 9.03599 4.99999H12.5C12.7761 4.99999 13 5.22385 13 5.49999V12.125C13 12.4011 12.7761 12.625 12.5 12.625H3.5C3.22386 12.625 3 12.4011 3 12.125V3.86932C3 3.59318 3.22386 3.36932 3.5 3.36932H7.34219C7.74141 3.36932 8.09483 3.60924 8.26046 3.97337Z" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
 </svg>

assets/icons/file_icons/folder_open.svg 🔗

@@ -1,4 +1,4 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M4.42782 7.2487C4.43495 6.97194 4.65009 6.75 4.91441 6.75H13.5293C13.7935 6.75 14.007 6.97194 13.9998 7.2487C13.9628 8.6885 13.7533 12.75 12.5721 12.75H3.375C4.55631 12.75 4.3907 8.6885 4.42782 7.2487Z" fill="black" stroke="black" stroke-width="1.5" stroke-linejoin="round"/>
-<path d="M5.19598 12.625H3.66515C3.42618 12.625 3.22289 12.4453 3.18626 12.2017L1.94333 3.93602C1.89776 3.63295 2.12496 3.35938 2.42223 3.35938H5.78585C6.11241 3.35938 6.41702 3.52903 6.59618 3.81071L6.94517 4.35938H9.92811C10.4007 4.35938 10.8044 4.71102 10.8836 5.1917L11.1251 6.65624" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M4.42782 7.2487C4.43495 6.97194 4.65009 6.75 4.91441 6.75H13.5293C13.7935 6.75 14.007 6.97194 13.9998 7.2487C13.9628 8.6885 13.7533 12.75 12.5721 12.75H3.375C4.55631 12.75 4.3907 8.6885 4.42782 7.2487Z" fill="black" stroke="black" stroke-width="1.2" stroke-linejoin="round"/>
+<path d="M5.19598 12.625H3.66515C3.42618 12.625 3.22289 12.4453 3.18626 12.2017L1.94333 3.93602C1.89776 3.63295 2.12496 3.35938 2.42223 3.35938H5.78585C6.11241 3.35938 6.41702 3.52903 6.59618 3.81071L6.94517 4.35938H9.92811C10.4007 4.35938 10.8044 4.71102 10.8836 5.1917L11.1251 6.65624" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/file_icons/font.svg 🔗

@@ -1,3 +1,3 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M10.3352 13.2519H12.375M4.49719 13.2519L8.00001 2.74811L11.5028 13.2519M3.625 13.2519H5.6648M9.74908 9.16761H6.25095" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M10.3352 13.2519H12.375M4.49719 13.2519L8.00001 2.74811L11.5028 13.2519M3.625 13.2519H5.6648M9.74908 9.16761H6.25095" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/file_icons/git.svg 🔗

@@ -1,6 +1,6 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M5 13C6.10457 13 7 12.1046 7 11C7 9.89543 6.10457 9 5 9C3.89543 9 3 9.89543 3 11C3 12.1046 3.89543 13 5 13Z" stroke="black" stroke-width="1.5"/>
-<path d="M11 7C12.1046 7 13 6.10457 13 5C13 3.89543 12.1046 3 11 3C9.89543 3 9 3.89543 9 5C9 6.10457 9.89543 7 11 7Z" fill="black" stroke="black" stroke-width="1.5"/>
-<path d="M4.625 3.625V8.375" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
-<path d="M11 7C11 9.20914 9.20914 11 7 11" stroke="black" stroke-width="1.5"/>
+<path d="M5 13C6.10457 13 7 12.1046 7 11C7 9.89543 6.10457 9 5 9C3.89543 9 3 9.89543 3 11C3 12.1046 3.89543 13 5 13Z" stroke="black" stroke-width="1.2"/>
+<path d="M11 7C12.1046 7 13 6.10457 13 5C13 3.89543 12.1046 3 11 3C9.89543 3 9 3.89543 9 5C9 6.10457 9.89543 7 11 7Z" fill="black" stroke="black" stroke-width="1.2"/>
+<path d="M4.625 3.625V8.375" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
+<path d="M11 7C11 9.20914 9.20914 11 7 11" stroke="black" stroke-width="1.2"/>
 </svg>

assets/icons/file_icons/gleam.svg 🔗

@@ -1,7 +1,7 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
 <path d="M7.3848 9.30444C7.3848 9.30444 7.53254 10.2646 8.53248 10.0882C9.53242 9.91193 9.36378 8.95549 9.36378 8.95549" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M5.54155 5.54157C6.12355 4.90104 6.01688 2.62541 7.22875 2.3985C8.44063 2.17158 9.19097 4.33148 9.91982 4.6814C10.6487 5.03133 12.8517 4.3028 13.4381 5.38734C14.0244 6.47188 12.1395 7.95973 12.026 8.64088C11.9126 9.32203 13.3614 11.2416 12.4675 12.1701C11.5736 13.0986 9.73005 11.7545 8.90486 11.8834C8.07966 12.0123 6.79244 13.9095 5.67367 13.3502C4.55491 12.7909 5.16702 10.5455 4.82437 9.87612C4.48171 9.20673 2.34028 8.54978 2.4525 7.35049C2.56471 6.15121 4.95956 6.1821 5.54155 5.54157Z" stroke="#FF7676" stroke-opacity="0.52" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M5.54155 5.54157C6.12355 4.90104 6.01688 2.62541 7.22875 2.3985C8.44063 2.17158 9.19097 4.33148 9.91982 4.6814C10.6487 5.03133 12.8517 4.3028 13.4381 5.38734C14.0244 6.47188 12.1395 7.95973 12.026 8.64088C11.9126 9.32203 13.3614 11.2416 12.4675 12.1701C11.5736 13.0986 9.73005 11.7545 8.90486 11.8834C8.07966 12.0123 6.79244 13.9095 5.67367 13.3502C4.55491 12.7909 5.16702 10.5455 4.82437 9.87612C4.48171 9.20673 2.34028 8.54978 2.4525 7.35049C2.56471 6.15121 4.95956 6.1821 5.54155 5.54157Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M5.54155 5.54157C6.12355 4.90104 6.01688 2.62541 7.22875 2.3985C8.44063 2.17158 9.19097 4.33148 9.91982 4.6814C10.6487 5.03133 12.8517 4.3028 13.4381 5.38734C14.0244 6.47188 12.1395 7.95973 12.026 8.64088C11.9126 9.32203 13.3614 11.2416 12.4675 12.1701C11.5736 13.0986 9.73005 11.7545 8.90486 11.8834C8.07966 12.0123 6.79244 13.9095 5.67367 13.3502C4.55491 12.7909 5.16702 10.5455 4.82437 9.87612C4.48171 9.20673 2.34028 8.54978 2.4525 7.35049C2.56471 6.15121 4.95956 6.1821 5.54155 5.54157Z" stroke="#FF7676" stroke-opacity="0.52" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M5.54155 5.54157C6.12355 4.90104 6.01688 2.62541 7.22875 2.3985C8.44063 2.17158 9.19097 4.33148 9.91982 4.6814C10.6487 5.03133 12.8517 4.3028 13.4381 5.38734C14.0244 6.47188 12.1395 7.95973 12.026 8.64088C11.9126 9.32203 13.3614 11.2416 12.4675 12.1701C11.5736 13.0986 9.73005 11.7545 8.90486 11.8834C8.07966 12.0123 6.79244 13.9095 5.67367 13.3502C4.55491 12.7909 5.16702 10.5455 4.82437 9.87612C4.48171 9.20673 2.34028 8.54978 2.4525 7.35049C2.56471 6.15121 4.95956 6.1821 5.54155 5.54157Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 <circle cx="6.25098" cy="7.75" r="0.75" fill="black"/>
 <circle cx="10.1035" cy="7.25" r="0.75" fill="black"/>
 </svg>

assets/icons/file_icons/graphql.svg 🔗

@@ -1,6 +1,6 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M8 5L10.5981 9.5H5.40192L8 5Z" stroke="black" stroke-width="1.5"/>
-<path d="M8 3L12.3301 5.5V10.5L8 13L3.66987 10.5V5.5L8 3Z" stroke="black" stroke-width="1.5"/>
+<path d="M8 5L10.5981 9.5H5.40192L8 5Z" stroke="black" stroke-width="1.2"/>
+<path d="M8 3L12.3301 5.5V10.5L8 13L3.66987 10.5V5.5L8 3Z" stroke="black" stroke-width="1.2"/>
 <circle cx="3.5" cy="5.5" r="1" fill="black"/>
 <circle cx="12.5" cy="5.5" r="1" fill="black"/>
 <circle cx="8" cy="3" r="1" fill="black"/>

assets/icons/file_icons/hash.svg 🔗

@@ -1,6 +1,6 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M11.2795 3.63849L8.7478 12.0142" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
-<path d="M7.26626 3.99597L4.73462 12.3717" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
-<path d="M4.15991 6.37988H12.9099" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
-<path d="M3.09839 9.62408H11.8484" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
+<path d="M11.2795 3.63849L8.7478 12.0142" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
+<path d="M7.26626 3.99597L4.73462 12.3717" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
+<path d="M4.15991 6.37988H12.9099" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
+<path d="M3.09839 9.62408H11.8484" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
 </svg>

assets/icons/file_icons/heroku.svg 🔗

@@ -1,5 +1,5 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<rect x="2" y="2" width="12" height="12" rx="2" stroke="black" stroke-width="1.5"/>
+<rect x="2" y="2" width="12" height="12" rx="2" stroke="black" stroke-width="1.2"/>
 <path d="M6.74217 7.13317V4.26172V4.13672H6.61717H5.50781H5.38281V4.26172V8.8824V9.07567L5.55908 8.9964L6.33378 8.64803L6.33378 8.64803L6.33389 8.64798L6.33443 8.64774L6.3347 8.64762L6.3369 8.64667L6.34717 8.64225C6.35635 8.63832 6.37013 8.63248 6.38816 8.62501C6.42423 8.61006 6.47729 8.58857 6.54454 8.56272C6.67911 8.511 6.87014 8.44194 7.09531 8.3729C7.54765 8.2342 8.12948 8.09817 8.66592 8.09817C8.92095 8.09817 9.05676 8.16584 9.12979 8.241C9.2037 8.31708 9.23311 8.42118 9.23311 8.53361V11.7383V11.8633H9.35811H10.4922H10.6172V11.7383V8.53361V8.53322C10.6172 8.43725 10.6172 7.81276 10.1093 7.32276C9.86619 7.08825 9.42187 6.80777 8.69373 6.80777C8.00022 6.80777 7.28721 6.96253 6.74217 7.13317ZM8.45652 5.91932L8.29694 6.12171H8.55468H9.66094H9.71713L9.75443 6.07969C10.2672 5.502 10.5291 4.91889 10.616 4.27855L10.6353 4.13672H10.4922H9.38592H9.28153L9.26292 4.23944C9.1561 4.82914 8.88874 5.37113 8.45652 5.91932Z" fill="black" stroke="black" stroke-width="0.25"/>
 <path d="M5.38281 11.7383V12.01L5.58915 11.8332L6.83447 10.766L6.94523 10.671L6.83447 10.5761L5.58915 9.50891L5.38281 9.33207V9.60382V11.7383Z" fill="black" stroke="black" stroke-width="0.25"/>
 </svg>

assets/icons/file_icons/html.svg 🔗

@@ -1,5 +1,5 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M9.15741 4.17108L6.84277 11.8289" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
-<path d="M4.74951 6L2.74951 8L4.74951 10" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M11.25 10L13.25 8L11.25 6" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M9.15741 4.17108L6.84277 11.8289" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
+<path d="M4.74951 6L2.74951 8L4.74951 10" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M11.25 10L13.25 8L11.25 6" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/file_icons/image.svg 🔗

@@ -1,7 +1,7 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
 <path d="M7.5 4C7.91421 4 8.25 3.66421 8.25 3.25C8.25 2.83579 7.91421 2.5 7.5 2.5C7.08579 2.5 6.75 2.83579 6.75 3.25C6.75 3.66421 7.08579 4 7.5 4Z" fill="black" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
 <path d="M8 8L10 6L12 8H8Z" fill="black"/>
-<path d="M3 11L6 8L8.375 10.375" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M7 9L8.5 7.5L10 6L11.5 7.5L13 9" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M4.375 3H3.5C3.22386 3 3 3.22386 3 3.5V12.5C3 12.7761 3.22386 13 3.5 13H8.35938M10.6406 3H12.5C12.7761 3 13 3.22386 13 3.5V12.5C13 12.7761 12.7761 13 12.5 13H11.125" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
+<path d="M3 11L6 8L8.375 10.375" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M7 9L8.5 7.5L10 6L11.5 7.5L13 9" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M4.375 3H3.5C3.22386 3 3 3.22386 3 3.5V12.5C3 12.7761 3.22386 13 3.5 13H8.35938M10.6406 3H12.5C12.7761 3 13 3.22386 13 3.5V12.5C13 12.7761 12.7761 13 12.5 13H11.125" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
 </svg>

assets/icons/file_icons/java.svg 🔗

@@ -1,7 +1,7 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M4.99219 8.56632C6.5 9 10.5415 8.99989 12 7.99995C13.4585 7 12.5 9.49999 12.5 10" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
-<path d="M11.5 13C9 13.5781 6 13.5938 4 13" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
-<path d="M10.0156 10.9844C8.51562 11.2031 6.5 11.2031 5 10.8906" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
-<path d="M6.5625 6.5C6.34375 6 6.06838 4.93125 6.99999 4.03125C7.93161 3.13125 8.58082 3.33636 9.00002 2" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
-<path d="M9.18477 6.50002C8.88168 6.05002 8.40637 5.71875 9.00014 5.40625C9.5939 5.09375 10.3126 4.65625 10.8751 3.53125" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
+<path d="M4.99219 8.56632C6.5 9 10.5415 8.99989 12 7.99995C13.4585 7 12.5 9.49999 12.5 10" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
+<path d="M11.5 13C9 13.5781 6 13.5938 4 13" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
+<path d="M10.0156 10.9844C8.51562 11.2031 6.5 11.2031 5 10.8906" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
+<path d="M6.5625 6.5C6.34375 6 6.06838 4.93125 6.99999 4.03125C7.93161 3.13125 8.58082 3.33636 9.00002 2" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
+<path d="M9.18477 6.50002C8.88168 6.05002 8.40637 5.71875 9.00014 5.40625C9.5939 5.09375 10.3126 4.65625 10.8751 3.53125" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
 </svg>

assets/icons/file_icons/lock.svg 🔗

@@ -1,4 +1,4 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M5 5C5 3.89543 5.89543 3 7 3H9C10.1046 3 11 3.89543 11 5V6H5V5Z" stroke="black" stroke-width="1.5"/>
+<path d="M5 5C5 3.89543 5.89543 3 7 3H9C10.1046 3 11 3.89543 11 5V6H5V5Z" stroke="black" stroke-width="1.2"/>
 <path fill-rule="evenodd" clip-rule="evenodd" d="M3.25 6.5C3.25 5.80964 3.80964 5.25 4.5 5.25H11.5C12.1904 5.25 12.75 5.80964 12.75 6.5V12.5C12.75 13.1904 12.1904 13.75 11.5 13.75H4.5C3.80964 13.75 3.25 13.1904 3.25 12.5V6.5ZM8.75 9.66146C8.90559 9.48517 9 9.25361 9 9C9 8.44772 8.55228 8 8 8C7.44772 8 7 8.44772 7 9C7 9.25361 7.09441 9.48517 7.25 9.66146V11C7.25 11.4142 7.58579 11.75 8 11.75C8.41421 11.75 8.75 11.4142 8.75 11V9.66146Z" fill="black"/>
 </svg>

assets/icons/file_icons/magnifying_glass.svg 🔗

@@ -1,3 +1,3 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M13 13L10.4138 10.4138M3 7.31034C3 4.92981 4.92981 3 7.31034 3C9.6909 3 11.6207 4.92981 11.6207 7.31034C11.6207 9.6909 9.6909 11.6207 7.31034 11.6207C4.92981 11.6207 3 9.6909 3 7.31034Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M13 13L10.4138 10.4138M3 7.31034C3 4.92981 4.92981 3 7.31034 3C9.6909 3 11.6207 4.92981 11.6207 7.31034C11.6207 9.6909 9.6909 11.6207 7.31034 11.6207C4.92981 11.6207 3 9.6909 3 7.31034Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/file_icons/nix.svg 🔗

@@ -1,8 +1,8 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M6.00005 4.76556L4.76569 2.74996M6.00005 4.76556L3.75 4.76563M6.00005 4.76556L7.25006 4.7656" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
-<path d="M10.0232 11.2311L11.2675 13.2406M10.0232 11.2311L12.2732 11.2199M10.0232 11.2311L8.7732 11.2373" stroke="black" stroke-opacity="0.5" stroke-width="1.5" stroke-linecap="round"/>
-<path d="M9.99025 4.91551L10.9985 2.77781M9.99025 4.91551L8.75599 3.03419M9.99025 4.91551L10.6759 5.9607" stroke="black" stroke-opacity="0.5" stroke-width="1.5" stroke-linecap="round"/>
-<path d="M6.0323 11.1009L5.03465 13.2436M6.0323 11.1009L7.27585 12.9761M6.0323 11.1009L5.34151 10.0592" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
-<path d="M11.883 8.19023L14.2466 8.19287M11.883 8.19023L13.0602 6.27268M11.883 8.19023L11.229 9.25547" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
-<path d="M4.12354 7.8356L1.76002 7.84465M4.12354 7.8356L2.95585 9.75894M4.12354 7.8356L4.7723 6.76713" stroke="black" stroke-opacity="0.5" stroke-width="1.5" stroke-linecap="round"/>
+<path d="M6.00005 4.76556L4.76569 2.74996M6.00005 4.76556L3.75 4.76563M6.00005 4.76556L7.25006 4.7656" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
+<path d="M10.0232 11.2311L11.2675 13.2406M10.0232 11.2311L12.2732 11.2199M10.0232 11.2311L8.7732 11.2373" stroke="black" stroke-opacity="0.5" stroke-width="1.2" stroke-linecap="round"/>
+<path d="M9.99025 4.91551L10.9985 2.77781M9.99025 4.91551L8.75599 3.03419M9.99025 4.91551L10.6759 5.9607" stroke="black" stroke-opacity="0.5" stroke-width="1.2" stroke-linecap="round"/>
+<path d="M6.0323 11.1009L5.03465 13.2436M6.0323 11.1009L7.27585 12.9761M6.0323 11.1009L5.34151 10.0592" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
+<path d="M11.883 8.19023L14.2466 8.19287M11.883 8.19023L13.0602 6.27268M11.883 8.19023L11.229 9.25547" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
+<path d="M4.12354 7.8356L1.76002 7.84465M4.12354 7.8356L2.95585 9.75894M4.12354 7.8356L4.7723 6.76713" stroke="black" stroke-opacity="0.5" stroke-width="1.2" stroke-linecap="round"/>
 </svg>

assets/icons/file_icons/notebook.svg 🔗

@@ -1,8 +1,8 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
 <path d="M3.03125 3.96875C3.03125 3.41647 3.47897 2.96875 4.03125 2.96875H6V13H4.03125C3.47897 13 3.03125 12.5523 3.03125 12V3.96875Z" fill="black"/>
-<path d="M12.5 3H3.5C3.22386 3 3 3.22386 3 3.5V12.5C3 12.7761 3.22386 13 3.5 13H12.5C12.7761 13 13 12.7761 13 12.5V3.5C13 3.22386 12.7761 3 12.5 3Z" stroke="black" stroke-width="1.5"/>
-<path d="M10.5 5.75H8.5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M10.5 8H8.5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M10.5 10.25H8.5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M6 3V14" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M12.5 3H3.5C3.22386 3 3 3.22386 3 3.5V12.5C3 12.7761 3.22386 13 3.5 13H12.5C12.7761 13 13 12.7761 13 12.5V3.5C13 3.22386 12.7761 3 12.5 3Z" stroke="black" stroke-width="1.2"/>
+<path d="M10.5 5.75H8.5" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M10.5 8H8.5" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M10.5 10.25H8.5" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M6 3V14" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/file_icons/package.svg 🔗

@@ -1,4 +1,4 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M2.62671 4.88474L7.99977 7.78519M2.62671 4.88474L2.63131 10.9001L8.00436 13.8005M2.62671 4.88474L5.31111 3.54213M7.99977 7.78519L8.00436 13.8005M7.99977 7.78519L10.6841 6.33086M8.00436 13.8005L13.3729 10.8919L13.3683 4.87654M5.31111 3.54213L7.9955 2.19952L13.3683 4.87654M5.31111 3.54213L10.6841 6.33086M10.6841 6.33086L13.3683 4.87654" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M2.62671 4.88474L7.99977 7.78519M2.62671 4.88474L2.63131 10.9001L8.00436 13.8005M2.62671 4.88474L5.31111 3.54213M7.99977 7.78519L8.00436 13.8005M7.99977 7.78519L10.6841 6.33086M8.00436 13.8005L13.3729 10.8919L13.3683 4.87654M5.31111 3.54213L7.9955 2.19952L13.3683 4.87654M5.31111 3.54213L10.6841 6.33086M10.6841 6.33086L13.3683 4.87654" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 <path d="M8.03125 13.5625V7.78125L2.5625 4.9375V10.75L8.03125 13.5625Z" fill="black"/>
 </svg>

assets/icons/file_icons/phoenix.svg 🔗

@@ -1,4 +1,4 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-    <path d="M13 9C13 8.32138 12.9375 7.5 12.7188 6.75C12.0625 7.53125 10.875 8.1875 10 8.5C10.75 5.90625 9.5625 3.1875 8 3C8 4.96875 7.625 5.90625 6.5 7.5C5 5 3.5 6.5 3 7C3.5 7.5 4.21832 8.24064 4.34375 9.3125C4.6875 12.25 6.75 13 8.5 13C10.25 13 10.5 11 12.5 12" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+    <path d="M13 9C13 8.32138 12.9375 7.5 12.7188 6.75C12.0625 7.53125 10.875 8.1875 10 8.5C10.75 5.90625 9.5625 3.1875 8 3C8 4.96875 7.625 5.90625 6.5 7.5C5 5 3.5 6.5 3 7C3.5 7.5 4.21832 8.24064 4.34375 9.3125C4.6875 12.25 6.75 13 8.5 13C10.25 13 10.5 11 12.5 12" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
     <path d="M5.03125 9.15625C5.87694 9.15625 6.5625 8.47069 6.5625 7.625C6.5625 6.77931 5.87694 6.09375 5.03125 6.09375C4.18556 6.09375 3.5 6.77931 3.5 7.625C3.5 8.47069 4.18556 9.15625 5.03125 9.15625Z" fill="black"/>
 </svg>

assets/icons/file_icons/plus.svg 🔗

@@ -1,3 +1,3 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M8 4V12M12 8H4" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
+<path d="M8 4V12M12 8H4" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
 </svg>

assets/icons/file_icons/prettier.svg 🔗

@@ -1,12 +1,12 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-    <path d="M3 3.86328H9.51563" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
-    <path d="M12 3.86328H13" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
-    <path d="M10.6406 6.62628H13" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
-    <path d="M5.79688 6.62628H8.15625" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
-    <path d="M3 6.62628H3.35937" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
-    <path d="M8.15625 9.37372H13" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
-    <path d="M3 9.37372H5.64062" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
-    <path d="M3 12.1094H4.54687" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
-    <path d="M6.97656 12.1094H9.35938" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
-    <path d="M11.8203 12.1094H13" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
+    <path d="M3 3.86328H9.51563" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
+    <path d="M12 3.86328H13" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
+    <path d="M10.6406 6.62628H13" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
+    <path d="M5.79688 6.62628H8.15625" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
+    <path d="M3 6.62628H3.35937" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
+    <path d="M8.15625 9.37372H13" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
+    <path d="M3 9.37372H5.64062" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
+    <path d="M3 12.1094H4.54687" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
+    <path d="M6.97656 12.1094H9.35938" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
+    <path d="M11.8203 12.1094H13" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
 </svg>

assets/icons/file_icons/project.svg 🔗

@@ -1,5 +1,5 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M3.03125 3V3.03125M3.03125 3.03125V9M3.03125 3.03125C3.03125 5 6 5 6 5M3.03125 9C3.03125 11 6 11 6 11M3.03125 9V12" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M3.03125 3V3.03125M3.03125 3.03125V9M3.03125 3.03125C3.03125 5 6 5 6 5M3.03125 9C3.03125 11 6 11 6 11M3.03125 9V12" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 <rect x="8" y="2.5" width="6" height="5" rx="1.5" fill="black"/>
 <rect x="8" y="8.46875" width="6" height="5.0625" rx="1.5" fill="black"/>
 </svg>

assets/icons/file_icons/python.svg 🔗

@@ -1,6 +1,6 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M7.18452 2.91638C6.01625 2.91638 4.98489 3.77623 4.91991 4.94678H4.72024C3.81569 4.94678 3 5.63731 3 6.58698V8.10978C3 9.05945 3.81569 9.74998 4.72024 9.74998H5.33631C5.67376 9.74998 6.02976 9.48559 6.02976 9.06153C6.02976 8.46056 6.51694 7.97338 7.11791 7.97338H8.27976C9.18431 7.97338 10 7.28286 10 6.33318V5.06418C10 3.83417 8.93913 2.91638 7.73214 2.91638H7.18452Z" stroke="black" stroke-width="1.5"/>
-<path d="M8.79613 13.0836C9.97889 13.0836 11.0103 12.2025 11.0702 11.0191H11.2738C12.1885 11.0191 13 10.3146 13 9.36187V7.8135C13 6.86077 12.1885 6.15625 11.2738 6.15625H10.6544C10.3099 6.15625 9.96057 6.42749 9.96057 6.84577C9.96057 7.46262 9.46051 7.96268 8.84365 7.96268H7.69494C6.78027 7.96268 5.96875 8.6672 5.96875 9.61993V10.9102C5.96875 12.148 7.02678 13.0836 8.24554 13.0836H8.79613Z" stroke="black" stroke-width="1.5"/>
+<path d="M7.18452 2.91638C6.01625 2.91638 4.98489 3.77623 4.91991 4.94678H4.72024C3.81569 4.94678 3 5.63731 3 6.58698V8.10978C3 9.05945 3.81569 9.74998 4.72024 9.74998H5.33631C5.67376 9.74998 6.02976 9.48559 6.02976 9.06153C6.02976 8.46056 6.51694 7.97338 7.11791 7.97338H8.27976C9.18431 7.97338 10 7.28286 10 6.33318V5.06418C10 3.83417 8.93913 2.91638 7.73214 2.91638H7.18452Z" stroke="black" stroke-width="1.2"/>
+<path d="M8.79613 13.0836C9.97889 13.0836 11.0103 12.2025 11.0702 11.0191H11.2738C12.1885 11.0191 13 10.3146 13 9.36187V7.8135C13 6.86077 12.1885 6.15625 11.2738 6.15625H10.6544C10.3099 6.15625 9.96057 6.42749 9.96057 6.84577C9.96057 7.46262 9.46051 7.96268 8.84365 7.96268H7.69494C6.78027 7.96268 5.96875 8.6672 5.96875 9.61993V10.9102C5.96875 12.148 7.02678 13.0836 8.24554 13.0836H8.79613Z" stroke="black" stroke-width="1.2"/>
 <path d="M7.20312 6.01758C7.64323 6.01758 8 5.6608 8 5.2207C8 4.7806 7.64323 4.42383 7.20312 4.42383C6.76302 4.42383 6.40625 4.7806 6.40625 5.2207C6.40625 5.6608 6.76302 6.01758 7.20312 6.01758Z" fill="black"/>
 <path d="M8.79687 11.5939C9.23698 11.5939 9.59375 11.2372 9.59375 10.7971C9.59375 10.357 9.23698 10.0002 8.79687 10.0002C8.35677 10.0002 8 10.357 8 10.7971C8 11.2372 8.35677 11.5939 8.79687 11.5939Z" fill="black"/>
 </svg>

assets/icons/file_icons/replace.svg 🔗

@@ -1,5 +1,5 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M7.99988 13C5.97267 13 4.22723 11.7936 3.44238 10.0595M7.99988 3C10.1122 3 11.9185 4.30981 12.6511 6.16152" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M7.99988 13C5.97267 13 4.22723 11.7936 3.44238 10.0595M7.99988 3C10.1122 3 11.9185 4.30981 12.6511 6.16152" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 <path d="M2.65625 3.29688C3.00143 3.29688 3.28125 3.01705 3.28125 2.67188C3.28125 2.3267 3.00143 2.04688 2.65625 2.04688C2.31107 2.04688 2.03125 2.3267 2.03125 2.67188C2.03125 3.01705 2.31107 3.29688 2.65625 3.29688Z" fill="black"/>
 <path d="M4.71094 3.29688C5.05612 3.29688 5.33594 3.01705 5.33594 2.67188C5.33594 2.3267 5.05612 2.04688 4.71094 2.04688C4.36576 2.04688 4.08594 2.3267 4.08594 2.67188C4.08594 3.01705 4.36576 3.29688 4.71094 3.29688Z" fill="black"/>
 <path d="M5.96094 4.99219C6.30612 4.99219 6.58594 4.71237 6.58594 4.36719C6.58594 4.02201 6.30612 3.74219 5.96094 3.74219C5.61576 3.74219 5.33594 4.02201 5.33594 4.36719C5.33594 4.71237 5.61576 4.99219 5.96094 4.99219Z" fill="black"/>

assets/icons/file_icons/replace_next.svg 🔗

@@ -1,4 +1,4 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-    <path d="M8.5 6.88672C10.433 6.88672 12 8.45372 12 10.3867V13M12 13L14 11M12 13L10 11" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+    <path d="M8.5 6.88672C10.433 6.88672 12 8.45372 12 10.3867V13M12 13L14 11M12 13L10 11" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>

assets/icons/file_icons/rust.svg 🔗

@@ -1,4 +1,4 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M5.27935 10.9821C5.32063 10.4038 4.9204 9.89049 4.35998 9.80276L3.60081 9.68387C3.37979 9.64945 3.20167 9.48001 3.15225 9.25614L3.01378 8.63511C2.96382 8.41235 3.05233 8.1807 3.23696 8.05125L3.8631 7.61242C4.33337 7.28297 4.47456 6.6369 4.18621 6.13364L3.79467 5.45092C3.68118 5.25261 3.69801 5.00374 3.83757 4.82321L4.22314 4.32436C4.3627 4.14438 4.59621 4.06994 4.81071 4.13772L5.57531 4.37769C6.11944 4.54879 6.70048 4.26159 6.90683 3.71886L7.1811 2.99782C7.26255 2.78395 7.46345 2.64285 7.68772 2.6423L8.31007 2.64063C8.53434 2.64007 8.73579 2.78006 8.81834 2.99337L9.09965 3.72275C9.30821 4.26214 9.88655 4.54712 10.429 4.37714L11.1632 4.14716C11.3772 4.07994 11.6096 4.15382 11.7492 4.3327L12.1374 4.83099" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M5.27935 10.9821C5.32063 10.4038 4.9204 9.89049 4.35998 9.80276L3.60081 9.68387C3.37979 9.64945 3.20167 9.48001 3.15225 9.25614L3.01378 8.63511C2.96382 8.41235 3.05233 8.1807 3.23696 8.05125L3.8631 7.61242C4.33337 7.28297 4.47456 6.6369 4.18621 6.13364L3.79467 5.45092C3.68118 5.25261 3.69801 5.00374 3.83757 4.82321L4.22314 4.32436C4.3627 4.14438 4.59621 4.06994 4.81071 4.13772L5.57531 4.37769C6.11944 4.54879 6.70048 4.26159 6.90683 3.71886L7.1811 2.99782C7.26255 2.78395 7.46345 2.64285 7.68772 2.6423L8.31007 2.64063C8.53434 2.64007 8.73579 2.78006 8.81834 2.99337L9.09965 3.72275C9.30821 4.26214 9.88655 4.54712 10.429 4.37714L11.1632 4.14716C11.3772 4.07994 11.6096 4.15382 11.7492 4.3327L12.1374 4.83099" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>

assets/icons/file_icons/scala.svg 🔗

@@ -1,7 +1,7 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
 <path d="M12.5827 1.27457C10.4978 2.48798 6.32365 2.94703 3.49893 2.99561C3.22283 3.00036 3.00001 3.22384 3.00001 3.49998L3.00002 6.00003C3.00002 6.27617 3.22284 6.50038 3.49895 6.49563C6.42334 6.44533 10.7942 5.95506 12.7954 4.64327C12.927 4.557 13 4.40741 13 4.25004L13 1.50027C13 1.29426 12.7607 1.17094 12.5827 1.27457Z" fill="black"/>
 <path d="M12.3072 6.51584C12.6851 6.6855 12.8539 7.12936 12.6842 7.50724C12.5145 7.88511 12.0707 8.05391 11.6928 7.88425L12.3072 6.51584ZM3 5.02142C4.32178 5.02142 6.01669 5.1159 7.68605 5.34579C9.34359 5.57406 11.0313 5.94302 12.3072 6.51584L11.6928 7.88425C10.611 7.39853 9.08921 7.05318 7.48142 6.83177C5.88546 6.61199 4.25922 6.52142 3 6.52142L3 5.02142Z" fill="black"/>
-<path d="M3 10.0214C5.581 10.0214 9.64229 10.3915 12 11.45" stroke="black" stroke-width="1.5"/>
+<path d="M3 10.0214C5.581 10.0214 9.64229 10.3915 12 11.45" stroke="black" stroke-width="1.2"/>
 <path d="M12.1401 10.0067C9.94879 11.0472 6.13586 11.4503 3.49893 11.4956C3.22283 11.5004 3.00001 11.7238 3.00001 12L3.00002 14.5C3.00002 14.7762 3.22284 15.0004 3.49895 14.9956C6.42334 14.9453 10.7942 14.4551 12.7954 13.1433C12.927 13.057 13 12.9074 13 12.75L13 10.5002C13 10.0882 12.5123 9.82995 12.1401 10.0067Z" fill="black"/>
 <path d="M12.1401 5.75668C9.94879 6.7972 6.13586 7.20026 3.49893 7.24561C3.22283 7.25036 3.00001 7.47384 3.00001 7.74998L3.00002 10.25C3.00002 10.5262 3.22284 10.7504 3.49895 10.7456C6.42334 10.6953 10.7942 10.2051 12.7954 8.89327C12.927 8.807 13 8.65741 13 8.50004L13 6.25023C13 5.83821 12.5123 5.57995 12.1401 5.75668Z" fill="black"/>
 </svg>

assets/icons/file_icons/tcl.svg 🔗

@@ -1,3 +1,3 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M3.03125 13C3.46875 12.2812 3.68556 12.0378 4.0625 11.5312M4.0625 11.5312C4.0625 9.86595 4.27768 8.02844 4.75687 7M4.0625 11.5312C4.75687 10.5981 6.6875 8.57812 7.92188 7.54688M4.0625 11.5312C7.875 11.5312 10.0507 9.46738 11.4062 8.03125C11.5818 7.84528 11.2307 7.34164 10.9157 6.96235C10.7718 6.78906 10.8964 6.50073 11.1213 6.48823C11.6657 6.45798 12.3874 6.36175 12.5 6.06684C12.7544 5.4003 12.9585 4.2437 13.0409 3.28832C13.0541 3.13644 12.9264 3.01119 12.7745 3.0243C10.5824 3.21343 8.22052 3.5262 6.5 4.82764" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
+<path d="M3.03125 13C3.46875 12.2812 3.68556 12.0378 4.0625 11.5312M4.0625 11.5312C4.0625 9.86595 4.27768 8.02844 4.75687 7M4.0625 11.5312C4.75687 10.5981 6.6875 8.57812 7.92188 7.54688M4.0625 11.5312C7.875 11.5312 10.0507 9.46738 11.4062 8.03125C11.5818 7.84528 11.2307 7.34164 10.9157 6.96235C10.7718 6.78906 10.8964 6.50073 11.1213 6.48823C11.6657 6.45798 12.3874 6.36175 12.5 6.06684C12.7544 5.4003 12.9585 4.2437 13.0409 3.28832C13.0541 3.13644 12.9264 3.01119 12.7745 3.0243C10.5824 3.21343 8.22052 3.5262 6.5 4.82764" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
 </svg>

assets/icons/file_icons/toml.svg 🔗

@@ -1,5 +1,5 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M6 6H10" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
-<path d="M8 6V11" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
-<path d="M5 3H3.5C3.22386 3 3 3.22386 3 3.5V12.5C3 12.7761 3.22386 13 3.5 13H5M11 3H12.5C12.7761 3 13 3.22386 13 3.5V12.5C13 12.7761 12.7761 13 12.5 13H11" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
+<path d="M6 6H10" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
+<path d="M8 6V11" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
+<path d="M5 3H3.5C3.22386 3 3 3.22386 3 3.5V12.5C3 12.7761 3.22386 13 3.5 13H5M11 3H12.5C12.7761 3 13 3.22386 13 3.5V12.5C13 12.7761 12.7761 13 12.5 13H11" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
 </svg>

assets/icons/file_icons/video.svg 🔗

@@ -1,4 +1,4 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M2.65625 3H12.8437C13.1199 3 13.3438 3.22386 13.3438 3.5V10.3438M13.3438 13H3.15625C2.88011 13 2.65625 12.7761 2.65625 12.5V5.65625" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
-<path d="M10 8.01562L6.65625 10.3125V5.6875L10 8.01562Z" fill="black" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M2.65625 3H12.8437C13.1199 3 13.3438 3.22386 13.3438 3.5V10.3438M13.3438 13H3.15625C2.88011 13 2.65625 12.7761 2.65625 12.5V5.65625" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
+<path d="M10 8.01562L6.65625 10.3125V5.6875L10 8.01562Z" fill="black" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/file_icons/vue.svg 🔗

@@ -1,4 +1,4 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
 <path d="M8 9.13502L4.578 3.21202L4.47016 3.02509C4.42551 2.9477 4.34295 2.90002 4.25361 2.90002H2.43302C2.24057 2.90002 2.12029 3.10836 2.21651 3.27503L7.7835 12.917C7.87972 13.0837 8.12028 13.0837 8.2165 12.917L13.7835 3.27503C13.8797 3.10836 13.7594 2.90002 13.567 2.90002H11.7443C11.655 2.90002 11.5725 2.94767 11.5278 3.02502L8 9.13502Z" fill="black"/>
-<path d="M3.5 3.65002H6.80469L8 5.73596L9.20312 3.65002H12.5234" stroke="black" stroke-width="1.5"/>
+<path d="M3.5 3.65002H6.80469L8 5.73596L9.20312 3.65002H12.5234" stroke="black" stroke-width="1.2"/>
 </svg>

assets/icons/file_lock.svg 🔗

@@ -1,4 +1,4 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M5 5C5 3.89543 5.89543 3 7 3H9C10.1046 3 11 3.89543 11 5V6H5V5Z" stroke="black" stroke-width="1.5"/>
+<path d="M5 5C5 3.89543 5.89543 3 7 3H9C10.1046 3 11 3.89543 11 5V6H5V5Z" stroke="black" stroke-width="1.2"/>
 <path fill-rule="evenodd" clip-rule="evenodd" d="M3.25 6.5C3.25 5.80964 3.80964 5.25 4.5 5.25H11.5C12.1904 5.25 12.75 5.80964 12.75 6.5V12.5C12.75 13.1904 12.1904 13.75 11.5 13.75H4.5C3.80964 13.75 3.25 13.1904 3.25 12.5V6.5ZM8.75 9.66146C8.90559 9.48517 9 9.25361 9 9C9 8.44772 8.55228 8 8 8C7.44772 8 7 8.44772 7 9C7 9.25361 7.09441 9.48517 7.25 9.66146V11C7.25 11.4142 7.58579 11.75 8 11.75C8.41421 11.75 8.75 11.4142 8.75 11V9.66146Z" fill="black"/>
 </svg>

assets/icons/file_markdown.svg 🔗

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><g stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" clip-path="url(#a)"><path d="M13 13.5v-8L9.5 2h-6a.5.5 0 0 0-.5.5V6"/><path d="M9 2v4h4M8 9.5V13h1a1.75 1.75 0 0 0 0-3.5H8ZM6 13V9.5L4.25 12 2.5 9.5V13"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

assets/icons/file_rust.svg 🔗

@@ -1,4 +1,4 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M5.27935 10.9821C5.32063 10.4038 4.9204 9.89049 4.35998 9.80276L3.60081 9.68387C3.37979 9.64945 3.20167 9.48001 3.15225 9.25614L3.01378 8.63511C2.96382 8.41235 3.05233 8.1807 3.23696 8.05125L3.8631 7.61242C4.33337 7.28297 4.47456 6.6369 4.18621 6.13364L3.79467 5.45092C3.68118 5.25261 3.69801 5.00374 3.83757 4.82321L4.22314 4.32436C4.3627 4.14438 4.59621 4.06994 4.81071 4.13772L5.57531 4.37769C6.11944 4.54879 6.70048 4.26159 6.90683 3.71886L7.1811 2.99782C7.26255 2.78395 7.46345 2.64285 7.68772 2.6423L8.31007 2.64063C8.53434 2.64007 8.73579 2.78006 8.81834 2.99337L9.09965 3.72275C9.30821 4.26214 9.88655 4.54712 10.429 4.37714L11.1632 4.14716C11.3772 4.07994 11.6096 4.15382 11.7492 4.3327L12.1374 4.83099" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M5.27935 10.9821C5.32063 10.4038 4.9204 9.89049 4.35998 9.80276L3.60081 9.68387C3.37979 9.64945 3.20167 9.48001 3.15225 9.25614L3.01378 8.63511C2.96382 8.41235 3.05233 8.1807 3.23696 8.05125L3.8631 7.61242C4.33337 7.28297 4.47456 6.6369 4.18621 6.13364L3.79467 5.45092C3.68118 5.25261 3.69801 5.00374 3.83757 4.82321L4.22314 4.32436C4.3627 4.14438 4.59621 4.06994 4.81071 4.13772L5.57531 4.37769C6.11944 4.54879 6.70048 4.26159 6.90683 3.71886L7.1811 2.99782C7.26255 2.78395 7.46345 2.64285 7.68772 2.6423L8.31007 2.64063C8.53434 2.64007 8.73579 2.78006 8.81834 2.99337L9.09965 3.72275C9.30821 4.26214 9.88655 4.54712 10.429 4.37714L11.1632 4.14716C11.3772 4.07994 11.6096 4.15382 11.7492 4.3327L12.1374 4.83099" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>

assets/icons/file_search.svg 🔗

@@ -1,5 +0,0 @@
-<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M5.2345 20.1C5.38772 20.373 5.60794 20.5998 5.87313 20.7577C6.13832 20.9157 6.43919 20.9992 6.74562 21H17.25C17.7141 21 18.1592 20.8104 18.4874 20.4728C18.8156 20.1352 19 19.6774 19 19.2V7.5L14.625 3H6.75C6.28587 3 5.84075 3.18964 5.51256 3.52721C5.18437 3.86477 5 4.32261 5 4.8V6.5" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M10 16.8182L8.5 15.3182" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M6 15.8182C7.65685 15.8182 9 14.475 9 12.8182C9 11.1613 7.65685 9.81818 6 9.81818C4.34315 9.81818 3 11.1613 3 12.8182C3 14.475 4.34315 15.8182 6 15.8182Z" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
-</svg>

assets/icons/file_text.svg 🔗

@@ -1,6 +0,0 @@
-<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M9.70035 2.55853H4.59897C4.29831 2.55853 4.00997 2.67319 3.79737 2.87729C3.58477 3.08138 3.46533 3.35819 3.46533 3.64683V12.3532C3.46533 12.6418 3.58477 12.9186 3.79737 13.1227C4.00997 13.3268 4.29831 13.4415 4.59897 13.4415H11.4008C11.7015 13.4415 11.9898 13.3268 12.2024 13.1227C12.415 12.9186 12.5344 12.6418 12.5344 12.3532V5.27927L9.70035 2.55853Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M8.90698 2.55853V4.97696C8.90698 5.29767 9.03438 5.60523 9.26115 5.83201C9.48793 6.05878 9.79549 6.18618 10.1162 6.18618H12.5346" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M8.4534 8.5L5.73267 8.5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M10.2672 10.7207L5.73267 10.7207" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-</svg>

assets/icons/file_text_filled.svg 🔗

@@ -0,0 +1,3 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M9.875 1.25C10.0686 1.25 10.2549 1.3249 10.3945 1.45898L13.5195 4.45898C13.6668 4.60041 13.75 4.79582 13.75 5V12.7998C13.75 13.327 13.5316 13.8263 13.1533 14.1895C12.7762 14.5515 12.2709 14.75 11.75 14.75H4.25C3.72915 14.75 3.22383 14.5515 2.84668 14.1895C2.46849 13.8264 2.25 13.3271 2.25 12.7998V3.2002C2.25 2.67294 2.46848 2.17365 2.84668 1.81055C3.22388 1.44843 3.72922 1.25 4.25 1.25H9.875ZM5.73242 9.9707C5.31832 9.97084 4.98242 10.3066 4.98242 10.7207C4.98242 11.1348 5.31832 11.4706 5.73242 11.4707H10.2666C10.6808 11.4707 11.0166 11.1349 11.0166 10.7207C11.0166 10.3065 10.6808 9.9707 10.2666 9.9707H5.73242ZM5.73242 7.75C5.31832 7.75013 4.98242 8.08587 4.98242 8.5C4.98242 8.91413 5.31832 9.24987 5.73242 9.25H8.45312C8.86734 9.25 9.20312 8.91421 9.20312 8.5C9.20312 8.08579 8.86734 7.75 8.45312 7.75H5.73242Z" fill="black"/>
+</svg>

assets/icons/file_text_outlined.svg 🔗

@@ -0,0 +1,6 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M9.87504 2H4.25001C3.91848 2 3.60055 2.12643 3.36612 2.35148C3.1317 2.57652 3 2.88174 3 3.2V12.8C3 13.1182 3.1317 13.4234 3.36612 13.6485C3.60055 13.8735 3.91848 14 4.25001 14H11.75C12.0816 14 12.3995 13.8735 12.6339 13.6485C12.8683 13.4234 13 13.1182 13 12.8V5L9.87504 2Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M9 2V4.66666C9 5.02029 9.14048 5.35942 9.39053 5.60948C9.64059 5.85952 9.97972 6 10.3333 6H13" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M8.4534 8.5H5.73267" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M10.2672 10.7207H5.73267" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

assets/icons/file_toml.svg 🔗

@@ -1,5 +1,5 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M6 6H10" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
-<path d="M8 6V11" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
-<path d="M5 3H3.5C3.22386 3 3 3.22386 3 3.5V12.5C3 12.7761 3.22386 13 3.5 13H5M11 3H12.5C12.7761 3 13 3.22386 13 3.5V12.5C13 12.7761 12.7761 13 12.5 13H11" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
+<path d="M6 6H10" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
+<path d="M8 6V11" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
+<path d="M5 3H3.5C3.22386 3 3 3.22386 3 3.5V12.5C3 12.7761 3.22386 13 3.5 13H5M11 3H12.5C12.7761 3 13 3.22386 13 3.5V12.5C13 12.7761 12.7761 13 12.5 13H11" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
 </svg>

assets/icons/file_tree.svg 🔗

@@ -1,5 +1,5 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M3 3V3.03125M3 3.03125V9M3 3.03125C3 5 5.96875 5 5.96875 5M3 9C3 11 5.96875 11 5.96875 11M3 9V12" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<rect x="8" y="3" width="5.5" height="4" rx="1.5" fill="black"/>
-<rect x="8" y="9" width="5.5" height="4" rx="1.5" fill="black"/>
+<path d="M3 2.5V3.5M3 3.5V9M3 3.5C3 5.46875 5.96875 5 5.96875 5M3 9C3 11 5.96875 11 5.96875 11M3 9V12.5" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M12 3H9.5C8.67157 3 8 3.67157 8 4.5V5.5C8 6.32843 8.67157 7 9.5 7H12C12.8284 7 13.5 6.32843 13.5 5.5V4.5C13.5 3.67157 12.8284 3 12 3Z" fill="black"/>
+<path d="M12 9H9.5C8.67157 9 8 9.67157 8 10.5V11.5C8 12.3284 8.67157 13 9.5 13H12C12.8284 13 13.5 12.3284 13.5 11.5V10.5C13.5 9.67157 12.8284 9 12 9Z" fill="black"/>
 </svg>

assets/icons/filter.svg 🔗

@@ -1,3 +1,3 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M12.9416 2.99643C13.08 2.79636 12.9568 2.5 12.7352 2.5H3.26475C3.04317 2.5 2.91999 2.79636 3.0584 2.99643L6.04033 7.30646C6.24713 7.60535 6.35981 7.97674 6.35981 8.3596C6.35981 9.18422 6.35981 11.4639 6.35981 12.891C6.35981 13.2285 6.59643 13.5 6.88831 13.5H9.11168C9.40357 13.5 9.64019 13.2285 9.64019 12.891C9.64019 11.4639 9.64019 9.18422 9.64019 8.3596C9.64019 7.97674 9.75289 7.60535 9.95969 7.30646L12.9416 2.99643Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M12.9416 2.99643C13.08 2.79636 12.9568 2.5 12.7352 2.5H3.26475C3.04317 2.5 2.91999 2.79636 3.0584 2.99643L6.04033 7.30646C6.24713 7.60535 6.35981 7.97674 6.35981 8.3596C6.35981 9.18422 6.35981 11.4639 6.35981 12.891C6.35981 13.2285 6.59643 13.5 6.88831 13.5H9.11168C9.40357 13.5 9.64019 13.2285 9.64019 12.891C9.64019 11.4639 9.64019 9.18422 9.64019 8.3596C9.64019 7.97674 9.75289 7.60535 9.95969 7.30646L12.9416 2.99643Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/flame.svg 🔗

@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-flame-icon lucide-flame"><path d="M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M5.5 9.868c.474 0 .928-.18 1.263-.5.335-.321.523-.756.523-1.21 0-.944-.357-1.369-.715-2.053C5.806 4.64 6.411 3.331 8 2c.357 1.71 1.429 3.353 2.857 4.447C12.286 7.542 13 8.842 13 10.21c0 .63-.13 1.252-.38 1.833a4.781 4.781 0 0 1-1.084 1.554 5.021 5.021 0 0 1-1.623 1.038 5.191 5.191 0 0 1-3.826 0 5.02 5.02 0 0 1-1.623-1.038 4.78 4.78 0 0 1-1.083-1.554A4.615 4.615 0 0 1 3 10.211c0-.79.31-1.57.714-2.053 0 .454.188.889.523 1.21.335.32.79.5 1.263.5Z"/></svg>

assets/icons/folder.svg 🔗

@@ -1,3 +1,3 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M13.3333 13.3333C13.6869 13.3333 14.026 13.1929 14.2761 12.9428C14.5261 12.6928 14.6666 12.3536 14.6666 12V5.33333C14.6666 4.97971 14.5261 4.64057 14.2761 4.39052C14.026 4.14048 13.6869 4 13.3333 4H8.06659C7.84359 4.00219 7.62362 3.94841 7.42679 3.84359C7.22996 3.73877 7.06256 3.58625 6.93992 3.4L6.39992 2.6C6.27851 2.41565 6.11324 2.26432 5.91892 2.1596C5.7246 2.05488 5.50732 2.00004 5.28659 2H2.66659C2.31296 2 1.97382 2.14048 1.72378 2.39052C1.47373 2.64057 1.33325 2.97971 1.33325 3.33333V12C1.33325 12.3536 1.47373 12.6928 1.72378 12.9428C1.97382 13.1929 2.31296 13.3333 2.66659 13.3333H13.3333Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M12.8 13C13.1183 13 13.4235 12.8761 13.6486 12.6554C13.8735 12.4349 14 12.1356 14 11.8236V5.94118C14 5.62916 13.8735 5.32992 13.6486 5.10929C13.4235 4.88866 13.1183 4.76471 12.8 4.76471H8.06C7.8593 4.76664 7.66133 4.71919 7.48418 4.6267C7.30703 4.53421 7.15637 4.39964 7.046 4.2353L6.56 3.52941C6.45073 3.36675 6.30199 3.23322 6.1271 3.14082C5.95221 3.04842 5.75666 3.00004 5.558 3H3.2C2.88174 3 2.57651 3.12395 2.35148 3.34458C2.12643 3.56521 2 3.86445 2 4.17647V11.8236C2 12.1356 2.12643 12.4349 2.35148 12.6554C2.57651 12.8761 2.88174 13 3.2 13H12.8Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/folder_open.svg 🔗

@@ -1,4 +1,4 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M4.42782 7.2487C4.43495 6.97194 4.65009 6.75 4.91441 6.75H13.5293C13.7935 6.75 14.007 6.97194 13.9998 7.2487C13.9628 8.6885 13.7533 12.75 12.5721 12.75H3.375C4.55631 12.75 4.3907 8.6885 4.42782 7.2487Z" fill="black" stroke="black" stroke-width="1.5" stroke-linejoin="round"/>
-<path d="M5.19598 12.625H3.66515C3.42618 12.625 3.22289 12.4453 3.18626 12.2017L1.94333 3.93602C1.89776 3.63295 2.12496 3.35938 2.42223 3.35938H5.78585C6.11241 3.35938 6.41702 3.52903 6.59618 3.81071L6.94517 4.35938H9.92811C10.4007 4.35938 10.8044 4.71102 10.8836 5.1917L11.1251 6.65624" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M4.42782 7.2487C4.43495 6.97194 4.65009 6.75 4.91441 6.75H13.5293C13.7935 6.75 14.007 6.97194 13.9998 7.2487C13.9628 8.6885 13.7533 12.75 12.5721 12.75H3.375C4.55631 12.75 4.3907 8.6885 4.42782 7.2487Z" fill="black" stroke="black" stroke-width="1.2" stroke-linejoin="round"/>
+<path d="M5.19598 12.625H3.66515C3.42618 12.625 3.22289 12.4453 3.18626 12.2017L1.94333 3.93602C1.89776 3.63295 2.12496 3.35938 2.42223 3.35938H5.78585C6.11241 3.35938 6.41702 3.52903 6.59618 3.81071L6.94517 4.35938H9.92811C10.4007 4.35938 10.8044 4.71102 10.8836 5.1917L11.1251 6.65624" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/folder_search.svg 🔗

@@ -0,0 +1,5 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M14.0001 13.9999L12.7334 12.7333" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M11.3333 13.3334C12.4378 13.3334 13.3333 12.4379 13.3333 11.3334C13.3333 10.2288 12.4378 9.33337 11.3333 9.33337C10.2287 9.33337 9.33325 10.2288 9.33325 11.3334C9.33325 12.4379 10.2287 13.3334 11.3333 13.3334Z" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M6 13H3.2C2.88174 13 2.57651 12.8761 2.35148 12.6554C2.12643 12.4349 2 12.1356 2 11.8236V4.17647C2 3.86445 2.12643 3.56521 2.35148 3.34458C2.57651 3.12395 2.88174 3 3.2 3H5.558C5.75666 3.00004 5.95221 3.04842 6.1271 3.14082C6.30199 3.23322 6.45073 3.36675 6.56 3.52941L7.046 4.2353C7.15637 4.39964 7.30703 4.53421 7.48418 4.6267C7.66133 4.71919 7.8593 4.76664 8.06 4.76471H12.8C13.1183 4.76471 13.4235 4.88866 13.6486 5.10929C13.8735 5.32992 14 5.62916 14 5.94118V7" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

assets/icons/folder_x.svg 🔗

@@ -1,5 +0,0 @@
-<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M7.70312 4L7.26046 2.97339C7.10239 2.60679 6.74141 2.36933 6.34219 2.36933H2.5C2.22386 2.36933 2 2.59319 2 2.86933V4.375V8" stroke="#11181C" stroke-width="1.25" stroke-linecap="round"/>

assets/icons/font.svg 🔗

@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-type"><polyline points="4 7 4 4 20 4 20 7"/><line x1="9" x2="15" y1="20" y2="20"/><line x1="12" x2="12" y1="4" y2="20"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M3 5V3h10v2M6 13h4M8 3v10"/></svg>

assets/icons/font_size.svg 🔗

@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-a-large-small"><path d="M21 14h-5"/><path d="M16 16v-3.5a2.5 2.5 0 0 1 5 0V16"/><path d="M4.5 13h6"/><path d="m3 16 4.5-9 4.5 9"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M14 9.333h-3.333M10.667 10.667V8.333a1.667 1.667 0 1 1 3.333 0v2.334"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.333" d="M3 8.667h4"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="m2 10.667 3-6 3 6"/></svg>

assets/icons/font_weight.svg 🔗

@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-bold"><path d="M6 12h9a4 4 0 0 1 0 8H7a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h7a4 4 0 0 1 0 8"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M4.27 8h5.626a2.5 2.5 0 1 1 0 5h-5a.625.625 0 0 1-.625-.625v-8.75A.625.625 0 0 1 4.896 3H9.27a2.5 2.5 0 1 1 0 5"/></svg>

assets/icons/forward_arrow.svg 🔗

@@ -1 +1,4 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-forward-icon lucide-forward"><polyline points="15 17 20 12 15 7"/><path d="M4 18v-2a4 4 0 0 1 4-4h12"/></svg>
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M10 11.3334L13.3333 8.00002L10 4.66669" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M2.66675 12V10.6667C2.66675 9.95942 2.9477 9.28115 3.4478 8.78105C3.94789 8.28095 4.62617 8 5.33341 8H13.3334" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

assets/icons/function.svg 🔗

@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-square-function-icon lucide-square-function"><rect width="18" height="18" x="3" y="3" rx="2" ry="2"/><path d="M9 17c2 0 2.8-1 2.8-2.8V10c0-2 1-3.3 3.2-3"/><path d="M9 11.2h5.7"/></svg>

assets/icons/generic_maximize.svg 🔗

@@ -1,3 +1,3 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M11.5 4.5H4.5V11.5H11.5V4.5Z" stroke="#FBF1C7"/>
+<path d="M11.5 4.5H4.5V11.5H11.5V4.5Z" stroke="black"/>
 </svg>

assets/icons/generic_restore.svg 🔗

@@ -1,4 +1,4 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M9.5 6.5H3.5V12.5H9.5V6.5Z" stroke="#FBF1C7"/>
-<path d="M10 8.5L12.5 8.5L12.5 3.5L7.5 3.5L7.5 6" stroke="#FBF1C7"/>
+<path d="M9.5 6.5H3.5V12.5H9.5V6.5Z" stroke="black"/>
+<path d="M10 8.5L12.5 8.5L12.5 3.5L7.5 3.5L7.5 6" stroke="black"/>
 </svg>

assets/icons/git_branch.svg 🔗

@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-git-branch"><line x1="6" x2="6" y1="3" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M5 3v7M11.5 6a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3ZM4.5 13a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3ZM11 6a5 5 0 0 1-5 5"/></svg>

assets/icons/git_branch_alt.svg 🔗

@@ -0,0 +1,7 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M4.5 14C5.32843 14 6 13.3284 6 12.5C6 11.6716 5.32843 11 4.5 11C3.67157 11 3 11.6716 3 12.5C3 13.3284 3.67157 14 4.5 14Z" stroke="black" stroke-width="1.2"/>
+<path d="M4.5 11V5.5" stroke="black" stroke-width="1.2"/>
+<path d="M4.5 10C4.5 10 4.875 8 6.5 8C7.29195 8 9.00787 8 9.87553 8C10.773 8 11.5 7.32843 11.5 6.5V5.5" stroke="black" stroke-width="1.2"/>
+<path d="M4.5 6C5.32843 6 6 5.32843 6 4.5C6 3.67157 5.32843 3 4.5 3C3.67157 3 3 3.67157 3 4.5C3 5.32843 3.67157 6 4.5 6Z" stroke="black" stroke-width="1.2"/>
+<path d="M11.5 6C12.3284 6 13 5.32843 13 4.5C13 3.67157 12.3284 3 11.5 3C10.6716 3 10 3.67157 10 4.5C10 5.32843 10.6716 6 11.5 6Z" stroke="black" stroke-width="1.2"/>
+</svg>

assets/icons/git_branch_small.svg 🔗

@@ -1,7 +0,0 @@
-<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<circle cx="5" cy="12" r="1.25" stroke="black" stroke-width="1.5"/>
-<path d="M5 11V5" stroke="black" stroke-width="1.5"/>
-<path d="M5 10C5 10 5.5 8 7 8C7.73103 8 8.69957 8 9.50049 8C10.3289 8 11 7.32843 11 6.5V5" stroke="black" stroke-width="1.5"/>
-<circle cx="5" cy="4" r="1.25" stroke="black" stroke-width="1.5"/>
-<circle cx="11" cy="4" r="1.25" stroke="black" stroke-width="1.5"/>
-</svg>

assets/icons/github.svg 🔗

@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-github"><path d="M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4"/><path d="M9 18c-4.51 2-5-2-7-2"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M9.849 14.288v-2.515a3.018 3.018 0 0 0-.629-2.201c1.887 0 3.773-1.258 3.773-3.459.05-.786-.17-1.559-.629-2.2a4.65 4.65 0 0 0 0-2.201s-.629 0-1.886.943a13.533 13.533 0 0 0-5.03 0c-1.259-.943-1.887-.943-1.887-.943a4.35 4.35 0 0 0 0 2.2 3.398 3.398 0 0 0-.63 2.201c0 2.201 1.887 3.459 3.774 3.459a2.965 2.965 0 0 0-.63 2.2v2.516"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M6.076 11.773c-2.836 1.258-3.144-1.258-4.402-1.258"/></svg>

assets/icons/globe.svg 🔗

@@ -1,12 +0,0 @@
-<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<g clip-path="url(#clip0_2226_61)">
-<path d="M7.99992 14.6667C11.6818 14.6667 14.6666 11.6819 14.6666 8C14.6666 4.3181 11.6818 1.33333 7.99992 1.33333C4.31802 1.33333 1.33325 4.3181 1.33325 8C1.33325 11.6819 4.31802 14.6667 7.99992 14.6667Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M7.99992 1.33333C6.28807 3.13076 5.33325 5.51782 5.33325 8C5.33325 10.4822 6.28807 12.8692 7.99992 14.6667C9.71176 12.8692 10.6666 10.4822 10.6666 8C10.6666 5.51782 9.71176 3.13076 7.99992 1.33333Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M1.33325 8H14.6666" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-</g>
-<defs>
-<clipPath id="clip0_2226_61">
-<rect width="16" height="16" fill="white"/>
-</clipPath>
-</defs>
-</svg>

assets/icons/hammer.svg 🔗

@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-hammer-icon lucide-hammer"><path d="m15 12-8.373 8.373a1 1 0 1 1-3-3L12 9"/><path d="m18 15 4-4"/><path d="m21.5 11.5-1.914-1.914A2 2 0 0 1 19 8.172V7l-2.26-2.26a6 6 0 0 0-4.202-1.756L9 2.96l.92.82A6.18 6.18 0 0 1 12 8.4V10l2 2h1.172a2 2 0 0 1 1.414.586L18.5 14.5"/></svg>

assets/icons/hash.svg 🔗

@@ -1,6 +1 @@
-<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
-<line x1="10.2795" y1="2.63847" x2="7.74786" y2="11.0142" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
-<line x1="6.26625" y1="2.99597" x2="3.73461" y2="11.3717" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
-<line x1="3.15979" y1="5.3799" x2="11.9098" y2="5.3799" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
-<line x1="2.09833" y1="8.62407" x2="10.8483" y2="8.62407" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
-</svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-width="1.2" d="m11.748 3.015-2.893 9.573M7.161 3.424l-2.893 9.572M3.611 6.148h10M2.398 9.856h10"/></svg>

assets/icons/history_rerun.svg 🔗

@@ -1,5 +1,5 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M2 8C2 9.18669 2.35189 10.3467 3.01118 11.3334C3.67047 12.3201 4.60754 13.0892 5.7039 13.5433C6.80026 13.9974 8.00666 14.1162 9.17054 13.8847C10.3344 13.6532 11.4035 13.0818 12.2426 12.2426C13.0818 11.4035 13.6532 10.3344 13.8847 9.17054C14.1162 8.00666 13.9974 6.80026 13.5433 5.7039C13.0892 4.60754 12.3201 3.67047 11.3334 3.01118C10.3467 2.35189 9.18669 2 8 2C6.32263 2.00631 4.71265 2.66082 3.50667 3.82667L2 5.33333" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M2 2V5.33333H5.33333" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M8 5V8.5L10 9.5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M2 8C2 9.18669 2.35189 10.3467 3.01118 11.3334C3.67047 12.3201 4.60754 13.0892 5.7039 13.5433C6.80026 13.9974 8.00666 14.1162 9.17054 13.8847C10.3344 13.6532 11.4035 13.0818 12.2426 12.2426C13.0818 11.4035 13.6532 10.3344 13.8847 9.17054C14.1162 8.00666 13.9974 6.80026 13.5433 5.7039C13.0892 4.60754 12.3201 3.67047 11.3334 3.01118C10.3467 2.35189 9.18669 2 8 2C6.32263 2.00631 4.71265 2.66082 3.50667 3.82667L2 5.33333" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M2 2V5.33333H5.33333" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M8 5V8.5L10 9.5" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/image.svg 🔗

@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-image-icon lucide-image"><rect width="18" height="18" x="3" y="3" rx="2" ry="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M11.889 3H4.11C3.497 3 3 3.497 3 4.111v7.778C3 12.503 3.497 13 4.111 13h7.778c.614 0 1.111-.498 1.111-1.111V4.11C13 3.497 12.502 3 11.889 3Z"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M6.333 7.444a1.111 1.111 0 1 0 0-2.222 1.111 1.111 0 0 0 0 2.222ZM13 9.667l-1.714-1.715a1.11 1.11 0 0 0-1.571 0L4.667 13"/></svg>

assets/icons/info.svg 🔗

@@ -1,5 +1,5 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M8 14C11.3137 14 14 11.3137 14 8C14 4.68629 11.3137 2 8 2C4.68629 2 2 4.68629 2 8C2 11.3137 4.68629 14 8 14Z" stroke="black" stroke-width="1.5"/>
-<path d="M7 11H8M8 11H9M8 11V8.1C8 8.04477 7.95523 8 7.9 8H7" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
+<path d="M8 14C11.3137 14 14 11.3137 14 8C14 4.68629 11.3137 2 8 2C4.68629 2 2 4.68629 2 8C2 11.3137 4.68629 14 8 14Z" stroke="black" stroke-width="1.2"/>
+<path d="M7 11H8M8 11H9M8 11V8.1C8 8.04477 7.95523 8 7.9 8H7" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
 <path d="M8 6.5C8.55228 6.5 9 6.05228 9 5.5C9 4.94772 8.55228 4.5 8 4.5C7.44772 4.5 7 4.94772 7 5.5C7 6.05228 7.44772 6.5 8 6.5Z" fill="black"/>
 </svg>

assets/icons/inlay_hint.svg 🔗

@@ -1,5 +0,0 @@
-<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
-<circle cx="3" cy="9" r="1" fill="black"/>
-<circle cx="3" cy="5" r="1" fill="black"/>
-<path d="M7 3H10M13 3H10M10 3C10 3 10 11 10 11.5" stroke="black" stroke-width="1.25"/>
-</svg>

assets/icons/json.svg 🔗

@@ -0,0 +1,4 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M5.78125 3C3.90625 3 3.90625 4.5 3.90625 5.5C3.90625 6.5 3.40625 7.50106 2.40625 8C3.40625 8.50106 3.90625 9.5 3.90625 10.5C3.90625 11.5 3.90625 13 5.78125 13" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M10.2422 3C12.1172 3 12.1172 4.5 12.1172 5.5C12.1172 6.5 12.6172 7.50106 13.6172 8C12.6172 8.50106 12.1172 9.5 12.1172 10.5C12.1172 11.5 12.1172 13 10.2422 13" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

assets/icons/keyboard.svg 🔗

@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-keyboard"><path d="M10 8h.01"/><path d="M12 12h.01"/><path d="M14 8h.01"/><path d="M16 12h.01"/><path d="M18 8h.01"/><path d="M6 8h.01"/><path d="M7 16h10"/><path d="M8 12h.01"/><rect width="20" height="16" x="2" y="4" rx="2"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M6.75 5.5h.007M8 8h.007M9.25 5.5h.007M10.5 8h.007M11.75 5.5h.007M4.25 5.5h.007M4.875 10.5h6.25M5.5 8h.007M13 3H3c-.69 0-1.25.56-1.25 1.25v7.5c0 .69.56 1.25 1.25 1.25h10c.69 0 1.25-.56 1.25-1.25v-7.5C14.25 3.56 13.69 3 13 3Z"/></svg>

assets/icons/knockouts/x_fg.svg 🔗

@@ -1,3 +1,3 @@
 <svg width="11" height="11" viewBox="0 0 11 11" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M3 3L5.5 5.5M8 8L5.5 5.5M5.5 5.5L3 8M5.5 5.5L8 3" stroke="black" stroke-width="1.5"/>
+<path d="M3 3L5.5 5.5M8 8L5.5 5.5M5.5 5.5L3 8M5.5 5.5L8 3" stroke="black" stroke-width="1.2"/>
 </svg>

assets/icons/layout.svg 🔗

@@ -1,5 +0,0 @@
-<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M20 14H4C3.44772 14 3 14.4477 3 15V20C3 20.5523 3.44772 21 4 21H20C20.5523 21 21 20.5523 21 20V15C21 14.4477 20.5523 14 20 14Z" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M11 3H4C3.44772 3 3 3.44772 3 4V9C3 9.55228 3.44772 10 4 10H11C11.5523 10 12 9.55228 12 9V4C12 3.44772 11.5523 3 11 3Z" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M20 3H17C16.4477 3 16 3.44772 16 4V9C16 9.55228 16.4477 10 17 10H20C20.5523 10 21 9.55228 21 9V4C21 3.44772 20.5523 3 20 3Z" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
-</svg>

assets/icons/library.svg 🔗

@@ -1 +1,6 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-library"><path d="m16 6 4 14"/><path d="M12 6v14"/><path d="M8 8v12"/><path d="M4 4v16"/></svg>
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M10.6667 4L13.3334 13.3333" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M8 4V13.3333" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M5.33325 5.33331V13.3333" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M2.66675 2.66669V13.3334" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

assets/icons/light_bulb.svg 🔗

@@ -1,3 +0,0 @@
-<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M10.1331 11.3776C10.2754 10.6665 10.1331 9.78593 11.1998 8.53327C11.82 7.80489 12.2664 6.96894 12.2664 6.04456C12.2664 4.91305 11.8169 3.82788 11.0168 3.02778C10.2167 2.22769 9.13152 1.7782 8.00001 1.7782C6.8685 1.7782 5.78334 2.22769 4.98324 3.02778C4.18314 3.82788 3.73364 4.91305 3.73364 6.04456C3.73364 6.75562 3.87586 7.6089 4.80024 8.53327C5.86683 9.80679 5.72462 10.6665 5.86683 11.3776M10.1331 11.3776V12.8821C10.1331 13.622 9.53341 14.2218 8.79353 14.2218H7.2065C6.46662 14.2218 5.86683 13.622 5.86683 12.8821V11.3776M10.1331 11.3776H5.86683" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
-</svg>

assets/icons/line_height.svg 🔗

@@ -1,6 +1 @@
-<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M4 13.6667H12" stroke="#B3B3B3" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M4 2.33333H12" stroke="#B3B3B3" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M5 11L8 5L11 11" stroke="#B3B3B3" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M6 9H10" stroke="#B3B3B3" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
-</svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M4 13.667h8M4 2.333h8M5 11l3-6 3 6M6 9h4"/></svg>

assets/icons/link.svg 🔗

@@ -1,3 +0,0 @@
-<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">

assets/icons/list_collapse.svg 🔗

@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-list-collapse-icon lucide-list-collapse"><path d="m3 10 2.5-2.5L3 5"/><path d="m3 19 2.5-2.5L3 14"/><path d="M10 6h11"/><path d="M10 12h11"/><path d="M10 18h11"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M2.857 6.857 4.286 5.43 2.857 4M2.857 12l1.429-1.429-1.429-1.428M6.857 4.571h6.286M6.857 8h6.286M6.857 11.428h6.286"/></svg>

assets/icons/list_todo.svg 🔗

@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-list-todo-icon lucide-list-todo"><rect x="3" y="5" width="6" height="6" rx="1"/><path d="m3 17 2 2 4-4"/><path d="M13 6h8"/><path d="M13 12h8"/><path d="M13 18h8"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M5.333 3.333H2.667A.667.667 0 0 0 2 4v2.667c0 .368.298.666.667.666h2.666A.667.667 0 0 0 6 6.667V4a.667.667 0 0 0-.667-.667ZM2 11.333l1.333 1.334L6 10M8.667 4H14M8.667 8H14M8.667 12H14"/></svg>

assets/icons/list_tree.svg 🔗

@@ -1,7 +1,7 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M13.5 8H9.5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M13.5 4L6.5 4" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M13.5 12H9.5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M3 3.5V6.33333C3 7.25 3.72 8 4.6 8H7" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M3 6V10.5C3 11.325 3.72 12 4.6 12H7" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M13.5 8H9.5" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M13.5 4L6.5 4" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M13.5 12H9.5" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M3 3.5V6.33333C3 7.25 3.72 8 4.6 8H7" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M3 6V10.5C3 11.325 3.72 12 4.6 12H7" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/list_x.svg 🔗

@@ -1,7 +1,7 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M8.33333 8H3" stroke="#FBF1C7" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M11.6667 4H3" stroke="#FBF1C7" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M11.6667 12H3" stroke="#FBF1C7" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M13.6667 6.66663L11 9.33329" stroke="#FBF1C7" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M11 6.66663L13.6667 9.33329" stroke="#FBF1C7" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M8.33333 8H3" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M11.6667 4H3" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M11.6667 12H3" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M13.6667 6.66663L11 9.33329" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M11 6.66663L13.6667 9.33329" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/load_circle.svg 🔗

@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-loader-circle-icon lucide-loader-circle"><path d="M21 12a9 9 0 1 1-6.219-8.56"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M13 8a5 5 0 1 1-3.455-4.755"/></svg>

assets/icons/location_edit.svg 🔗

@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-location-edit-icon lucide-location-edit"><path d="M17.97 9.304A8 8 0 0 0 2 10c0 4.69 4.887 9.562 7.022 11.468"/><path d="M21.378 16.626a1 1 0 0 0-3.004-3.004l-4.01 4.012a2 2 0 0 0-.506.854l-.837 2.87a.5.5 0 0 0 .62.62l2.87-.837a2 2 0 0 0 .854-.506z"/><circle cx="10" cy="10" r="3"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M12 6.502a4.904 4.904 0 0 0-1.686-3.28 5.059 5.059 0 0 0-3.522-1.22A5.045 5.045 0 0 0 3.39 3.52 4.889 4.889 0 0 0 2 6.93c0 2.89 3.06 5.893 4.397 7.068M13.655 11.013a1.18 1.18 0 0 0-1.67-1.67l-2.227 2.23a1.112 1.112 0 0 0-.282.475l-.465 1.594a.278.278 0 0 0 .345.345l1.594-.465a1.11 1.11 0 0 0 .475-.281l2.23-2.228Z"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M7 8.998a2 2 0 1 0 0-4 2 2 0 0 0 0 4Z"/></svg>

assets/icons/lock_outlined.svg 🔗

@@ -1,6 +1,6 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M5 5C5 3.89543 5.89543 3 7 3H9C10.1046 3 11 3.89543 11 5V6H5V5Z" stroke="black" stroke-width="1.5"/>
-<path d="M8 9V11" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
+<path d="M5 5C5 3.89543 5.89543 3 7 3H9C10.1046 3 11 3.89543 11 5V6H5V5Z" stroke="black" stroke-width="1.2"/>
+<path d="M8 9V11" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
 <circle cx="8" cy="9" r="1" fill="black"/>
-<rect x="3.75" y="5.75" width="8.5" height="7.5" rx="1.25" stroke="black" stroke-width="1.5" stroke-linejoin="round"/>
+<rect x="3.75" y="5.75" width="8.5" height="7.5" rx="1.25" stroke="black" stroke-width="1.2" stroke-linejoin="round"/>
 </svg>

assets/icons/logo_96.svg 🔗

@@ -1,3 +0,0 @@
-<svg width="96" height="96" viewBox="0 0 96 96" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path fill-rule="evenodd" clip-rule="evenodd" d="M9 6C7.34315 6 6 7.34315 6 9V75H0V9C0 4.02944 4.02944 0 9 0H89.3787C93.3878 0 95.3955 4.84715 92.5607 7.68198L43.0551 57.1875H57V51H63V58.6875C63 61.1728 60.9853 63.1875 58.5 63.1875H37.0551L26.7426 73.5H73.5V36H79.5V73.5C79.5 76.8137 76.8137 79.5 73.5 79.5H20.7426L10.2426 90H87C88.6569 90 90 88.6569 90 87V21H96V87C96 91.9706 91.9706 96 87 96H6.62132C2.61224 96 0.604504 91.1529 3.43934 88.318L52.7574 39H39V45H33V37.5C33 35.0147 35.0147 33 37.5 33H58.7574L69.2574 22.5H22.5V60H16.5V22.5C16.5 19.1863 19.1863 16.5 22.5 16.5H75.2574L85.7574 6H9Z" fill="white"/>
-</svg>

assets/icons/lsp_debug.svg 🔗

@@ -1,12 +0,0 @@
-<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M6 3L7 4" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M9 4L10 3" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M6.002 6V5.51658C5.98992 5.32067 6.03266 5.12502 6.12762 4.94143C6.22259 4.75784 6.36781 4.59012 6.55453 4.44839C6.74125 4.30666 6.9656 4.19386 7.21403 4.1168C7.46246 4.03973 7.72983 4 8 4C8.27017 4 8.53754 4.03973 8.78597 4.1168C9.0344 4.19386 9.25875 4.30666 9.44547 4.44839C9.63219 4.59012 9.77741 4.75784 9.87238 4.94143C9.96734 5.12502 10.0101 5.32067 9.998 5.51658V6" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M8 13C6.35 13 5 11.5462 5 9.76923V8.15385C5 7.58261 5.21071 7.03477 5.58579 6.63085C5.96086 6.22692 6.46957 6 7 6H9C9.53043 6 10.0391 6.22692 10.4142 6.63085C10.7893 7.03477 11 7.58261 11 8.15385V9.76923C11 11.5462 9.65 13 8 13Z" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M5 6.16663C3.90652 6.06663 3 5.21663 3 4.16663" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M5 9H3" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M3 13C3 11.95 3.89474 11.05 5 11" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M13 4C13 5.05 12.0857 5.9 11 6" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M13 9H11" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M11 11C12.1053 11.05 13 11.95 13 13" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
-</svg>

assets/icons/lsp_restart.svg 🔗

@@ -1,4 +0,0 @@
-<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M3.84265 10.7778C4.39206 11.6001 5.17295 12.241 6.08658 12.6194C7.00021 12.9978 8.00555 13.0969 8.97545 12.9039C9.94535 12.711 10.8363 12.2348 11.5355 11.5355C12.2348 10.8363 12.711 9.94535 12.9039 8.97545C13.0969 8.00555 12.9978 7.00021 12.6194 6.08658C12.241 5.17295 11.6001 4.39206 10.7778 3.84265C9.9556 3.29324 8.9889 3 8 3C6.60219 3.00526 5.26054 3.55068 4.25556 4.52222L3 5.77778" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M3 3V6H6" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-</svg>

assets/icons/lsp_stop.svg 🔗

@@ -1,4 +0,0 @@
-<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M8 13C10.7614 13 13 10.7614 13 8C13 5.23858 10.7614 3 8 3C5.23858 3 3 5.23858 3 8C3 10.7614 5.23858 13 8 13Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M5 5L11 11" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-</svg>

assets/icons/magnifying_glass.svg 🔗

@@ -1,3 +1,4 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M13 13L10.4138 10.4138M3 7.31034C3 4.92981 4.92981 3 7.31034 3C9.6909 3 11.6207 4.92981 11.6207 7.31034C11.6207 9.6909 9.6909 11.6207 7.31034 11.6207C4.92981 11.6207 3 9.6909 3 7.31034Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M13 13L10.4138 10.4138ZM3 7.31034C3 4.92981 4.92981 3 7.31034 3C9.6909 3 11.6207 4.92981 11.6207 7.31034C11.6207 9.6909 9.6909 11.6207 7.31034 11.6207C4.92981 11.6207 3 9.6909 3 7.31034Z" fill="black" fill-opacity="0.15"/>
+<path d="M13 13L10.4138 10.4138M3 7.31034C3 4.92981 4.92981 3 7.31034 3C9.6909 3 11.6207 4.92981 11.6207 7.31034C11.6207 9.6909 9.6909 11.6207 7.31034 11.6207C4.92981 11.6207 3 9.6909 3 7.31034Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/mail_open.svg 🔗

@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-mail-open"><path d="M21.2 8.4c.5.38.8.97.8 1.6v10a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V10a2 2 0 0 1 .8-1.6l8-6a2 2 0 0 1 2.4 0l8 6Z"/><path d="m22 10-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 10"/></svg>

assets/icons/maximize.svg 🔗

@@ -1 +1,6 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize-2"><polyline points="15 3 21 3 21 9"/><polyline points="9 21 3 21 3 15"/><line x1="21" x2="14" y1="3" y2="10"/><line x1="3" x2="10" y1="21" y2="14"/></svg>
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M10 3H13V6" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M6 13H3V10" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M13 3L9.5 6.5" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M3 13L6.5 9.5" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

assets/icons/menu.svg 🔗

@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-menu"><line x1="4" x2="20" y1="12" y2="12"/><line x1="4" x2="20" y1="6" y2="6"/><line x1="4" x2="20" y1="18" y2="18"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M2.667 8h10.666M2.667 4h10.666M2.667 12h10.666"/></svg>

assets/icons/menu_alt.svg 🔗

@@ -1,5 +1,3 @@
-<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M4 12H16" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M4 6H20" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M4 18H12" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M2.66699 8H10.667M2.66699 4H13.333M2.66699 12H7.99999" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/menu_alt_temp.svg 🔗

@@ -0,0 +1,3 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M13.333 10H8M13.333 6H2.66701" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

assets/icons/mic.svg 🔗

@@ -1,5 +1,5 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M8 12.2028V14.3042" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M12.2027 6.94928V8.11672C12.2027 9.20041 11.7599 10.2397 10.9717 11.006C10.1836 11.7723 9.11457 12.2028 7.99992 12.2028C6.88527 12.2028 5.81627 11.7723 5.02809 11.006C4.23991 10.2397 3.79712 9.20041 3.79712 8.11672V6.94928" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M10.1015 3.63555C10.1015 2.56426 9.16065 1.6958 8.00008 1.6958C6.83951 1.6958 5.89868 2.56426 5.89868 3.63555V8.16165C5.89868 9.23294 6.83951 10.1014 8.00008 10.1014C9.16065 10.1014 10.1015 9.23294 10.1015 8.16165V3.63555Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M8 12.2028V14.3042" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M12.2027 6.94928V8.11672C12.2027 9.20041 11.7599 10.2397 10.9717 11.006C10.1836 11.7723 9.11457 12.2028 7.99992 12.2028C6.88527 12.2028 5.81627 11.7723 5.02809 11.006C4.23991 10.2397 3.79712 9.20041 3.79712 8.11672V6.94928" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M10.1015 3.63555C10.1015 2.56426 9.16065 1.6958 8.00008 1.6958C6.83951 1.6958 5.89868 2.56426 5.89868 3.63555V8.16165C5.89868 9.23294 6.83951 10.1014 8.00008 10.1014C9.16065 10.1014 10.1015 9.23294 10.1015 8.16165V3.63555Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/mic_mute.svg 🔗

@@ -1,8 +1,8 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M3 3L13 13" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M12 9C12 8.74858 12 8.49375 12 8.23839V7" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M4.00043 7V8.09869C3.98856 8.86731 4.22157 9.62164 4.66938 10.2643C5.11718 10.907 5.75924 11.4085 6.51267 11.7042C7.2661 11.9999 8.09632 12.0761 8.89619 11.923C9.47851 11.8115 10.0253 11.5823 10.5 11.2539" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M10 6V3.62904C9.99714 3.26103 9.8347 2.90448 9.53885 2.6168C9.24299 2.32913 8.83093 2.12707 8.36903 2.04316C7.90713 1.95926 7.42226 1.9984 6.99252 2.15427C6.56278 2.31015 6.21317 2.57369 6 2.90245" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M6 6V8.00088C6.00031 8.39636 6.10356 8.78287 6.29674 9.11159C6.48991 9.44031 6.76433 9.69649 7.08534 9.84779C7.40634 9.99909 7.75954 10.0387 8.10032 9.96165C8.4411 9.88459 8.75417 9.69431 9 9.41483" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M8 12V14" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M3 3L13 13" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M12 9C12 8.74858 12 8.49375 12 8.23839V7" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M4.00043 7V8.09869C3.98856 8.86731 4.22157 9.62164 4.66938 10.2643C5.11718 10.907 5.75924 11.4085 6.51267 11.7042C7.2661 11.9999 8.09632 12.0761 8.89619 11.923C9.47851 11.8115 10.0253 11.5823 10.5 11.2539" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M10 6V3.62904C9.99714 3.26103 9.8347 2.90448 9.53885 2.6168C9.24299 2.32913 8.83093 2.12707 8.36903 2.04316C7.90713 1.95926 7.42226 1.9984 6.99252 2.15427C6.56278 2.31015 6.21317 2.57369 6 2.90245" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M6 6V8.00088C6.00031 8.39636 6.10356 8.78287 6.29674 9.11159C6.48991 9.44031 6.76433 9.69649 7.08534 9.84779C7.40634 9.99909 7.75954 10.0387 8.10032 9.96165C8.4411 9.88459 8.75417 9.69431 9 9.41483" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M8 12V14" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/minimize.svg 🔗

@@ -1 +1,6 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-minimize-2"><polyline points="4 14 10 14 10 20"/><polyline points="20 10 14 10 14 4"/><line x1="14" x2="21" y1="10" y2="3"/><line x1="3" x2="10" y1="21" y2="14"/></svg>
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M3.5 9.5H6.5V12.5" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M12.5 6.5H9.5V3.5" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M9.5 6.5L13 3" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M3 13L6.5 9.5" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

assets/icons/notepad.svg 🔗

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><g stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" clip-path="url(#a)"><path d="M6 8h4M6 10.5h4M3 3h10v9.565c0 .38-.158.746-.44 1.015-.28.269-.662.42-1.06.42h-7c-.398 0-.78-.151-1.06-.42A1.404 1.404 0 0 1 3 12.565V3ZM6.5 1v4M9.5 1v4"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

assets/icons/option.svg 🔗

@@ -1,3 +1,4 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path fill-rule="evenodd" clip-rule="evenodd" d="M5.35606 1.005H1.62545C1.28002 1.005 1 1.28502 1 1.63044C1 1.97587 1.28002 2.25589 1.62545 2.25589L5.35606 2.25589C5.62311 2.25589 5.8607 2.42545 5.94752 2.67799L9.75029 13.7387C10.0108 14.4963 10.7235 15.005 11.5247 15.005H14.3746C14.72 15.005 15 14.725 15 14.3796C15 14.0341 14.72 13.7541 14.3746 13.7541H11.5247C11.2576 13.7541 11.02 13.5845 10.9332 13.332L7.13046 2.27128C6.86998 1.51366 6.15721 1.005 5.35606 1.005ZM14.3745 1.005H9.75125C9.40582 1.005 9.1258 1.28502 9.1258 1.63044C9.1258 1.97587 9.40582 2.25589 9.75125 2.25589L14.3745 2.25589C14.72 2.25589 15 1.97587 15 1.63044C15 1.28502 14.72 1.005 14.3745 1.005Z" fill="black"/>
+<path d="M3 3H6.33333L9.66667 13H13" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M9.11108 3H13" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/panel_left.svg 🔗

@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-panel-left"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M9 3v18"/></svg>

assets/icons/panel_right.svg 🔗

@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-panel-right"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M15 3v18"/></svg>

assets/icons/pencil.svg 🔗

@@ -1,3 +1,4 @@
-<svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="m12 6.668 2-2L11.332 2l-2 2M12 6.668l-6.668 6.664H2.668v-2.664L9.332 4M12 6.668 9.332 4" stroke="black" stroke-width="1" stroke-linejoin="round"/>
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M12.5871 5.40582C12.8514 5.14152 13 4.78304 13 4.40922C13 4.03541 12.8516 3.67688 12.5873 3.41252C12.323 3.14816 11.9645 2.99962 11.5907 2.99957C11.2169 2.99953 10.8584 3.14798 10.594 3.41227L3.92098 10.0869C3.80488 10.2027 3.71903 10.3452 3.67097 10.5019L3.01047 12.678C2.99754 12.7212 2.99657 12.7672 3.00764 12.8109C3.01872 12.8547 3.04143 12.8946 3.07337 12.9265C3.1053 12.9584 3.14528 12.981 3.18905 12.992C3.23282 13.003 3.27875 13.002 3.32197 12.989L5.49849 12.329C5.65508 12.2813 5.79758 12.196 5.91349 12.0805L12.5871 5.40582Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M9 5L11 7" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/person.svg 🔗

@@ -1,4 +1 @@
-<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M12.6666 14V12.6667C12.6666 11.9594 12.3856 11.2811 11.8855 10.781C11.3854 10.281 10.7072 10 9.99992 10H5.99992C5.29267 10 4.6144 10.281 4.1143 10.781C3.6142 11.2811 3.33325 11.9594 3.33325 12.6667V14" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M7.99992 7.33333C9.47268 7.33333 10.6666 6.13943 10.6666 4.66667C10.6666 3.19391 9.47268 2 7.99992 2C6.52716 2 5.33325 3.19391 5.33325 4.66667C5.33325 6.13943 6.52716 7.33333 7.99992 7.33333Z" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
-</svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M12.667 14v-1.333A2.667 2.667 0 0 0 10 10H6a2.667 2.667 0 0 0-2.667 2.667V14M8 7.333A2.667 2.667 0 1 0 8 2a2.667 2.667 0 0 0 0 5.333Z"/></svg>

assets/icons/person_circle.svg 🔗

@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-user"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="10" r="3"/><path d="M7 20.662V19a2 2 0 0 1 2-2h6a2 2 0 0 1 2 2v1.662"/></svg>

assets/icons/phone_incoming.svg 🔗

@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-phone-incoming"><polyline points="16 2 16 8 22 8"/><line x1="22" x2="16" y1="2" y2="8"/><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"/></svg>

assets/icons/pin.svg 🔗

@@ -1,4 +1,4 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M8 10V13" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>

assets/icons/play_filled.svg 🔗

@@ -1,3 +1,3 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M5 4L12 8L5 12V4Z" fill="black" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M5 4L12 8L5 12V4Z" fill="black" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/play_outlined.svg 🔗

@@ -1,3 +1,3 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M5 4L12 8L5 12V4Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M5 4L12 8L5 12V4Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/plus.svg 🔗

@@ -1,4 +1,4 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M3.33325 8H12.6666" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M8 3.33333V12.6667" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M3.33325 8H12.6666" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M8 3.33333V12.6667" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/pocket_knife.svg 🔗

@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-pocket-knife"><path d="M3 2v1c0 1 2 1 2 2S3 6 3 7s2 1 2 2-2 1-2 2 2 1 2 2"/><path d="M18 6h.01"/><path d="M6 18h.01"/><path d="M20.83 8.83a4 4 0 0 0-5.66-5.66l-12 12a4 4 0 1 0 5.66 5.66Z"/><path d="M18 11.66V22a4 4 0 0 0 4-4V6"/></svg>

assets/icons/power.svg 🔗

@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-power-icon lucide-power"><path d="M12 2v10"/><path d="M18.4 6.6a9 9 0 1 1-12.77.04"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M8 2v5M11.536 4a5.368 5.368 0 0 1 1.367 2.696 5.54 5.54 0 0 1-.28 3.042 5.223 5.223 0 0 1-1.836 2.367A4.82 4.82 0 0 1 8.016 13a4.817 4.817 0 0 1-2.777-.877 5.22 5.22 0 0 1-1.85-2.354 5.54 5.54 0 0 1-.298-3.041 5.371 5.371 0 0 1 1.35-2.705"/></svg>

assets/icons/public.svg 🔗

@@ -1,3 +1 @@
-<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path fill-rule="evenodd" clip-rule="evenodd" d="M3.74393 2.00204C3.41963 1.97524 3.13502 2.37572 3.10823 2.70001C3.08143 3.0243 3.32558 3.47321 3.64986 3.50001C7.99878 3.85934 11.1406 7.00122 11.5 11.3501C11.5267 11.6744 11.9756 12.0269 12.3 12C12.6243 11.9733 13.0247 11.5804 12.998 11.2561C12.5912 6.33295 8.66704 2.40882 3.74393 2.00204ZM2.9 6.00001C2.96411 5.68099 3.33084 5.29361 3.64986 5.35772C6.66377 5.96341 9.03654 8.33618 9.64223 11.3501C9.70634 11.6691 9.319 12.0359 8.99999 12.1C8.68097 12.1641 8.06411 11.819 7.99999 11.5C7.48788 8.95167 6.0483 7.51213 3.49999 7.00001C3.18097 6.9359 2.8359 6.31902 2.9 6.00001ZM2 9.20001C2.0641 8.88099 2.38635 8.65788 2.70537 8.722C4.50255 9.08317 5.91684 10.4975 6.27801 12.2946C6.34212 12.6137 6.13547 12.9242 5.81646 12.9883C5.49744 13.0525 4.86411 12.819 4.8 12.5C4.53239 11.1683 3.83158 10.4676 2.5 10.2C2.18098 10.1359 1.93588 9.51902 2 9.20001Z" fill="black"/>
-</svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M4 6.375A5.625 5.625 0 0 1 9.625 12M4 10a2 2 0 0 1 2 2M4 3a9 9 0 0 1 9 9"/></svg>

assets/icons/pull_request.svg 🔗

@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-git-pull-request-arrow"><circle cx="5" cy="6" r="3"/><path d="M5 9v12"/><circle cx="19" cy="18" r="3"/><path d="m15 9-3-3 3-3"/><path d="M12 6h5a2 2 0 0 1 2 2v7"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M4 6a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3ZM4 6v7M12 13a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3ZM10 6.5l-2-2 2-2"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M8.5 4.389h2.357c.303 0 .594.117.808.325.215.209.335.491.335.786V10"/></svg>

assets/icons/quote.svg 🔗

@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-text-quote"><path d="M17 6H3"/><path d="M21 12H8"/><path d="M21 18H8"/><path d="M3 12v6"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M11.333 4H2M14 8H5.333M12 12H5M2 8v4"/></svg>

assets/icons/reader.svg 🔗

@@ -0,0 +1,5 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M11.8889 2H4.11111C3.49746 2 3 2.59695 3 3.33333V12.6667C3 13.403 3.49746 14 4.11111 14H11.8889C12.5025 14 13 13.403 13 12.6667V3.33333C13 2.59695 12.5025 2 11.8889 2Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M9 6H6" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M10 10H6" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

assets/icons/refresh_title.svg 🔗

@@ -1,5 +1 @@
-<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M7 21V12M7 12H3M7 12H11" stroke="black" stroke-width="2" stroke-linecap="round"/>
-<path d="M21 19L16 19L16 14" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M7.99987 5.07027L7.49915 4.20467L7.99987 5.07027ZM6.04652 5.25026C5.63245 5.61573 5.59305 6.24766 5.95851 6.66173C6.32398 7.0758 6.95592 7.1152 7.36999 6.74974L6.04652 5.25026ZM11.9999 5C15.8659 5 18.9999 8.13401 18.9999 12H20.9999C20.9999 7.02944 16.9705 3 11.9999 3V5ZM18.9999 12C18.9999 14.2101 17.9768 16.1806 16.3744 17.4651L17.6254 19.0256C19.6809 17.3779 20.9999 14.8426 20.9999 12H18.9999ZM8.5006 5.93588C9.5292 5.34086 10.7232 5 11.9999 5V3C10.3623 3 8.82395 3.4383 7.49915 4.20467L8.5006 5.93588ZM7.36999 6.74974C7.71803 6.44255 8.09667 6.16954 8.5006 5.93588L7.49915 4.20467C6.9797 4.50515 6.49329 4.85593 6.04652 5.25026L7.36999 6.74974Z" fill="black"/>
-</svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-width="1.2" d="M4.667 14V8m0 0H2m2.667 0h2.666"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M14 12.667h-3.333V9.333"/><path fill="#000" d="M4.03 3.5a.667.667 0 0 0 .883 1l-.882-1ZM8 3.333A4.667 4.667 0 0 1 12.666 8H14a6 6 0 0 0-6-6v1.333ZM12.666 8a4.656 4.656 0 0 1-1.75 3.643l.834 1.04A5.99 5.99 0 0 0 14 8h-1.334ZM5.667 3.957A4.642 4.642 0 0 1 8 3.333V2a5.976 5.976 0 0 0-3 .803l.667 1.154Zm-.754.543c.232-.205.485-.387.754-.543l-.668-1.154c-.346.2-.67.434-.968.697l.882 1Z"/></svg>

assets/icons/regex.svg 🔗

@@ -1,4 +1,4 @@
-<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
-<circle cx="4" cy="11" r="1" fill="#787D87"/>
-<path d="M9 2.5V5M9 5V7.5M9 5H11.5M9 5H6.5M9 5L10.6667 3.33333M9 5L7.33333 6.6667M9 5L10.6667 6.6667M9 5L7.33333 3.33333" stroke="#787D87" stroke-width="1.25" stroke-linecap="round"/>
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M4.57132 13.7143C5.20251 13.7143 5.71418 13.2026 5.71418 12.5714C5.71418 11.9403 5.20251 11.4286 4.57132 11.4286C3.94014 11.4286 3.42847 11.9403 3.42847 12.5714C3.42847 13.2026 3.94014 13.7143 4.57132 13.7143Z" fill="black"/>
+<path d="M10.2856 2.85712V5.71426M10.2856 5.71426V8.5714M10.2856 5.71426H13.1428M10.2856 5.71426H7.42847M10.2856 5.71426L12.1904 3.80949M10.2856 5.71426L8.38084 7.61906M10.2856 5.71426L12.1904 7.61906M10.2856 5.71426L8.38084 3.80949" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
 </svg>

assets/icons/repl_neutral.svg 🔗

@@ -1,13 +1,6 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<g clip-path="url(#clip0_62_95)">
-<path d="M4.5 6C3.67157 6 3 5.32843 3 4.5C3 3.67157 3.67157 3 4.5 3C5.32843 3 6 3.67157 6 4.5C6 5.32843 5.32843 6 4.5 6Z" stroke="white" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M6.54433 13.4334C6.87775 13.5227 7.22046 13.3249 7.3098 12.9914C7.39914 12.658 7.20127 12.3153 6.86786 12.226L6.54433 13.4334ZM3.77426 6.86772L3.93603 6.26401L2.72862 5.94049L2.56686 6.54419L3.77426 6.86772ZM6.86786 12.226C4.53394 11.6006 3.14889 9.20163 3.77426 6.86772L2.56686 6.54419C1.76281 9.54494 3.54359 12.6293 6.54433 13.4334L6.86786 12.226Z" fill="white"/>
-<path d="M11.5 13C10.6716 13 10 12.3284 10 11.5C10 10.6716 10.6716 10 11.5 10C12.3284 10 13 10.6716 13 11.5C13 12.3284 12.3284 13 11.5 13Z" stroke="white" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M10.1875 4.21113C9.88852 4.03854 9.7861 3.65629 9.95869 3.35736C10.1313 3.05843 10.5135 2.95601 10.8125 3.12859L10.1875 4.21113ZM11.7888 10.1875C12.9969 8.09496 12.28 5.41925 10.1875 4.21113L10.8125 3.12859C13.5028 4.6819 14.4246 8.12209 12.8713 10.8125L11.7888 10.1875Z" fill="white"/>
-</g>
-<defs>
-<clipPath id="clip0_62_95">
-<rect width="16" height="16" fill="white" transform="matrix(-1 0 0 1 16 0)"/>
-</clipPath>
-</defs>
+<path d="M12.5 6C13.3284 6 14 5.32843 14 4.5C14 3.67157 13.3284 3 12.5 3C11.6716 3 11 3.67157 11 4.5C11 5.32843 11.6716 6 12.5 6Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M3.5 13C4.32843 13 5 12.3284 5 11.5C5 10.6716 4.32843 10 3.5 10C2.67157 10 2 10.6716 2 11.5C2 12.3284 2.67157 13 3.5 13Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M7.00391 13.9293C8.16208 14.1122 9.35055 13.9659 10.4234 13.5085C11.4962 13.0511 12.4066 12.3024 13.0426 11.3545C13.6787 10.4066 14.0128 9.30075 14.0037 8.17293C13.9977 7.42342 13.8404 6.68587 13.5444 6" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M9.00391 2.07233C7.83928 1.90288 6.64809 2.05936 5.57504 2.52278C4.502 2.9862 3.59331 3.73659 2.95939 4.68279C2.32547 5.62899 1.99362 6.73024 2.00415 7.85274C2.0111 8.59299 2.16675 9.32147 2.45883 10" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/repl_off.svg 🔗

@@ -1,20 +1,11 @@
-<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
-<g clip-path="url(#clip0_39_129)">
-<path d="M22.0209 11.9553C22.0059 10.0068 21.4219 8.10512 20.3408 6.48401" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M10.1001 2.18C11.355 1.93537 12.1493 1.93674 13.5027 2.10594" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M21.8198 10.1C22.0644 11.3548 22.0644 12.6451 21.8198 13.9" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M20.2898 17.6C19.5716 18.6622 18.6548 19.5757 17.5898 20.29" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M13.9008 21.82C12.6459 22.0644 11.6432 22.1543 10.3883 21.91" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M2.18005 13.9C1.93543 12.6451 1.93543 11.3548 2.18005 10.1" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M3.70996 6.40002C4.42822 5.33775 5.34503 4.42433 6.40996 3.71002" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M12 13C12.5523 13 13 12.5523 13 12C13 11.4477 12.5523 11 12 11C11.4477 11 11 11.4477 11 12C11 12.5523 11.4477 13 12 13Z" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M19 7C20.1046 7 21 6.10457 21 5C21 3.89543 20.1046 3 19 3C17.8954 3 17 3.89543 17 5C17 6.10457 17.8954 7 19 7Z" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M5 21C6.10457 21 7 20.1046 7 19C7 17.8954 6.10457 17 5 17C3.89543 17 3 17.8954 3 19C3 20.1046 3.89543 21 5 21Z" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M1.99072 12.0748C2.00804 14.0118 2.58758 15.9021 3.65891 17.516" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
-</g>
-<defs>
-<clipPath id="clip0_39_129">
-<rect width="24" height="24" fill="white"/>
-</clipPath>
-</defs>
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M14.0039 8.01361C13.9981 7.31989 13.8495 6.63699 13.5698 6" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M7.00391 2.01361C7.74152 2.01361 8.2084 2.01361 9.00391 2.01361" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M13.0039 11.0136C12.4719 11.8034 11.7928 12.4825 11.0039 13.0136" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M9.00391 14.0136C8.28937 14.0136 7.71844 14.0136 7.00391 14.0136" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M3.00391 5.01361C3.53595 4.22382 4.21507 3.5447 5.00391 3.01361" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M8.00391 9.01361C8.55621 9.01361 9.00391 8.56591 9.00391 8.01361C9.00391 7.46131 8.55621 7.01361 8.00391 7.01361C7.45161 7.01361 7.00391 7.46131 7.00391 8.01361C7.00391 8.56591 7.45161 9.01361 8.00391 9.01361Z" fill="black" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M12.5 6C13.3284 6 14 5.32843 14 4.5C14 3.67157 13.3284 3 12.5 3C11.6716 3 11 3.67157 11 4.5C11 5.32843 11.6716 6 12.5 6Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M3.5 13C4.32843 13 5 12.3284 5 11.5C5 10.6716 4.32843 10 3.5 10C2.67157 10 2 10.6716 2 11.5C2 12.3284 2.67157 13 3.5 13Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M2.00391 8.01361C2.01055 8.69737 2.15535 9.37058 2.42723 10" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/repl_pause.svg 🔗

@@ -1,15 +1,8 @@
-<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
-<g clip-path="url(#clip0_32_70)">
-<path d="M19 7C20.1046 7 21 6.10457 21 5C21 3.89543 20.1046 3 19 3C17.8954 3 17 3.89543 17 5C17 6.10457 17.8954 7 19 7Z" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M5 21C6.10457 21 7 20.1046 7 19C7 17.8954 6.10457 17 5 17C3.89543 17 3 17.8954 3 19C3 20.1046 3.89543 21 5 21Z" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M10.3999 21.9C12.3227 22.2159 14.2958 21.9632 16.0769 21.173C17.858 20.3827 19.3694 19.0893 20.4254 17.4517C21.4814 15.8142 22.036 13.9037 22.021 11.9553C22.006 10.0068 21.422 8.10512 20.3409 6.48401" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M13.4998 2.10002C11.5849 1.8076 9.62631 2.07763 7.86198 2.87732C6.09765 3.677 4.60356 4.9719 3.56126 6.60468C2.51896 8.23745 1.97332 10.1378 1.99063 12.0748C2.00795 14.0118 2.58749 15.9021 3.65882 17.516" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M10 15V9" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M14 15V9" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
-</g>
-<defs>
-<clipPath id="clip0_32_70">
-<rect width="24" height="24" fill="white"/>
-</clipPath>
-</defs>
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M12.5 6C13.3284 6 14 5.32843 14 4.5C14 3.67157 13.3284 3 12.5 3C11.6716 3 11 3.67157 11 4.5C11 5.32843 11.6716 6 12.5 6Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M3.5 13C4.32843 13 5 12.3284 5 11.5C5 10.6716 4.32843 10 3.5 10C2.67157 10 2 10.6716 2 11.5C2 12.3284 2.67157 13 3.5 13Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M7.00391 13.9293C8.16208 14.1122 9.35055 13.9659 10.4234 13.5085C11.4962 13.0511 12.4066 12.3024 13.0426 11.3545C13.6787 10.4066 14.0128 9.30075 14.0037 8.17293C13.9977 7.42342 13.8404 6.68587 13.5444 6" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M9.00391 2.07233C7.83928 1.90288 6.64809 2.05936 5.57504 2.52278C4.502 2.9862 3.59331 3.73659 2.95939 4.68279C2.32547 5.62899 1.99362 6.73024 2.00415 7.85274C2.0111 8.59299 2.16675 9.32147 2.45883 10" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M7.00391 10.0059V6.00592" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M9.00391 10.0059V6.00592" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/repl_play.svg 🔗

@@ -1,14 +1,7 @@
-<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
-<g clip-path="url(#clip0_32_64)">
-<path d="M19 7C20.1046 7 21 6.10457 21 5C21 3.89543 20.1046 3 19 3C17.8954 3 17 3.89543 17 5C17 6.10457 17.8954 7 19 7Z" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M5 21C6.10457 21 7 20.1046 7 19C7 17.8954 6.10457 17 5 17C3.89543 17 3 17.8954 3 19C3 20.1046 3.89543 21 5 21Z" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M10.3999 21.9C12.3227 22.2159 14.2958 21.9632 16.0769 21.173C17.858 20.3827 19.3694 19.0893 20.4254 17.4517C21.4814 15.8142 22.036 13.9037 22.021 11.9553C22.006 10.0068 21.422 8.10512 20.3409 6.48401" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M13.4998 2.10002C11.5849 1.8076 9.62631 2.07763 7.86198 2.87732C6.09765 3.677 4.60356 4.9719 3.56126 6.60468C2.51896 8.23745 1.97332 10.1378 1.99063 12.0748C2.00795 14.0118 2.58749 15.9021 3.65882 17.516" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M10 8.56055C10 8.32095 10.267 8.17803 10.4664 8.31094L15.6256 11.7504C15.8037 11.8691 15.8037 12.1309 15.6256 12.2496L10.4664 15.6891C10.267 15.822 10 15.6791 10 15.4394V8.56055Z" fill="white" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
-</g>
-<defs>
-<clipPath id="clip0_32_64">
-<rect width="24" height="24" fill="white"/>
-</clipPath>
-</defs>
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M7.00366 6.16662C7.00366 6.03849 7.14274 5.96206 7.24661 6.03313L9.93408 7.87243C10.0269 7.93591 10.0269 8.07591 9.93408 8.13939L7.24661 9.97871C7.14274 10.0498 7.00366 9.97336 7.00366 9.84518V6.16662Z" fill="black" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M12.5 6C13.3284 6 14 5.32843 14 4.5C14 3.67157 13.3284 3 12.5 3C11.6716 3 11 3.67157 11 4.5C11 5.32843 11.6716 6 12.5 6Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M3.5 13C4.32843 13 5 12.3284 5 11.5C5 10.6716 4.32843 10 3.5 10C2.67157 10 2 10.6716 2 11.5C2 12.3284 2.67157 13 3.5 13Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M7.00391 13.9293C8.16208 14.1122 9.35055 13.9659 10.4234 13.5085C11.4962 13.0511 12.4066 12.3024 13.0426 11.3545C13.6787 10.4066 14.0128 9.30075 14.0037 8.17293C13.9977 7.42342 13.8404 6.68587 13.5444 6" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M9.00391 2.07233C7.83928 1.90288 6.64809 2.05936 5.57504 2.52278C4.502 2.9862 3.59331 3.73659 2.95939 4.68279C2.32547 5.62899 1.99362 6.73024 2.00415 7.85274C2.0111 8.59299 2.16675 9.32147 2.45883 10" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/replace.svg 🔗

@@ -1,5 +1,5 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M7.99988 13C5.97267 13 4.22723 11.7936 3.44238 10.0595M7.99988 3C10.1122 3 11.9185 4.30981 12.6511 6.16152" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M7.99988 13C5.97267 13 4.22723 11.7936 3.44238 10.0595M7.99988 3C10.1122 3 11.9185 4.30981 12.6511 6.16152" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 <path d="M2.65625 3.29688C3.00143 3.29688 3.28125 3.01705 3.28125 2.67188C3.28125 2.3267 3.00143 2.04688 2.65625 2.04688C2.31107 2.04688 2.03125 2.3267 2.03125 2.67188C2.03125 3.01705 2.31107 3.29688 2.65625 3.29688Z" fill="black"/>
 <path d="M4.71094 3.29688C5.05612 3.29688 5.33594 3.01705 5.33594 2.67188C5.33594 2.3267 5.05612 2.04688 4.71094 2.04688C4.36576 2.04688 4.08594 2.3267 4.08594 2.67188C4.08594 3.01705 4.36576 3.29688 4.71094 3.29688Z" fill="black"/>
 <path d="M5.96094 4.99219C6.30612 4.99219 6.58594 4.71237 6.58594 4.36719C6.58594 4.02201 6.30612 3.74219 5.96094 3.74219C5.61576 3.74219 5.33594 4.02201 5.33594 4.36719C5.33594 4.71237 5.61576 4.99219 5.96094 4.99219Z" fill="black"/>

assets/icons/replace_next.svg 🔗

@@ -1,4 +1,4 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-    <path d="M8.5 6.88672C10.433 6.88672 12 8.45372 12 10.3867V13M12 13L14 11M12 13L10 11" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+    <path d="M8.5 6.88672C10.433 6.88672 12 8.45372 12 10.3867V13M12 13L14 11M12 13L10 11" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>

assets/icons/rerun.svg 🔗

@@ -1,7 +1 @@
-<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M3 12C3 9.61305 3.94821 7.32387 5.63604 5.63604C7.32387 3.94821 9.61305 3 12 3C14.516 3.00947 16.931 3.99122 18.74 5.74L21 8" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M21 3V8H16" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M21 12C21 14.3869 20.0518 16.6761 18.364 18.364C16.6761 20.0518 14.3869 21 12 21C9.48395 20.9905 7.06897 20.0088 5.26 18.26L3 16" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M8 16H3V21" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M10 9.37052C10 8.98462 10.4186 8.74419 10.7519 8.93863L15.2596 11.5681C15.5904 11.761 15.5904 12.2389 15.2596 12.4319L10.7519 15.0614C10.4186 15.2558 10 15.0154 10 14.6295V9.37052Z" fill="black"/>
-</svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M2 8a6 6 0 0 1 6-6 6.5 6.5 0 0 1 4.493 1.827L14 5.333"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M14 2v3.333h-3.333M14 8a6 6 0 0 1-6 6 6.5 6.5 0 0 1-4.493-1.827L2 10.667"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M5.333 10.667H2V14"/><path fill="#000" d="M6.667 6.247c0-.257.279-.418.501-.288l3.005 1.753c.22.129.22.447 0 .576L7.168 10.04a.333.333 0 0 1-.501-.288V6.247Z"/></svg>

assets/icons/return.svg 🔗

@@ -1 +1,4 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-corner-down-left"><polyline points="9 10 4 15 9 20"/><path d="M20 4v7a4 4 0 0 1-4 4H4"/></svg>
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M6.00008 6.66669L2.66675 10L6.00008 13.3334" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M13.3334 2.66669V7.33335C13.3334 8.0406 13.0525 8.71888 12.5524 9.21897C12.0523 9.71907 11.374 10 10.6667 10H2.66675" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

assets/icons/rotate_ccw.svg 🔗

@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-rotate-ccw"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="m3 5.778 1.256-1.256A5.417 5.417 0 0 1 8 3a5 5 0 1 1-4.583 7"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M3 3v3h3"/></svg>

assets/icons/rotate_cw.svg 🔗

@@ -1,4 +1 @@
-<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M12 6.5L9.99556 4.21778C9.27778 3.5 8.12 3 7 3C6.20888 3 5.43552 3.2346 4.77772 3.67412C4.11992 4.11365 3.60723 4.73836 3.30448 5.46927C3.00173 6.20017 2.92252 7.00444 3.07686 7.78036C3.2312 8.55628 3.61216 9.26902 4.17157 9.82842C4.73098 10.3878 5.44372 10.7688 6.21964 10.9231C6.99556 11.0775 7.79983 10.9983 8.53073 10.6955C8.88113 10.5504 9.20712 10.357 9.5 10.1225" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M12 4V6.5H9.5" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
-</svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="m13 5.778-1.256-1.256A5.416 5.416 0 0 0 8 3a5 5 0 1 0 4.583 7"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M13 3v3h-3"/></svg>

assets/icons/route.svg 🔗

@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-route"><circle cx="6" cy="19" r="3"/><path d="M9 19h8.5a3.5 3.5 0 0 0 0-7h-11a3.5 3.5 0 0 1 0-7H15"/><circle cx="18" cy="5" r="3"/></svg>

assets/icons/save.svg 🔗

@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-save"><path d="M15.2 3a2 2 0 0 1 1.4.6l3.8 3.8a2 2 0 0 1 .6 1.4V19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z"/><path d="M17 21v-7a1 1 0 0 0-1-1H8a1 1 0 0 0-1 1v7"/><path d="M7 3v4a1 1 0 0 0 1 1h7"/></svg>

assets/icons/scissors.svg 🔗

@@ -1 +1,3 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-scissors-icon lucide-scissors"><circle cx="6" cy="6" r="3"/><path d="M8.12 8.12 12 12"/><path d="M20 4 8.12 15.88"/><circle cx="6" cy="18" r="3"/><path d="M14.8 14.8 20 20"/></svg>
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M6.03641 5.53641L8.33797 7.83797M13.0825 3.0934L6.03641 10.1395M9.99896 9.49896L13.0825 12.5825M4.77932 6.05864C5.25123 6.05864 5.7038 5.87118 6.03749 5.53749C6.37118 5.2038 6.55864 4.75123 6.55864 4.27932C6.55864 3.80742 6.37118 3.35484 6.03749 3.02115C5.7038 2.68746 5.25123 2.5 4.77932 2.5C4.30742 2.5 3.85484 2.68746 3.52115 3.02115C3.18746 3.35484 3 3.80742 3 4.27932C3 4.75123 3.18746 5.2038 3.52115 5.53749C3.85484 5.87118 4.30742 6.05864 4.77932 6.05864ZM4.77932 13.1759C5.25123 13.1759 5.7038 12.9885 6.03749 12.6548C6.37118 12.3211 6.55864 11.8685 6.55864 11.3966C6.55864 10.9247 6.37118 10.4721 6.03749 10.1384C5.7038 9.80475 5.25123 9.61729 4.77932 9.61729C4.30742 9.61729 3.85484 9.80475 3.52115 10.1384C3.18746 10.4721 3 10.9247 3 11.3966C3 11.8685 3.18746 12.3211 3.52115 12.6548C3.85484 12.9885 4.30742 13.1759 4.77932 13.1759Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

assets/icons/screen.svg 🔗

@@ -1,5 +1,5 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M12.8 3H3.2C2.53726 3 2 3.51167 2 4.14286V9.85714C2 10.4883 2.53726 11 3.2 11H12.8C13.4627 11 14 10.4883 14 9.85714V4.14286C14 3.51167 13.4627 3 12.8 3Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M5.33325 14H10.6666" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M8 11.3333V14" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M12.8 3H3.2C2.53726 3 2 3.51167 2 4.14286V9.85714C2 10.4883 2.53726 11 3.2 11H12.8C13.4627 11 14 10.4883 14 9.85714V4.14286C14 3.51167 13.4627 3 12.8 3Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M5.33325 14H10.6666" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M8 11.3333V14" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/scroll_text.svg 🔗

@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-scroll-text-icon lucide-scroll-text"><path d="M15 12h-5"/><path d="M15 8h-5"/><path d="M19 17V5a2 2 0 0 0-2-2H4"/><path d="M8 21h12a2 2 0 0 0 2-2v-1a1 1 0 0 0-1-1H11a1 1 0 0 0-1 1v1a2 2 0 1 1-4 0V5a2 2 0 1 0-4 0v2a1 1 0 0 0 1 1h3"/></svg>

assets/icons/search_selection.svg 🔗

@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-text-quote"><path d="M17 6H3"/><path d="M21 12H8"/><path d="M21 18H8"/><path d="M3 12v6"/></svg>

assets/icons/select_all.svg 🔗

@@ -1,5 +1 @@
-<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M9.5 7V9.5M9.5 9.5V12M9.5 9.5H12M9.5 9.5H7M9.5 9.5L11.1667 7.83333M9.5 9.5L7.83333 11.1667M9.5 9.5L11.1667 11.1667M9.5 9.5L7.83333 7.83333" stroke="#687076" stroke-width="1.25" stroke-linecap="round"/>

assets/icons/send.svg 🔗

@@ -1,4 +1 @@
-<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M3.09666 3.02263C3.0567 3.00312 3.01178 2.9961 2.96778 3.0025C2.92377 3.00889 2.88271 3.02839 2.84995 3.05847C2.8172 3.08854 2.79426 3.12778 2.78413 3.17108C2.77401 3.21439 2.77716 3.25973 2.79319 3.30121L4.05638 6.69C4.13088 6.89005 4.13088 7.11022 4.05638 7.31027L2.79363 10.6991C2.77769 10.7405 2.77457 10.7858 2.78469 10.829C2.79481 10.8722 2.8177 10.9114 2.85038 10.9414C2.88306 10.9715 2.92402 10.991 2.96794 10.9975C3.01186 11.0039 3.05671 10.997 3.09666 10.9776L11.0943 7.20097C11.1324 7.18297 11.1645 7.15455 11.187 7.11899C11.2096 7.08344 11.2215 7.04222 11.2215 7.00014C11.2215 6.95805 11.2096 6.91683 11.187 6.88128C11.1645 6.84573 11.1324 6.8173 11.0943 6.79931L3.09666 3.02263Z" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M4.11255 7.00014H11.2216" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
-</svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M3.377 3.028a.25.25 0 0 0-.292.045.291.291 0 0 0-.067.303l1.496 4.236c.088.25.088.526 0 .776l-1.496 4.236a.291.291 0 0 0 .067.303.25.25 0 0 0 .292.045l9.472-4.72a.267.267 0 0 0 .11-.103.288.288 0 0 0-.11-.4L3.377 3.028ZM5 8h8"/></svg>

assets/icons/server.svg 🔗

@@ -1,16 +1,6 @@
-<svg
-  xmlns="http://www.w3.org/2000/svg"
-  width="24"
-  height="24"
-  viewBox="0 0 24 24"
-  fill="none"
-  stroke="currentColor"
-  stroke-width="2"
-  stroke-linecap="round"
-  stroke-linejoin="round"
->
-  <rect width="20" height="8" x="2" y="2" rx="2" ry="2" />
-  <rect width="20" height="8" x="2" y="14" rx="2" ry="2" />
-  <line x1="6" x2="6.01" y1="6" y2="6" />
-  <line x1="6" x2="6.01" y1="18" y2="18" />
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M12.8 9H3.2C2.53726 9 2 9.44772 2 10V12C2 12.5523 2.53726 13 3.2 13H12.8C13.4627 13 14 12.5523 14 12V10C14 9.44772 13.4627 9 12.8 9Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M12.8 3H3.2C2.53726 3 2 3.44772 2 4V6C2 6.55228 2.53726 7 3.2 7H12.8C13.4627 7 14 6.55228 14 6V4C14 3.44772 13.4627 3 12.8 3Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M4 11H4.00667" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M4 5H4.00667" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/settings_alt.svg 🔗

@@ -1,6 +0,0 @@
-<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M3 4H8" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M6 10L11 10" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<circle cx="4" cy="10" r="1.875" stroke="black" stroke-width="1.5"/>
-<circle cx="10" cy="4" r="1.875" stroke="black" stroke-width="1.5"/>
-</svg>

assets/icons/shield_check.svg 🔗

@@ -1,4 +1,4 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M13.0001 8.62505C13.0001 11.75 10.8126 13.3125 8.21266 14.2187C8.07651 14.2648 7.92862 14.2626 7.79392 14.2125C5.18771 13.3125 3.00024 11.75 3.00024 8.62505V4.25012C3.00024 4.08436 3.06609 3.92539 3.1833 3.80818C3.30051 3.69098 3.45948 3.62513 3.62523 3.62513C4.87521 3.62513 6.43769 2.87514 7.52517 1.92516C7.65758 1.81203 7.82601 1.74988 8.00016 1.74988C8.17431 1.74988 8.34275 1.81203 8.47515 1.92516C9.56889 2.88139 11.1251 3.62513 12.3751 3.62513C12.5408 3.62513 12.6998 3.69098 12.817 3.80818C12.9342 3.92539 13.0001 4.08436 13.0001 4.25012V8.62505Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M6 8.00002L7.33333 9.33335L10 6.66669" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M13.0001 8.62505C13.0001 11.75 10.8126 13.3125 8.21266 14.2187C8.07651 14.2648 7.92862 14.2626 7.79392 14.2125C5.18771 13.3125 3.00024 11.75 3.00024 8.62505V4.25012C3.00024 4.08436 3.06609 3.92539 3.1833 3.80818C3.30051 3.69098 3.45948 3.62513 3.62523 3.62513C4.87521 3.62513 6.43769 2.87514 7.52517 1.92516C7.65758 1.81203 7.82601 1.74988 8.00016 1.74988C8.17431 1.74988 8.34275 1.81203 8.47515 1.92516C9.56889 2.88139 11.1251 3.62513 12.3751 3.62513C12.5408 3.62513 12.6998 3.69098 12.817 3.80818C12.9342 3.92539 13.0001 4.08436 13.0001 4.25012V8.62505Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M6 8.00002L7.33333 9.33335L10 6.66669" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/shift.svg 🔗

@@ -1,3 +1,3 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M2.46475 7.99652L7.85304 2.15921C7.93223 2.07342 8.06777 2.07341 8.14696 2.15921L13.5352 7.99652C13.7126 8.18869 13.5763 8.5 13.3148 8.5H10.5V13.7C10.5 13.8657 10.3657 14 10.2 14H5.8C5.63431 14 5.5 13.8657 5.5 13.7V8.5H2.6852C2.42367 8.5 2.28737 8.18869 2.46475 7.99652Z" stroke="black" stroke-width="1.25"/>
+<path d="M3.07136 7.95724L7.86916 3.05405C7.93967 2.98199 8.06036 2.98198 8.13087 3.05405L12.9286 7.95724C13.0866 8.11865 12.9652 8.38015 12.7324 8.38015H10.226V12.748C10.226 12.8872 10.1065 13 9.95892 13H6.04111C5.89358 13 5.77399 12.8872 5.77399 12.748V8.38015H3.26765C3.03479 8.38015 2.91342 8.11865 3.07136 7.95724Z" stroke="black" stroke-width="1.2"/>
 </svg>

assets/icons/slash.svg 🔗

@@ -1 +1,3 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-slash"><path d="M22 2 2 22"/></svg>
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M12.9999 2.99988L2.99976 13" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

assets/icons/slash_square.svg 🔗

@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-square-slash"><rect width="18" height="18" x="3" y="3" rx="2"/><line x1="9" x2="15" y1="15" y2="9"/></svg>

assets/icons/sliders.svg 🔗

@@ -1,8 +1,8 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M2 5H4" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
-<path d="M8 5L14 5" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
-<path d="M12 11L14 11" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
-<path d="M2 11H8" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
-<circle cx="6" cy="5" r="2" fill="black" fill-opacity="0.1" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
-<circle cx="10" cy="11" r="2" fill="black" fill-opacity="0.1" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
+<path d="M2 5H4" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
+<path d="M8 5L14 5" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
+<path d="M12 11L14 11" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
+<path d="M2 11H8" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
+<circle cx="6" cy="5" r="2" fill="black" fill-opacity="0.1" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
+<circle cx="10" cy="11" r="2" fill="black" fill-opacity="0.1" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
 </svg>

assets/icons/sliders_alt.svg 🔗

@@ -1,6 +0,0 @@
-<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M3 4H8" stroke="black" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M6 10L11 10" stroke="black" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"/>
-<circle cx="4" cy="10" r="1.875" stroke="black" stroke-width="1.75"/>
-<circle cx="10" cy="4" r="1.875" stroke="black" stroke-width="1.75"/>
-</svg>

assets/icons/sliders_vertical.svg 🔗

@@ -1,11 +0,0 @@
-<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M3.6665 14V9.33333" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M3.6665 6.66667V2" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M8 14V8" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M8 5.33333V2" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M12.3335 14V10.6667" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M12.3335 8V2" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M2.3335 9.33333H5.00016" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M6.6665 5.33334H9.33317" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M11 10.6667H13.6667" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-</svg>

assets/icons/snip.svg 🔗

@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-scissors"><circle cx="6" cy="6" r="3"/><path d="M8.12 8.12 12 12"/><path d="M20 4 8.12 15.88"/><circle cx="6" cy="18" r="3"/><path d="M14.8 14.8 20 20"/></svg>

assets/icons/space.svg 🔗

@@ -1 +1,3 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-space"><path d="M22 17v1c0 .5-.5 1-1 1H3c-.5 0-1-.5-1-1v-1"/></svg>
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M13.9999 11.4V12C13.9999 12.3 13.6999 12.6 13.3999 12.6H2.59976C2.29976 12.6 1.99976 12.3 1.99976 12V11.4" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

assets/icons/sparkle.svg 🔗

@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-sparkle"><path d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M6.762 10.1a1.2 1.2 0 0 0-.862-.862l-3.68-.95a.3.3 0 0 1 0-.577l3.68-.95a1.2 1.2 0 0 0 .862-.86l.95-3.682a.3.3 0 0 1 .577 0L9.238 5.9a1.2 1.2 0 0 0 .862.862l3.68.949a.3.3 0 0 1 0 .578l-3.68.949a1.2 1.2 0 0 0-.862.862l-.95 3.68a.3.3 0 0 1-.577 0l-.949-3.68Z"/></svg>

assets/icons/sparkle_alt.svg 🔗

@@ -1,3 +0,0 @@
-<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M6 6C5.69062 6.30938 4.56159 6.55977 3.51192 6.73263C3.27345 6.7719 3.27345 7.2281 3.51192 7.26737C4.56159 7.44023 5.69062 7.69062 6 8C6.30938 8.30938 6.55977 9.43841 6.73263 10.4881C6.7719 10.7266 7.2281 10.7266 7.26737 10.4881C7.44023 9.43841 7.69062 8.30938 8 8C8.30938 7.69062 9.43841 7.44023 10.4881 7.26737C10.7266 7.2281 10.7266 6.7719 10.4881 6.73263C9.43841 6.55977 8.30938 6.30938 8 6C7.69062 5.69062 7.44023 4.56159 7.26737 3.51192C7.2281 3.27345 6.7719 3.27345 6.73263 3.51192C6.55977 4.56159 6.30938 5.69062 6 6Z" stroke="black" stroke-width="1.25" stroke-linejoin="round"/>
-</svg>

assets/icons/speaker_loud.svg 🔗

@@ -1,8 +0,0 @@
-<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
-  <path
-    fill-rule="evenodd"
-    clip-rule="evenodd"

assets/icons/split.svg 🔗

@@ -1,5 +1,5 @@
-<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M7 2H10C11.1046 2 12 2.89543 12 4V10C12 11.1046 11.1046 12 10 12H7V2Z" fill="black" fill-opacity="0.25"/>
-<rect x="2" y="2" width="10" height="10" rx="0.5" stroke="black" stroke-width="1.25"/>
-<line x1="7" y1="2" x2="7" y2="12" stroke="black" stroke-width="1.25"/>
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<rect x="8" y="2" width="6" height="12" fill="black" fill-opacity="0.25"/>
+<path d="M13.4 2H2.6C2.26863 2 2 2.26863 2 2.6V13.4C2 13.7314 2.26863 14 2.6 14H13.4C13.7314 14 14 13.7314 14 13.4V2.6C14 2.26863 13.7314 2 13.4 2Z" stroke="black" stroke-width="1.2"/>
+<path d="M8 2L8 14" stroke="black" stroke-width="1.2"/>
 </svg>

assets/icons/split_alt.svg 🔗

@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-split-icon lucide-split"><path d="M16 3h5v5"/><path d="M8 3H3v5"/><path d="M12 22v-8.3a4 4 0 0 0-1.172-2.872L3 3"/><path d="m15 9 6-6"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M10 2.833h3v3M6 2.833H3v3"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M8 13.833V9.028a2.401 2.401 0 0 0-.165-.9 2.325 2.325 0 0 0-.486-.763L3 2.833M10 5.833l3-3"/></svg>

assets/icons/square_dot.svg 🔗

@@ -1 +1,4 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-square-dot"><rect width="18" height="18" x="3" y="3" rx="2"/><circle cx="12" cy="12" r="1"/></svg>
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M12.6667 2H3.33333C2.59695 2 2 2.59695 2 3.33333V12.6667C2 13.403 2.59695 14 3.33333 14H12.6667C13.403 14 14 13.403 14 12.6667V3.33333C14 2.59695 13.403 2 12.6667 2Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<circle cx="8" cy="8" r="1.25" fill="black" stroke="black" stroke-width="0.5"/>
+</svg>

assets/icons/square_minus.svg 🔗

@@ -1 +1,4 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-square-minus"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M8 12h8"/></svg>
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M12.6667 2H3.33333C2.59695 2 2 2.59695 2 3.33333V12.6667C2 13.403 2.59695 14 3.33333 14H12.6667C13.403 14 14 13.403 14 12.6667V3.33333C14 2.59695 13.403 2 12.6667 2Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M6 8H10" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

assets/icons/square_plus.svg 🔗

@@ -1 +1,5 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-square-plus"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M8 12h8"/><path d="M12 8v8"/></svg>
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M12.6667 2H3.33333C2.59695 2 2 2.59695 2 3.33333V12.6667C2 13.403 2.59695 14 3.33333 14H12.6667C13.403 14 14 13.403 14 12.6667V3.33333C14 2.59695 13.403 2 12.6667 2Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M6 8H10" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M8 6V10" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

assets/icons/star.svg 🔗

@@ -1,3 +1,3 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">

assets/icons/stop.svg 🔗

@@ -1,3 +1,3 @@
-<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M4 9.8V4.2C4 4.08954 4.08954 4 4.2 4H9.8C9.91046 4 10 4.08954 10 4.2V9.8C10 9.91046 9.91046 10 9.8 10H4.2C4.08954 10 4 9.91046 4 9.8Z" stroke="#C56757" stroke-width="1.25" stroke-linejoin="round"/>
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M5 10.8V5.2C5 5.08954 5.08954 5 5.2 5H10.8C10.9105 5 11 5.08954 11 5.2V10.8C11 10.9105 10.9105 11 10.8 11H5.2C5.08954 11 5 10.9105 5 10.8Z" fill="black" stroke="black" stroke-width="1.2" stroke-linejoin="round"/>
 </svg>

assets/icons/stop_filled.svg 🔗

@@ -1,3 +0,0 @@
-<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M4 9.8V4.2C4 4.08954 4.08954 4 4.2 4H9.8C9.91046 4 10 4.08954 10 4.2V9.8C10 9.91046 9.91046 10 9.8 10H4.2C4.08954 10 4 9.91046 4 9.8Z" fill="#C56757" stroke="#C56757" stroke-width="1.25" stroke-linejoin="round"/>
-</svg>

assets/icons/supermaven.svg 🔗

@@ -1,8 +1,8 @@
-<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M3.30859 13.0703C3.80693 13.0703 4.21094 12.6663 4.21094 12.168C4.21094 11.6696 3.80693 11.2656 3.30859 11.2656C2.81025 11.2656 2.40625 11.6696 2.40625 12.168C2.40625 12.6663 2.81025 13.0703 3.30859 13.0703Z" fill="black"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M6.53516 8.03849L4.10799 12.6055L2.51562 11.7584L4.94279 7.19141L6.53516 8.03849Z" fill="black"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M7.38281 2.62443L4.93916 7.19141L3.33594 6.34432L5.77959 1.77734L7.38281 2.62443Z" fill="black"/>
-<path d="M6.5625 3.08984C7.06084 3.08984 7.46484 2.68585 7.46484 2.1875C7.46484 1.68915 7.06084 1.28516 6.5625 1.28516C6.06416 1.28516 5.66016 1.68915 5.66016 2.1875C5.66016 2.68585 6.06416 3.08984 6.5625 3.08984Z" fill="black"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M10.882 1.31204C11.2842 1.41224 11.5664 1.7732 11.5664 2.18737V12.168H9.76084V5.8056L8.12938 8.87176L6.53516 8.02471L9.86653 1.76385C10.0611 1.39816 10.4799 1.21184 10.882 1.31204Z" fill="black"/>
-<path d="M10.6641 13.0703C11.1624 13.0703 11.5664 12.6663 11.5664 12.168C11.5664 11.6696 11.1624 11.2656 10.6641 11.2656C10.1657 11.2656 9.76172 11.6696 9.76172 12.168C9.76172 12.6663 10.1657 13.0703 10.6641 13.0703Z" fill="black"/>
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M3.78125 14.9375C4.35078 14.9375 4.8125 14.4758 4.8125 13.9063C4.8125 13.3367 4.35078 12.875 3.78125 12.875C3.21171 12.875 2.75 13.3367 2.75 13.9063C2.75 14.4758 3.21171 14.9375 3.78125 14.9375Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M7.46876 9.18684L4.69485 14.4063L2.875 13.4382L5.64891 8.21875L7.46876 9.18684Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M8.43749 2.99935L5.64475 8.21876L3.8125 7.25066L6.60524 2.03125L8.43749 2.99935Z" fill="black"/>
+<path d="M7.5 3.53124C8.06953 3.53124 8.53124 3.06954 8.53124 2.5C8.53124 1.93045 8.06953 1.46875 7.5 1.46875C6.93046 1.46875 6.46875 1.93045 6.46875 2.5C6.46875 3.06954 6.93046 3.53124 7.5 3.53124Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M12.4366 1.49947C12.8962 1.61399 13.2187 2.02651 13.2187 2.49985V13.9063H11.1552V6.63497L9.29072 10.1392L7.46875 9.1711L11.276 2.01583C11.4984 1.5979 11.977 1.38496 12.4366 1.49947Z" fill="black"/>
+<path d="M12.1875 14.9375C12.757 14.9375 13.2187 14.4758 13.2187 13.9063C13.2187 13.3367 12.757 12.875 12.1875 12.875C11.6179 12.875 11.1562 13.3367 11.1562 13.9063C11.1562 14.4758 11.6179 14.9375 12.1875 14.9375Z" fill="black"/>
 </svg>

assets/icons/supermaven_disabled.svg 🔗

@@ -1,15 +1 @@
-<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<g opacity="0.5">
-<path d="M3.78125 14.9375C4.35078 14.9375 4.8125 14.4758 4.8125 13.9062C4.8125 13.3367 4.35078 12.875 3.78125 12.875C3.21172 12.875 2.75 13.3367 2.75 13.9062C2.75 14.4758 3.21172 14.9375 3.78125 14.9375Z" fill="white"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M7.46875 9.18684L4.69484 14.4062L2.875 13.4382L5.64891 8.21875L7.46875 9.18684Z" fill="white"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M8.4375 2.99935L5.64475 8.21875L3.8125 7.25066L6.60525 2.03125L8.4375 2.99935Z" fill="white"/>
-<path d="M7.5 3.53125C8.06953 3.53125 8.53125 3.06954 8.53125 2.5C8.53125 1.93046 8.06953 1.46875 7.5 1.46875C6.93047 1.46875 6.46875 1.93046 6.46875 2.5C6.46875 3.06954 6.93047 3.53125 7.5 3.53125Z" fill="white"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M12.4366 1.49947C12.8962 1.61399 13.2188 2.02651 13.2188 2.49985V13.9063H11.1552V6.63497L9.29072 10.1392L7.46875 9.17109L11.276 2.01583C11.4984 1.59789 11.977 1.38496 12.4366 1.49947Z" fill="white"/>
-<path d="M12.1875 14.9375C12.757 14.9375 13.2188 14.4758 13.2188 13.9062C13.2188 13.3367 12.757 12.875 12.1875 12.875C11.618 12.875 11.1562 13.3367 11.1562 13.9062C11.1562 14.4758 11.618 14.9375 12.1875 14.9375Z" fill="white"/>
-</g>
-<g>
-<path d="M0.906311 6.42261L1.75155 4.60999L15.3462 10.9493L14.5009 12.7619L0.906311 6.42261Z" fill="white"/>
-<circle cx="14.7841" cy="11.7906" r="1" transform="rotate(-65 14.7841 11.7906)" fill="white"/>
-<circle cx="1.32893" cy="5.51631" r="1" transform="rotate(-65 1.32893 5.51631)" fill="white"/>
-</g>
-</svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><g fill="#000" clip-path="url(#a)"><g opacity=".5"><path d="M3.781 14.938a1.031 1.031 0 1 0 0-2.063 1.031 1.031 0 0 0 0 2.063Z"/><path fill-rule="evenodd" d="m7.469 9.187-2.774 5.22-1.82-.969 2.774-5.22 1.82.969ZM8.438 3 5.644 8.218 3.813 7.25l2.792-5.22L8.437 3Z" clip-rule="evenodd"/><path d="M7.5 3.531a1.031 1.031 0 1 0 0-2.062 1.031 1.031 0 0 0 0 2.062Z"/><path fill-rule="evenodd" d="M12.437 1.5c.46.114.782.527.782 1v11.406h-2.064V6.635l-1.864 3.504-1.822-.968 3.807-7.155c.222-.418.701-.631 1.16-.517Z" clip-rule="evenodd"/><path d="M12.188 14.938a1.031 1.031 0 1 0 0-2.063 1.031 1.031 0 0 0 0 2.063Z"/></g><path d="m.906 6.423.845-1.813 13.595 6.34-.845 1.812L.906 6.422Z"/><path d="M15.69 12.213a1 1 0 1 0-1.812-.845 1 1 0 0 0 1.812.845ZM2.235 5.939a1 1 0 1 0-1.812-.845 1 1 0 0 0 1.812.845Z"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

assets/icons/supermaven_error.svg 🔗

@@ -1,11 +1,11 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
 <g opacity="0.5">
-<path d="M3.78125 14.9375C4.35078 14.9375 4.8125 14.4758 4.8125 13.9062C4.8125 13.3367 4.35078 12.875 3.78125 12.875C3.21172 12.875 2.75 13.3367 2.75 13.9062C2.75 14.4758 3.21172 14.9375 3.78125 14.9375Z" fill="white"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M7.46875 9.18684L4.69484 14.4062L2.875 13.4382L5.64891 8.21875L7.46875 9.18684Z" fill="white"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M8.4375 2.99935L5.64475 8.21875L3.8125 7.25066L6.60525 2.03125L8.4375 2.99935Z" fill="white"/>
-<path d="M7.5 3.53125C8.06953 3.53125 8.53125 3.06954 8.53125 2.5C8.53125 1.93046 8.06953 1.46875 7.5 1.46875C6.93047 1.46875 6.46875 1.93046 6.46875 2.5C6.46875 3.06954 6.93047 3.53125 7.5 3.53125Z" fill="white"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M12.4366 1.49947C12.8962 1.61399 13.2188 2.02651 13.2188 2.49985V13.9063H11.1552V6.63497L9.29072 10.1392L7.46875 9.17109L11.276 2.01583C11.4984 1.59789 11.977 1.38496 12.4366 1.49947Z" fill="white"/>
-<path d="M12.1875 14.9375C12.757 14.9375 13.2188 14.4758 13.2188 13.9062C13.2188 13.3367 12.757 12.875 12.1875 12.875C11.618 12.875 11.1562 13.3367 11.1562 13.9062C11.1562 14.4758 11.618 14.9375 12.1875 14.9375Z" fill="white"/>
+<path d="M3.78125 14.9375C4.35078 14.9375 4.8125 14.4758 4.8125 13.9062C4.8125 13.3367 4.35078 12.875 3.78125 12.875C3.21172 12.875 2.75 13.3367 2.75 13.9062C2.75 14.4758 3.21172 14.9375 3.78125 14.9375Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M7.46875 9.18684L4.69484 14.4062L2.875 13.4382L5.64891 8.21875L7.46875 9.18684Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M8.4375 2.99935L5.64475 8.21875L3.8125 7.25066L6.60525 2.03125L8.4375 2.99935Z" fill="black"/>
+<path d="M7.5 3.53125C8.06953 3.53125 8.53125 3.06954 8.53125 2.5C8.53125 1.93046 8.06953 1.46875 7.5 1.46875C6.93047 1.46875 6.46875 1.93046 6.46875 2.5C6.46875 3.06954 6.93047 3.53125 7.5 3.53125Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M12.4366 1.49947C12.8962 1.61399 13.2188 2.02651 13.2188 2.49985V13.9063H11.1552V6.63497L9.29072 10.1392L7.46875 9.17109L11.276 2.01583C11.4984 1.59789 11.977 1.38496 12.4366 1.49947Z" fill="black"/>
+<path d="M12.1875 14.9375C12.757 14.9375 13.2188 14.4758 13.2188 13.9062C13.2188 13.3367 12.757 12.875 12.1875 12.875C11.618 12.875 11.1562 13.3367 11.1562 13.9062C11.1562 14.4758 11.618 14.9375 12.1875 14.9375Z" fill="black"/>
 </g>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M14.6847 15.9265C14.7823 16.0241 14.9406 16.0241 15.0382 15.9265L15.9259 15.0387C16.0235 14.9411 16.0235 14.7828 15.9259 14.6851L14.2408 12.9999L15.9259 11.3146C16.0236 11.217 16.0236 11.0587 15.9259 10.961L15.0382 10.0733C14.9406 9.97561 14.7823 9.97561 14.6847 10.0733L12.9996 11.7585L11.3145 10.0732C11.2169 9.97559 11.0586 9.97559 10.9609 10.0732L10.0732 10.961C9.97559 11.0587 9.97559 11.217 10.0732 11.3146L11.7584 12.9999L10.0732 14.6851C9.97562 14.7828 9.97562 14.9411 10.0732 15.0387L10.9609 15.9265C11.0586 16.0242 11.2169 16.0242 11.3145 15.9265L12.9996 14.2413L14.6847 15.9265Z" fill="white"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M14.6847 15.9265C14.7823 16.0241 14.9406 16.0241 15.0382 15.9265L15.9259 15.0387C16.0235 14.9411 16.0235 14.7828 15.9259 14.6851L14.2408 12.9999L15.9259 11.3146C16.0236 11.217 16.0236 11.0587 15.9259 10.961L15.0382 10.0733C14.9406 9.97562 14.7823 9.97562 14.6847 10.0733L12.9996 11.7585L11.3145 10.0732C11.2169 9.9756 11.0586 9.9756 10.9609 10.0732L10.0732 10.961C9.9756 11.0587 9.9756 11.217 10.0732 11.3146L11.7584 12.9999L10.0732 14.6851C9.97563 14.7828 9.97563 14.9411 10.0732 15.0387L10.9609 15.9265C11.0586 16.0242 11.2169 16.0242 11.3145 15.9265L12.9996 14.2413L14.6847 15.9265Z" fill="black"/>
 </svg>

assets/icons/supermaven_init.svg 🔗

@@ -1,11 +1,11 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
 <g opacity="0.5">
-<path d="M3.78125 14.9375C4.35078 14.9375 4.8125 14.4758 4.8125 13.9062C4.8125 13.3367 4.35078 12.875 3.78125 12.875C3.21172 12.875 2.75 13.3367 2.75 13.9062C2.75 14.4758 3.21172 14.9375 3.78125 14.9375Z" fill="white"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M7.46875 9.18684L4.69484 14.4062L2.875 13.4382L5.64891 8.21875L7.46875 9.18684Z" fill="white"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M8.4375 2.99935L5.64475 8.21875L3.8125 7.25066L6.60525 2.03125L8.4375 2.99935Z" fill="white"/>
-<path d="M7.5 3.53125C8.06953 3.53125 8.53125 3.06954 8.53125 2.5C8.53125 1.93046 8.06953 1.46875 7.5 1.46875C6.93047 1.46875 6.46875 1.93046 6.46875 2.5C6.46875 3.06954 6.93047 3.53125 7.5 3.53125Z" fill="white"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M12.4366 1.49947C12.8962 1.61399 13.2188 2.02651 13.2188 2.49985V13.9063H11.1552V6.63497L9.29072 10.1392L7.46875 9.17109L11.276 2.01583C11.4984 1.59789 11.977 1.38496 12.4366 1.49947Z" fill="white"/>
-<path d="M12.1875 14.9375C12.757 14.9375 13.2188 14.4758 13.2188 13.9062C13.2188 13.3367 12.757 12.875 12.1875 12.875C11.618 12.875 11.1562 13.3367 11.1562 13.9062C11.1562 14.4758 11.618 14.9375 12.1875 14.9375Z" fill="white"/>
+<path d="M3.78125 14.9375C4.35078 14.9375 4.8125 14.4758 4.8125 13.9062C4.8125 13.3367 4.35078 12.875 3.78125 12.875C3.21172 12.875 2.75 13.3367 2.75 13.9062C2.75 14.4758 3.21172 14.9375 3.78125 14.9375Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M7.46875 9.18684L4.69484 14.4062L2.875 13.4382L5.64891 8.21875L7.46875 9.18684Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M8.4375 2.99935L5.64475 8.21875L3.8125 7.25066L6.60525 2.03125L8.4375 2.99935Z" fill="black"/>
+<path d="M7.5 3.53125C8.06953 3.53125 8.53125 3.06954 8.53125 2.5C8.53125 1.93046 8.06953 1.46875 7.5 1.46875C6.93047 1.46875 6.46875 1.93046 6.46875 2.5C6.46875 3.06954 6.93047 3.53125 7.5 3.53125Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M12.4366 1.49947C12.8962 1.61399 13.2188 2.02651 13.2188 2.49985V13.9063H11.1552V6.63497L9.29072 10.1392L7.46875 9.17109L11.276 2.01583C11.4984 1.59789 11.977 1.38496 12.4366 1.49947Z" fill="black"/>
+<path d="M12.1875 14.9375C12.757 14.9375 13.2188 14.4758 13.2188 13.9062C13.2188 13.3367 12.757 12.875 12.1875 12.875C11.618 12.875 11.1562 13.3367 11.1562 13.9062C11.1562 14.4758 11.618 14.9375 12.1875 14.9375Z" fill="black"/>
 </g>
-<circle cx="13" cy="13" r="3" fill="white"/>
+<path d="M13 16C14.6569 16 16 14.6569 16 13C16 11.3431 14.6569 10 13 10C11.3431 10 10 11.3431 10 13C10 14.6569 11.3431 16 13 16Z" fill="black"/>
 </svg>

assets/icons/swatch_book.svg 🔗

@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-swatch-book"><path d="M11 17a4 4 0 0 1-8 0V5a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2Z"/><path d="M16.7 13H19a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2H7"/><path d="M 7 17h.01"/><path d="m11 8 2.3-2.3a2.4 2.4 0 0 1 3.404.004L18.6 7.6a2.4 2.4 0 0 1 .026 3.434L9.9 19.8"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M7.333 11.333a2.667 2.667 0 1 1-5.333 0v-8A1.333 1.333 0 0 1 3.333 2H6a1.333 1.333 0 0 1 1.333 1.333v8Z"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M11.133 8.667h1.534A1.333 1.333 0 0 1 14 10v2.667A1.334 1.334 0 0 1 12.667 14h-8M4.667 11.333h.006"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M7.333 5.333 8.867 3.8a1.6 1.6 0 0 1 2.269.003L12.4 5.067a1.6 1.6 0 0 1 .017 2.289L6.6 13.2"/></svg>

assets/icons/tab.svg 🔗

@@ -1 +1,5 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-right-to-line"><path d="M17 12H3"/><path d="m11 18 6-6-6-6"/><path d="M21 5v14"/></svg>
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M10.3333 8H2" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M6.33325 12L10.3333 8L6.33325 4" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M13 3.33331V12.6666" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

assets/icons/terminal_alt.svg 🔗

@@ -1,5 +1,5 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M11.8889 3H4.11111C3.49746 3 3 3.49746 3 4.11111V11.8889C3 12.5025 3.49746 13 4.11111 13H11.8889C12.5025 13 13 12.5025 13 11.8889V4.11111C13 3.49746 12.5025 3 11.8889 3Z" fill="black" fill-opacity="0.15" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M8.37939 10.3243H10.3794" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M5.64966 9.32837L7.64966 7.32837L5.64966 5.32837" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M12.2782 2.49951H3.72184C3.04677 2.49951 2.49951 3.04677 2.49951 3.72184V12.2782C2.49951 12.9532 3.04677 13.5005 3.72184 13.5005H12.2782C12.9532 13.5005 13.5005 12.9532 13.5005 12.2782V3.72184C13.5005 3.04677 12.9532 2.49951 12.2782 2.49951Z" fill="black" fill-opacity="0.15" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M8 10.7502H10.7502" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M5.24976 9.21777L7.08325 7.38428L5.24976 5.55078" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/text_snippet.svg 🔗

@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-text-select"><path d="M5 3a2 2 0 0 0-2 2"/><path d="M19 3a2 2 0 0 1 2 2"/><path d="M21 19a2 2 0 0 1-2 2"/><path d="M5 21a2 2 0 0 1-2-2"/><path d="M9 3h1"/><path d="M9 21h1"/><path d="M14 3h1"/><path d="M14 21h1"/><path d="M3 9v1"/><path d="M21 9v1"/><path d="M3 14v1"/><path d="M21 14v1"/><line x1="7" x2="15" y1="8" y2="8"/><line x1="7" x2="17" y1="12" y2="12"/><line x1="7" x2="13" y1="16" y2="16"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M3.333 2A1.333 1.333 0 0 0 2 3.333M12.667 2A1.334 1.334 0 0 1 14 3.333M14 12.667A1.334 1.334 0 0 1 12.667 14M3.333 14A1.334 1.334 0 0 1 2 12.667M6 2h.667M6 14h.667M9.333 2H10M9.333 14H10M2 6v.667M14 6v.667M2 9.333V10M14 9.333V10M4.667 5.333H10M4.667 8h6.666M4.667 10.667h4"/></svg>

assets/icons/text_thread.svg 🔗

@@ -1,7 +1,7 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M7.33333 8H2" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M10.6667 5H2" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M9 11H2" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M12 7V11" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M14 9H10" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M7.33333 8H2" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M10.6667 5H2" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M9 11H2" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M12 7V11" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M14 9H10" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/thread.svg 🔗

@@ -1,3 +1,3 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M6.31254 12.549C7.3841 13.0987 8.61676 13.2476 9.78839 12.9688C10.96 12.6901 11.9936 12.0021 12.7028 11.0287C13.412 10.0554 13.7503 8.8607 13.6566 7.66002C13.5629 6.45934 13.0435 5.33159 12.1919 4.48C11.3403 3.62841 10.2126 3.10898 9.01188 3.01531C7.8112 2.92164 6.61655 3.2599 5.64319 3.96912C4.66984 4.67834 3.9818 5.71188 3.70306 6.88351C3.42432 8.05514 3.5732 9.2878 4.12289 10.3594L3 13.6719L6.31254 12.549Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M6.31254 12.549C7.3841 13.0987 8.61676 13.2476 9.78839 12.9688C10.96 12.6901 11.9936 12.0021 12.7028 11.0287C13.412 10.0554 13.7503 8.8607 13.6566 7.66002C13.5629 6.45934 13.0435 5.33159 12.1919 4.48C11.3403 3.62841 10.2126 3.10898 9.01188 3.01531C7.8112 2.92164 6.61655 3.2599 5.64319 3.96912C4.66984 4.67834 3.9818 5.71188 3.70306 6.88351C3.42432 8.05514 3.5732 9.2878 4.12289 10.3594L3 13.6719L6.31254 12.549Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/thread_from_summary.svg 🔗

@@ -1,6 +1,6 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M8.98795 10.4323C9.40771 10.9919 9.99294 11.4054 10.6607 11.614C11.3285 11.8226 12.045 11.8158 12.7087 11.5945C13.3724 11.3733 13.9497 10.9488 14.3588 10.3813C14.7678 9.81373 14.9879 9.13186 14.9879 8.43225C14.9879 7.6366 14.6719 6.87354 14.1093 6.31093C13.5467 5.74832 12.7836 5.43225 11.9879 5.43225C11.6685 5.43225 11.3595 5.47897 11.0677 5.56586C10.0571 5.86681 9.46945 6.84992 8.98796 7.78806V7.78806" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M8.01318 5.93652V8.60319H10.6799" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M8.01318 5.93652V8.60319H10.6799" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M7.00558 12.4263C6.16246 12.4494 5.3211 12.2612 4.56083 11.8712L1.24829 12.994L2.37119 9.68151C1.8215 8.60995 1.67261 7.37729 1.95135 6.20566C2.23009 5.03403 2.91813 4.00048 3.89148 3.29126C4.86484 2.58204 6.05949 2.24379 7.26018 2.33746C7.86645 2.38475 8.45413 2.54061 8.99705 2.79296" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M8.98795 10.4323C9.40771 10.9919 9.99294 11.4054 10.6607 11.614C11.3285 11.8226 12.045 11.8158 12.7087 11.5945C13.3724 11.3733 13.9497 10.9488 14.3588 10.3813C14.7678 9.81373 14.9879 9.13186 14.9879 8.43225C14.9879 7.6366 14.6719 6.87354 14.1093 6.31093C13.5467 5.74832 12.7836 5.43225 11.9879 5.43225C11.6685 5.43225 11.3595 5.47897 11.0677 5.56586C10.0571 5.86681 9.46945 6.84992 8.98796 7.78806V7.78806" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M8.01318 5.93652V8.60319H10.6799" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M8.01318 5.93652V8.60319H10.6799" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M7.00558 12.4263C6.16246 12.4494 5.3211 12.2612 4.56083 11.8712L1.24829 12.994L2.37119 9.68151C1.8215 8.60995 1.67261 7.37729 1.95135 6.20566C2.23009 5.03403 2.91813 4.00048 3.89148 3.29126C4.86484 2.58204 6.05949 2.24379 7.26018 2.33746C7.86645 2.38475 8.45413 2.54061 8.99705 2.79296" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/thumbs_down.svg 🔗

@@ -1,3 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-5">
-  <path d="M18.905 12.75a1.25 1.25 0 1 1-2.5 0v-7.5a1.25 1.25 0 0 1 2.5 0v7.5ZM8.905 17v1.3c0 .268-.14.526-.395.607A2 2 0 0 1 5.905 17c0-.995.182-1.948.514-2.826.204-.54-.166-1.174-.744-1.174h-2.52c-1.243 0-2.261-1.01-2.146-2.247.193-2.08.651-4.082 1.341-5.974C2.752 3.678 3.833 3 5.005 3h3.192a3 3 0 0 1 1.341.317l2.734 1.366A3 3 0 0 0 13.613 5h1.292v7h-.963c-.685 0-1.258.482-1.612 1.068a4.01 4.01 0 0 1-2.166 1.73c-.432.143-.853.386-1.011.814-.16.432-.248.9-.248 1.388Z" />
-</svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><g stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" clip-path="url(#a)"><path d="M3.013 8.57h2.478V3.2H3.013a.413.413 0 0 0-.413.413v4.544a.413.413 0 0 0 .413.413ZM5.491 8.57l2.066 4.13a1.652 1.652 0 0 0 1.652-1.652v-1.24h3.304a.826.826 0 0 0 .82-.929l-.62-4.956a.827.827 0 0 0-.82-.723H5.492"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

assets/icons/thumbs_up.svg 🔗

@@ -1,3 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-5">
-  <path d="M1 8.25a1.25 1.25 0 1 1 2.5 0v7.5a1.25 1.25 0 1 1-2.5 0v-7.5ZM11 3V1.7c0-.268.14-.526.395-.607A2 2 0 0 1 14 3c0 .995-.182 1.948-.514 2.826-.204.54.166 1.174.744 1.174h2.52c1.243 0 2.261 1.01 2.146 2.247a23.864 23.864 0 0 1-1.341 5.974C17.153 16.323 16.072 17 14.9 17h-3.192a3 3 0 0 1-1.341-.317l-2.734-1.366A3 3 0 0 0 6.292 15H5V8h.963c.685 0 1.258-.483 1.612-1.068a4.011 4.011 0 0 1 2.166-1.73c.432-.143.853-.386 1.011-.814.16-.432.248-.9.248-1.388Z" />
-</svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><g stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" clip-path="url(#a)"><path d="M3.013 7.33h2.478v5.37H3.013a.413.413 0 0 1-.413-.413V7.743a.413.413 0 0 1 .413-.413ZM5.491 7.33 7.557 3.2a1.652 1.652 0 0 1 1.652 1.652v1.24h3.304a.826.826 0 0 1 .82.929l-.62 4.956a.827.827 0 0 1-.82.723H5.492"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

assets/icons/todo_complete.svg 🔗

@@ -1,4 +1 @@
-<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M8 13C10.7614 13 13 10.7614 13 8C13 5.23858 10.7614 3 8 3C5.23858 3 3 5.23858 3 8C3 10.7614 5.23858 13 8 13Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M6 8L7.33333 9L10 7" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-</svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M8 13A5 5 0 1 0 8 3a5 5 0 0 0 0 10Z"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="m5.949 9.026 1.538 1.025 2.564-3.59"/></svg>

assets/icons/todo_pending.svg 🔗

@@ -1,10 +1,10 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M7 3C7.66045 3 8.33955 3 9 3" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M11 4C11.3949 4.26602 11.7345 4.60558 12 5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M13 7C13 7.66045 13 8.33955 13 9" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M12 11C11.734 11.3949 11.3944 11.7345 11 12" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M9 13C8.33954 13 7.66046 13 7 13" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M5 12C4.6051 11.734 4.26554 11.3944 4 11" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M3 9C3 8.33955 3 7.66045 3 7" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M4 5C4.26602 4.6051 4.60558 4.26554 5 4" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M7 3C7.66045 3 8.33955 3 9 3" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M11 4C11.3949 4.26602 11.7345 4.60558 12 5" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M13 7C13 7.66045 13 8.33955 13 9" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M12 11C11.734 11.3949 11.3944 11.7345 11 12" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M9 13C8.33954 13 7.66046 13 7 13" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M5 12C4.6051 11.734 4.26554 11.3944 4 11" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M3 9C3 8.33955 3 7.66045 3 7" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M4 5C4.26602 4.6051 4.60558 4.26554 5 4" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/todo_progress.svg 🔗

@@ -1,11 +1,11 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M7 3C7.66045 3 8.33955 3 9 3" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M11 4C11.3949 4.26602 11.7345 4.60558 12 5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M13 7C13 7.66045 13 8.33955 13 9" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M12 11C11.734 11.3949 11.3944 11.7345 11 12" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M9 13C8.33954 13 7.66046 13 7 13" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M5 12C4.6051 11.734 4.26554 11.3944 4 11" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M3 9C3 8.33955 3 7.66045 3 7" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M4 5C4.26602 4.6051 4.60558 4.26554 5 4" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M8.00016 8.66665C8.36835 8.66665 8.66683 8.36817 8.66683 7.99998C8.66683 7.63179 8.36835 7.33331 8.00016 7.33331C7.63197 7.33331 7.3335 7.63179 7.3335 7.99998C7.3335 8.36817 7.63197 8.66665 8.00016 8.66665Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M7 3C7.66045 3 8.33955 3 9 3" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M11 4C11.3949 4.26602 11.7345 4.60558 12 5" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M13 7C13 7.66045 13 8.33955 13 9" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M12 11C11.734 11.3949 11.3944 11.7345 11 12" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M9 13C8.33954 13 7.66046 13 7 13" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M5 12C4.6051 11.734 4.26554 11.3944 4 11" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M3 9C3 8.33955 3 7.66045 3 7" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M4 5C4.26602 4.6051 4.60558 4.26554 5 4" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M8.00016 8.66665C8.36835 8.66665 8.66683 8.36817 8.66683 7.99998C8.66683 7.63179 8.36835 7.33331 8.00016 7.33331C7.63197 7.33331 7.3335 7.63179 7.3335 7.99998C7.3335 8.36817 7.63197 8.66665 8.00016 8.66665Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/tool_copy.svg 🔗

@@ -1,4 +1,4 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M9.5 2.5H6.5C6.22386 2.5 6 2.83579 6 3.25V4.75C6 5.16421 6.22386 5.5 6.5 5.5H9.5C9.77614 5.5 10 5.16421 10 4.75V3.25C10 2.83579 9.77614 2.5 9.5 2.5Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M10 3.5H11C11.2652 3.5 11.5196 3.61706 11.7071 3.82544C11.8946 4.03381 12 4.31643 12 4.61111V12.3889C12 12.6836 11.8946 12.9662 11.7071 13.1746C11.5196 13.3829 11.2652 13.5 11 13.5H5C4.73478 13.5 4.48043 13.3829 4.29289 13.1746C4.10536 12.9662 4 12.6836 4 12.3889V4.61111C4 4.31643 4.10536 4.03381 4.29289 3.82544C4.48043 3.61706 4.73478 3.5 5 3.5H6" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M9.5 2.5H6.5C6.22386 2.5 6 2.83579 6 3.25V4.75C6 5.16421 6.22386 5.5 6.5 5.5H9.5C9.77614 5.5 10 5.16421 10 4.75V3.25C10 2.83579 9.77614 2.5 9.5 2.5Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M10 3.5H11C11.2652 3.5 11.5196 3.61706 11.7071 3.82544C11.8946 4.03381 12 4.31643 12 4.61111V12.3889C12 12.6836 11.8946 12.9662 11.7071 13.1746C11.5196 13.3829 11.2652 13.5 11 13.5H5C4.73478 13.5 4.48043 13.3829 4.29289 13.1746C4.10536 12.9662 4 12.6836 4 12.3889V4.61111C4 4.31643 4.10536 4.03381 4.29289 3.82544C4.48043 3.61706 4.73478 3.5 5 3.5H6" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/tool_delete_file.svg 🔗

@@ -1,5 +1,5 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M9.50002 2.5H5C4.73478 2.5 4.48043 2.6159 4.29289 2.82219C4.10535 3.02848 4 3.30826 4 3.6V12.3999C4 12.6917 4.10535 12.9715 4.29289 13.1778C4.48043 13.3841 4.73478 13.5 5 13.5H11C11.2652 13.5 11.5195 13.3841 11.7071 13.1778C11.8946 12.9715 12 12.6917 12 12.3999V5.25L9.50002 2.5Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M9.3427 6.82379L6.65698 9.5095" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M6.65698 6.82379L9.3427 9.5095" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M9.50002 2.5H5C4.73478 2.5 4.48043 2.6159 4.29289 2.82219C4.10535 3.02848 4 3.30826 4 3.6V12.3999C4 12.6917 4.10535 12.9715 4.29289 13.1778C4.48043 13.3841 4.73478 13.5 5 13.5H11C11.2652 13.5 11.5195 13.3841 11.7071 13.1778C11.8946 12.9715 12 12.6917 12 12.3999V5.25L9.50002 2.5Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M9.3427 6.82379L6.65698 9.5095" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M6.65698 6.82379L9.3427 9.5095" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/tool_diagnostics.svg 🔗

@@ -1,5 +1,5 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M13.7244 11.5299L9.01711 3.2922C8.91447 3.11109 8.76562 2.96045 8.58576 2.85564C8.4059 2.75084 8.20145 2.69562 7.99328 2.69562C7.7851 2.69562 7.58066 2.75084 7.40079 2.85564C7.22093 2.96045 7.07209 3.11109 6.96945 3.2922L2.26218 11.5299C2.15844 11.7096 2.10404 11.9135 2.1045 12.121C2.10495 12.3285 2.16026 12.5321 2.2648 12.7113C2.36934 12.8905 2.5194 13.0389 2.69978 13.1415C2.88015 13.244 3.08443 13.297 3.2919 13.2951H12.7064C12.9129 13.2949 13.1157 13.2404 13.2944 13.137C13.4731 13.0336 13.6215 12.8851 13.7247 12.7062C13.8278 12.5273 13.8821 12.3245 13.882 12.118C13.882 11.9115 13.8276 11.7087 13.7244 11.5299Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M7.99927 6.23425V8.58788" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M7.99927 10.9415H8.00492" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M13.7244 11.5299L9.01711 3.2922C8.91447 3.11109 8.76562 2.96045 8.58576 2.85564C8.4059 2.75084 8.20145 2.69562 7.99328 2.69562C7.7851 2.69562 7.58066 2.75084 7.40079 2.85564C7.22093 2.96045 7.07209 3.11109 6.96945 3.2922L2.26218 11.5299C2.15844 11.7096 2.10404 11.9135 2.1045 12.121C2.10495 12.3285 2.16026 12.5321 2.2648 12.7113C2.36934 12.8905 2.5194 13.0389 2.69978 13.1415C2.88015 13.244 3.08443 13.297 3.2919 13.2951H12.7064C12.9129 13.2949 13.1157 13.2404 13.2944 13.137C13.4731 13.0336 13.6215 12.8851 13.7247 12.7062C13.8278 12.5273 13.8821 12.3245 13.882 12.118C13.882 11.9115 13.8276 11.7087 13.7244 11.5299Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M7.99927 6.23425V8.58788" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M7.99927 10.9415H8.00492" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/tool_folder.svg 🔗

@@ -1,3 +1,3 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M12.4 12.5C12.6917 12.5 12.9715 12.3884 13.1778 12.1899C13.3841 11.9913 13.5 11.722 13.5 11.4412V6.14706C13.5 5.86624 13.3841 5.59693 13.1778 5.39836C12.9715 5.19979 12.6917 5.08824 12.4 5.08824H8.055C7.87103 5.08997 7.68955 5.04726 7.52717 4.96402C7.36478 4.88078 7.22668 4.75967 7.1255 4.61176L6.68 3.97647C6.57984 3.83007 6.44349 3.7099 6.28317 3.62674C6.12286 3.54358 5.94361 3.50003 5.7615 3.5H3.6C3.30826 3.5 3.02847 3.61155 2.82218 3.81012C2.61589 4.00869 2.5 4.27801 2.5 4.55882V11.4412C2.5 11.722 2.61589 11.9913 2.82218 12.1899C3.02847 12.3884 3.30826 12.5 3.6 12.5H12.4Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M12.8 13C13.1183 13 13.4235 12.8761 13.6486 12.6554C13.8735 12.4349 14 12.1356 14 11.8236V5.94118C14 5.62916 13.8735 5.32992 13.6486 5.10929C13.4235 4.88866 13.1183 4.76471 12.8 4.76471H8.06C7.8593 4.76664 7.66133 4.71919 7.48418 4.6267C7.30703 4.53421 7.15637 4.39964 7.046 4.2353L6.56 3.52941C6.45073 3.36675 6.30199 3.23322 6.1271 3.14082C5.95221 3.04842 5.75666 3.00004 5.558 3H3.2C2.88174 3 2.57651 3.12395 2.35148 3.34458C2.12643 3.56521 2 3.86445 2 4.17647V11.8236C2 12.1356 2.12643 12.4349 2.35148 12.6554C2.57651 12.8761 2.88174 13 3.2 13H12.8Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/tool_hammer.svg 🔗

@@ -1,5 +1,5 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M9 8.5L4.94864 12.6222C4.71647 12.8544 4.40157 12.9848 4.07323 12.9848C3.74488 12.9848 3.42999 12.8544 3.19781 12.6222C2.96564 12.39 2.83521 12.0751 2.83521 11.7468C2.83521 11.4185 2.96564 11.1036 3.19781 10.8714L7.5 6.5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M10.8352 9.98474L13.8352 6.98474" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M12.8352 7.42495L11.7634 6.4298C11.5533 6.23484 11.4353 5.97039 11.4352 5.69462V5.08526L10.1696 3.91022C9.54495 3.33059 8.69961 3.00261 7.81649 2.99722L5.83521 2.98474L6.35041 3.41108C6.71634 3.71233 7.00935 4.08216 7.21013 4.4962C7.4109 4.91024 7.51488 5.35909 7.51521 5.81316L7.5 6.5L9 8.5L9.5 8C9.5 8 9.87337 7.79457 10.0834 7.98959L11.1552 8.98474" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M9 8.5L4.94864 12.6222C4.71647 12.8544 4.40157 12.9848 4.07323 12.9848C3.74488 12.9848 3.42999 12.8544 3.19781 12.6222C2.96564 12.39 2.83521 12.0751 2.83521 11.7468C2.83521 11.4185 2.96564 11.1036 3.19781 10.8714L7.5 6.5" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M10.8352 9.98474L13.8352 6.98474" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M12.8352 7.42495L11.7634 6.4298C11.5533 6.23484 11.4353 5.97039 11.4352 5.69462V5.08526L10.1696 3.91022C9.54495 3.33059 8.69961 3.00261 7.81649 2.99722L5.83521 2.98474L6.35041 3.41108C6.71634 3.71233 7.00935 4.08216 7.21013 4.4962C7.4109 4.91024 7.51488 5.35909 7.51521 5.81316L7.5 6.5L9 8.5L9.5 8C9.5 8 9.87337 7.79457 10.0834 7.98959L11.1552 8.98474" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/tool_notification.svg 🔗

@@ -1,4 +1,4 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M6.5 12C6.65203 12.304 6.87068 12.5565 7.13399 12.7321C7.39729 12.9076 7.69597 13 8 13C8.30403 13 8.60271 12.9076 8.86601 12.7321C9.12932 12.5565 9.34797 12.304 9.5 12" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M3.63088 9.21874C3.56556 9.28556 3.52246 9.36865 3.50681 9.45791C3.49116 9.54718 3.50364 9.63876 3.54273 9.72152C3.58183 9.80429 3.64585 9.87467 3.72701 9.92409C3.80817 9.97352 3.90298 9.99987 3.99989 9.99994H12.0001C12.097 9.99997 12.1918 9.97372 12.273 9.92439C12.3542 9.87505 12.4183 9.80476 12.4575 9.72205C12.4967 9.63934 12.5093 9.54778 12.4938 9.45851C12.4783 9.36924 12.4353 9.2861 12.3701 9.21921C11.705 8.57941 11 7.89947 11 5.79994C11 5.05733 10.684 4.34514 10.1213 3.82004C9.55872 3.29494 8.79564 2.99994 7.99997 2.99994C7.20431 2.99994 6.44123 3.29494 5.87861 3.82004C5.31599 4.34514 4.99991 5.05733 4.99991 5.79994C4.99991 7.89947 4.2944 8.57941 3.63088 9.21874Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M6.5 12C6.65203 12.304 6.87068 12.5565 7.13399 12.7321C7.39729 12.9076 7.69597 13 8 13C8.30403 13 8.60271 12.9076 8.86601 12.7321C9.12932 12.5565 9.34797 12.304 9.5 12" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M3.63088 9.21874C3.56556 9.28556 3.52246 9.36865 3.50681 9.45791C3.49116 9.54718 3.50364 9.63876 3.54273 9.72152C3.58183 9.80429 3.64585 9.87467 3.72701 9.92409C3.80817 9.97352 3.90298 9.99987 3.99989 9.99994H12.0001C12.097 9.99997 12.1918 9.97372 12.273 9.92439C12.3542 9.87505 12.4183 9.80476 12.4575 9.72205C12.4967 9.63934 12.5093 9.54778 12.4938 9.45851C12.4783 9.36924 12.4353 9.2861 12.3701 9.21921C11.705 8.57941 11 7.89947 11 5.79994C11 5.05733 10.684 4.34514 10.1213 3.82004C9.55872 3.29494 8.79564 2.99994 7.99997 2.99994C7.20431 2.99994 6.44123 3.29494 5.87861 3.82004C5.31599 4.34514 4.99991 5.05733 4.99991 5.79994C4.99991 7.89947 4.2944 8.57941 3.63088 9.21874Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/tool_pencil.svg 🔗

@@ -1,4 +1,4 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M12.5871 5.40582C12.8514 5.14152 13 4.78304 13 4.40922C13 4.03541 12.8516 3.67688 12.5873 3.41252C12.323 3.14816 11.9645 2.99962 11.5907 2.99957C11.2169 2.99953 10.8584 3.14798 10.594 3.41227L3.92098 10.0869C3.80488 10.2027 3.71903 10.3452 3.67097 10.5019L3.01047 12.678C2.99754 12.7212 2.99657 12.7672 3.00764 12.8109C3.01872 12.8547 3.04143 12.8946 3.07337 12.9265C3.1053 12.9584 3.14528 12.981 3.18905 12.992C3.23282 13.003 3.27875 13.002 3.32197 12.989L5.49849 12.329C5.65508 12.2813 5.79758 12.196 5.91349 12.0805L12.5871 5.40582Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M9 5L11 7" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M12.5871 5.40582C12.8514 5.14152 13 4.78304 13 4.40922C13 4.03541 12.8516 3.67688 12.5873 3.41252C12.323 3.14816 11.9645 2.99962 11.5907 2.99957C11.2169 2.99953 10.8584 3.14798 10.594 3.41227L3.92098 10.0869C3.80488 10.2027 3.71903 10.3452 3.67097 10.5019L3.01047 12.678C2.99754 12.7212 2.99657 12.7672 3.00764 12.8109C3.01872 12.8547 3.04143 12.8946 3.07337 12.9265C3.1053 12.9584 3.14528 12.981 3.18905 12.992C3.23282 13.003 3.27875 13.002 3.32197 12.989L5.49849 12.329C5.65508 12.2813 5.79758 12.196 5.91349 12.0805L12.5871 5.40582Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M9 5L11 7" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/tool_read.svg 🔗

@@ -1,7 +1,7 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M3 5.66667V4.33333C3 3.97971 3.14048 3.64057 3.39052 3.39052C3.64057 3.14048 3.97971 3 4.33333 3H5.66667" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M10.3333 3H11.6666C12.0202 3 12.3593 3.14048 12.6094 3.39052C12.8594 3.64057 12.9999 3.97971 12.9999 4.33333V5.66667" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M12.9999 10.3333V11.6666C12.9999 12.0203 12.8594 12.3594 12.6094 12.6095C12.3593 12.8595 12.0202 13 11.6666 13H10.3333" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M5.66667 13H4.33333C3.97971 13 3.64057 12.8595 3.39052 12.6095C3.14048 12.3594 3 12.0203 3 11.6666V10.3333" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M5.5 8H10.5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M3 5.66667V4.33333C3 3.97971 3.14048 3.64057 3.39052 3.39052C3.64057 3.14048 3.97971 3 4.33333 3H5.66667" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M10.3333 3H11.6666C12.0202 3 12.3593 3.14048 12.6094 3.39052C12.8594 3.64057 12.9999 3.97971 12.9999 4.33333V5.66667" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M12.9999 10.3333V11.6666C12.9999 12.0203 12.8594 12.3594 12.6094 12.6095C12.3593 12.8595 12.0202 13 11.6666 13H10.3333" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M5.66667 13H4.33333C3.97971 13 3.64057 12.8595 3.39052 12.6095C3.14048 12.3594 3 12.0203 3 11.6666V10.3333" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M5.5 8H10.5" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/tool_regex.svg 🔗

@@ -1,4 +1,4 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
 <path d="M4.57132 13.7143C5.20251 13.7143 5.71418 13.2026 5.71418 12.5714C5.71418 11.9403 5.20251 11.4286 4.57132 11.4286C3.94014 11.4286 3.42847 11.9403 3.42847 12.5714C3.42847 13.2026 3.94014 13.7143 4.57132 13.7143Z" fill="black"/>
-<path d="M10.2856 2.85712V5.71426M10.2856 5.71426V8.5714M10.2856 5.71426H13.1428M10.2856 5.71426H7.42847M10.2856 5.71426L12.1904 3.80949M10.2856 5.71426L8.38084 7.61906M10.2856 5.71426L12.1904 7.61906M10.2856 5.71426L8.38084 3.80949" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
+<path d="M10.2856 2.85712V5.71426M10.2856 5.71426V8.5714M10.2856 5.71426H13.1428M10.2856 5.71426H7.42847M10.2856 5.71426L12.1904 3.80949M10.2856 5.71426L8.38084 7.61906M10.2856 5.71426L12.1904 7.61906M10.2856 5.71426L8.38084 3.80949" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
 </svg>

assets/icons/tool_search.svg 🔗

@@ -1,4 +1,4 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M13 13L11 11" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M7.5 12C9.98528 12 12 9.98528 12 7.5C12 5.01472 9.98528 3 7.5 3C5.01472 3 3 5.01472 3 7.5C3 9.98528 5.01472 12 7.5 12Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M13 13L11 11" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M7.5 12C9.98528 12 12 9.98528 12 7.5C12 5.01472 9.98528 3 7.5 3C5.01472 3 3 5.01472 3 7.5C3 9.98528 5.01472 12 7.5 12Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/tool_terminal.svg 🔗

@@ -1,5 +1,5 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M5.99487 8.44023L7.32821 7.10689L5.99487 5.77356" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M7.33838 10.2264H10.005" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M11.8889 3H4.11111C3.49746 3 3 3.49746 3 4.11111V11.8889C3 12.5025 3.49746 13 4.11111 13H11.8889C12.5025 13 13 12.5025 13 11.8889V4.11111C13 3.49746 12.5025 3 11.8889 3Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M12.2782 2.49951H3.72184C3.04677 2.49951 2.49951 3.04677 2.49951 3.72184V12.2782C2.49951 12.9532 3.04677 13.5005 3.72184 13.5005H12.2782C12.9532 13.5005 13.5005 12.9532 13.5005 12.2782V3.72184C13.5005 3.04677 12.9532 2.49951 12.2782 2.49951Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M8 10.7502H10.7502" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M5.24976 9.21777L7.08325 7.38428L5.24976 5.55078" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/tool_think.svg 🔗

@@ -1,3 +1,3 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M10.4174 10.2159C10.5454 9.58974 10.4174 9.57261 11.3762 8.46959C11.9337 7.82822 12.335 7.09214 12.335 6.27818C12.335 5.28184 11.9309 4.32631 11.2118 3.62179C10.4926 2.91728 9.5171 2.52148 8.50001 2.52148C7.48291 2.52148 6.50748 2.91728 5.78828 3.62179C5.06909 4.32631 4.66504 5.28184 4.66504 6.27818C4.66504 6.9043 4.79288 7.65565 5.62379 8.46959C6.58253 9.59098 6.45474 9.58974 6.58257 10.2159M10.4174 10.2159L10.4174 12.2989C10.4174 12.9504 9.87836 13.4786 9.21329 13.4786H7.78674C7.12167 13.4786 6.58253 12.9504 6.58253 12.2989L6.58257 10.2159M10.4174 10.2159H8.50001H6.58257" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M9.95231 10.2159C10.0803 9.58974 9.95231 9.57261 10.9111 8.46959C11.4686 7.82822 11.8699 7.09214 11.8699 6.27818C11.8699 5.28184 11.4658 4.32631 10.7467 3.62179C10.0275 2.91728 9.05201 2.52148 8.03492 2.52148C7.01782 2.52148 6.04239 2.91728 5.32319 3.62179C4.604 4.32631 4.19995 5.28184 4.19995 6.27818C4.19995 6.9043 4.32779 7.65565 5.1587 8.46959C6.11744 9.59098 5.98965 9.58974 6.11748 10.2159M9.95231 10.2159V12.2989C9.95231 12.9504 9.41327 13.4786 8.7482 13.4786H7.32165C6.65658 13.4786 6.11744 12.9504 6.11744 12.2989L6.11748 10.2159M9.95231 10.2159H8.03492H6.11748" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/tool_web.svg 🔗

@@ -1,5 +1,5 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M7.99993 13.4804C11.0267 13.4804 13.4803 11.0267 13.4803 7.99999C13.4803 4.97325 11.0267 2.51959 7.99993 2.51959C4.97319 2.51959 2.51953 4.97325 2.51953 7.99999C2.51953 11.0267 4.97319 13.4804 7.99993 13.4804Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M8 3C6.71611 4.34807 6 6.13836 6 7.99999C6 9.86163 6.71611 11.6519 8 13C9.28387 11.6519 10 9.86163 10 7.99999C10 6.13836 9.28387 4.34807 8 3Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M3.24121 7.04827C4.52425 8.27022 6.22817 8.95178 7.99999 8.95178C9.77182 8.95178 11.4757 8.27022 12.7588 7.04827" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M7.99993 13.4804C11.0267 13.4804 13.4803 11.0267 13.4803 7.99999C13.4803 4.97325 11.0267 2.51959 7.99993 2.51959C4.97319 2.51959 2.51953 4.97325 2.51953 7.99999C2.51953 11.0267 4.97319 13.4804 7.99993 13.4804Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M8 3C6.71611 4.34807 6 6.13836 6 7.99999C6 9.86163 6.71611 11.6519 8 13C9.28387 11.6519 10 9.86163 10 7.99999C10 6.13836 9.28387 4.34807 8 3Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M3.24121 7.04827C4.52425 8.27022 6.22817 8.95178 7.99999 8.95178C9.77182 8.95178 11.4757 8.27022 12.7588 7.04827" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/trash.svg 🔗

@@ -1,5 +1,5 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M3 5L13 5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M12 5V12.875C12 13.4375 11.4286 14 10.8571 14H5.14286C4.57143 14 4 13.4375 4 12.875V5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M10 5V3C10 2.44772 9.55228 2 9 2H7C6.44772 2 6 2.44772 6 3V5" stroke="black" stroke-width="1.5"/>
+<path d="M3 5L13 5" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M12 5V12.875C12 13.4375 11.4286 14 10.8571 14H5.14286C4.57143 14 4 13.4375 4 12.875V5" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M10 5V3C10 2.44772 9.55228 2 9 2H7C6.44772 2 6 2.44772 6 3V5" stroke="black" stroke-width="1.2"/>
 </svg>

assets/icons/triangle.svg 🔗

@@ -1,3 +1,3 @@
-<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M8.5 3L3 12H14L8.5 3Z" fill="black"/>
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M7.99996 3L2.82349 12H13.1764L7.99996 3Z" fill="black"/>
 </svg>

assets/icons/triangle_right.svg 🔗

@@ -1 +1,3 @@
-<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M6 11L6 4L10.5 7.5L6 11Z" fill="currentColor"></path></svg>
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M6 11.4667V4L10.8 7.73333L6 11.4667Z" fill="black"/>
+</svg>

assets/icons/undo.svg 🔗

@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-undo"><path d="M3 7v6h6"/><path d="M21 17a9 9 0 0 0-9-9 9 9 0 0 0-6 2.3L3 13"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M2.6 5v3.6h3.6"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M13.4 11A5.4 5.4 0 0 0 8 5.6a5.4 5.4 0 0 0-3.6 1.38L2.6 8.6"/></svg>

assets/icons/update.svg 🔗

@@ -1,8 +0,0 @@
-<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
-  <path
-    fill-rule="evenodd"
-    clip-rule="evenodd"

assets/icons/user_check.svg 🔗

@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-user-round-check-icon lucide-user-round-check"><path d="M2 21a8 8 0 0 1 13.292-6"/><circle cx="10" cy="8" r="5"/><path d="m16 19 2 2 4-4"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M2 13.4a4.8 4.8 0 0 1 7.975-3.6"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M6.8 8.6a3 3 0 1 0 0-6 3 3 0 0 0 0 6ZM10.4 12.2l1.2 1.2L14 11"/></svg>

assets/icons/user_group.svg 🔗

@@ -1,5 +1,5 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M6.79118 8.27005C8.27568 8.27005 9.4791 7.06663 9.4791 5.58214C9.4791 4.09765 8.27568 2.89423 6.79118 2.89423C5.30669 2.89423 4.10327 4.09765 4.10327 5.58214C4.10327 7.06663 5.30669 8.27005 6.79118 8.27005Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M6.79112 8.60443C4.19441 8.60443 2.08936 10.7095 2.08936 13.3062H11.4929C11.4929 10.7095 9.38784 8.60443 6.79112 8.60443Z" fill="black" fill-opacity="0.15" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M14.6984 12.9263C14.6984 10.8893 13.4895 8.99736 12.2806 8.09067C12.6779 7.79254 12.9957 7.40104 13.2057 6.95083C13.4157 6.50062 13.5115 6.00558 13.4846 5.50952C13.4577 5.01346 13.309 4.53168 13.0515 4.10681C12.7941 3.68194 12.4358 3.3271 12.0085 3.07367" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M6.79118 8.27005C8.27568 8.27005 9.4791 7.06663 9.4791 5.58214C9.4791 4.09765 8.27568 2.89423 6.79118 2.89423C5.30669 2.89423 4.10327 4.09765 4.10327 5.58214C4.10327 7.06663 5.30669 8.27005 6.79118 8.27005Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M6.79112 8.60443C4.19441 8.60443 2.08936 10.7095 2.08936 13.3062H11.4929C11.4929 10.7095 9.38784 8.60443 6.79112 8.60443Z" fill="black" fill-opacity="0.15" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M14.6984 12.9263C14.6984 10.8893 13.4895 8.99736 12.2806 8.09067C12.6779 7.79254 12.9957 7.40104 13.2057 6.95083C13.4157 6.50062 13.5115 6.00558 13.4846 5.50952C13.4577 5.01346 13.309 4.53168 13.0515 4.10681C12.7941 3.68194 12.4358 3.3271 12.0085 3.07367" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/user_round_pen.svg 🔗

@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-user-round-pen-icon lucide-user-round-pen"><path d="M2 21a8 8 0 0 1 10.821-7.487"/><path d="M21.378 16.626a1 1 0 0 0-3.004-3.004l-4.01 4.012a2 2 0 0 0-.506.854l-.837 2.87a.5.5 0 0 0 .62.62l2.87-.837a2 2 0 0 0 .854-.506z"/><circle cx="10" cy="8" r="5"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M2 13.433a4.8 4.8 0 0 1 6.493-4.492M13.627 10.809a1.275 1.275 0 0 0-1.803-1.803l-2.406 2.408a1.2 1.2 0 0 0-.303.512l-.502 1.722a.3.3 0 0 0 .372.372l1.722-.502c.193-.057.37-.161.512-.304l2.408-2.405Z"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M6.8 8.633a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z"/></svg>

assets/icons/visible.svg 🔗

@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-eye"><path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/></svg>

assets/icons/wand.svg 🔗

@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-wand"><path d="M15 4V2"/><path d="M15 16v-2"/><path d="M8 9h2"/><path d="M20 9h2"/><path d="M17.8 11.8 19 13"/><path d="M15 9h.01"/><path d="M17.8 6.2 19 5"/><path d="m3 21 9-9"/><path d="M12.2 6.2 11 5"/></svg>

assets/icons/warning.svg 🔗

@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-alert-triangle"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"/><path d="M12 9v4"/><path d="M12 17h.01"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M13.84 11.6 9.037 3.199a1.2 1.2 0 0 0-2.089 0l-4.802 8.403a1.2 1.2 0 0 0 1.05 1.8h9.604a1.201 1.201 0 0 0 1.038-1.8ZM8 6v2.667M8 11.333h.007"/></svg>

assets/icons/whole_word.svg 🔗

@@ -1,5 +1 @@
-<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path fill-rule="evenodd" clip-rule="evenodd" d="M4.74672 9.48686L4.07031 6.03232L3.38584 9.48686H2.17614L1.00281 4.00781H2.27559L2.81566 7.41754L3.48439 4.01752H4.65865L5.31819 7.41176L5.85736 4.00781H7.13014L5.9568 9.48686H4.74672Z" fill="#787D87"/>

assets/icons/x.svg 🔗

@@ -1,3 +0,0 @@
-<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M5 4.5L12 11.5M12 4.5L5 11.5" stroke="black" stroke-width="2" stroke-linecap="round"/>
-</svg>

assets/icons/x_circle.svg 🔗

@@ -1,4 +1 @@
-<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M8.86396 2C8.99657 2 9.12375 2.05268 9.21751 2.14645L11.8536 4.78249C11.9473 4.87625 12 5.00343 12 5.13604L12 8.86396C12 8.99657 11.9473 9.12375 11.8536 9.21751L9.21751 11.8536C9.12375 11.9473 8.99657 12 8.86396 12L5.13604 12C5.00343 12 4.87625 11.9473 4.78249 11.8536L2.14645 9.21751C2.05268 9.12375 2 8.99657 2 8.86396L2 5.13604C2 5.00343 2.05268 4.87625 2.14645 4.78249L4.78249 2.14645C4.87625 2.05268 5.00343 2 5.13604 2L8.86396 2Z" fill="#001A33" fill-opacity="0.157" stroke="#11181C" stroke-width="1.25" stroke-linejoin="round"/>
-<path d="M8.89063 5.10938L5.10937 8.89063M8.89063 8.89063L5.10937 5.10938" stroke="#11181C" stroke-width="1.25" stroke-linecap="round"/>
-</svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path fill="#000" fill-opacity=".157" stroke="#000" stroke-linejoin="round" stroke-width="1.2" d="M9.864 3a.5.5 0 0 1 .353.146l2.636 2.636a.5.5 0 0 1 .147.354v3.728a.5.5 0 0 1-.146.353l-2.637 2.637a.5.5 0 0 1-.353.146H6.136a.5.5 0 0 1-.354-.146l-2.636-2.636A.5.5 0 0 1 3 9.864V6.136a.5.5 0 0 1 .146-.354l2.636-2.636A.5.5 0 0 1 6.136 3h3.728Z"/><path stroke="#000" stroke-linecap="round" stroke-width="1.2" d="m9.5 6.5-3 3m3 0-3-3"/></svg>

assets/icons/x_circle_filled.svg 🔗

@@ -0,0 +1,3 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M8 2C11.3137 2 14 4.68629 14 8C14 11.3137 11.3137 14 8 14C4.68629 14 2 11.3137 2 8C2 4.68629 4.68629 2 8 2ZM10.4238 5.57617C10.1895 5.34187 9.81049 5.3419 9.57617 5.57617L8 7.15234L6.42383 5.57617C6.18953 5.34187 5.81049 5.3419 5.57617 5.57617C5.34186 5.81049 5.34186 6.18951 5.57617 6.42383L7.15234 8L5.57617 9.57617C5.34186 9.81049 5.34186 10.1895 5.57617 10.4238C5.81049 10.6581 6.18954 10.6581 6.42383 10.4238L8 8.84766L9.57617 10.4238C9.81049 10.6581 10.1895 10.6581 10.4238 10.4238C10.6581 10.1895 10.658 9.81048 10.4238 9.57617L8.84766 8L10.4238 6.42383C10.6581 6.18954 10.658 5.81048 10.4238 5.57617Z" fill="black"/>
+</svg>

assets/icons/zed_agent.svg 🔗

@@ -0,0 +1,27 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M11 8.75V10.5C8.93097 10.5 8.06903 10.5 6 10.5V10L11 6V5.5H6V7.25" stroke="black" stroke-width="1.5"/>
+<path d="M2 8.5C2.27614 8.5 2.5 8.27614 2.5 8C2.5 7.72386 2.27614 7.5 2 7.5C1.72386 7.5 1.5 7.72386 1.5 8C1.5 8.27614 1.72386 8.5 2 8.5Z" fill="black"/>
+<path opacity="0.6" d="M2.99976 6.33002C3.2759 6.33002 3.49976 6.10616 3.49976 5.83002C3.49976 5.55387 3.2759 5.33002 2.99976 5.33002C2.72361 5.33002 2.49976 5.55387 2.49976 5.83002C2.49976 6.10616 2.72361 6.33002 2.99976 6.33002Z" fill="black"/>
+<path opacity="0.6" d="M2.99976 10.66C3.2759 10.66 3.49976 10.4361 3.49976 10.16C3.49976 9.88383 3.2759 9.65997 2.99976 9.65997C2.72361 9.65997 2.49976 9.88383 2.49976 10.16C2.49976 10.4361 2.72361 10.66 2.99976 10.66Z" fill="black"/>
+<path d="M15 8.5C15.2761 8.5 15.5 8.27614 15.5 8C15.5 7.72386 15.2761 7.5 15 7.5C14.7239 7.5 14.5 7.72386 14.5 8C14.5 8.27614 14.7239 8.5 15 8.5Z" fill="black"/>
+<path opacity="0.6" d="M14 6.33002C14.2761 6.33002 14.5 6.10616 14.5 5.83002C14.5 5.55387 14.2761 5.33002 14 5.33002C13.7239 5.33002 13.5 5.55387 13.5 5.83002C13.5 6.10616 13.7239 6.33002 14 6.33002Z" fill="black"/>
+<path opacity="0.6" d="M14 10.66C14.2761 10.66 14.5 10.4361 14.5 10.16C14.5 9.88383 14.2761 9.65997 14 9.65997C13.7239 9.65997 13.5 9.88383 13.5 10.16C13.5 10.4361 13.7239 10.66 14 10.66Z" fill="black"/>
+<path d="M8.49219 2C8.76833 2 8.99219 1.77614 8.99219 1.5C8.99219 1.22386 8.76833 1 8.49219 1C8.21605 1 7.99219 1.22386 7.99219 1.5C7.99219 1.77614 8.21605 2 8.49219 2Z" fill="black"/>
+<path opacity="0.6" d="M6 3C6.27614 3 6.5 2.77614 6.5 2.5C6.5 2.22386 6.27614 2 6 2C5.72386 2 5.5 2.22386 5.5 2.5C5.5 2.77614 5.72386 3 6 3Z" fill="black"/>
+<path d="M4 4C4.27614 4 4.5 3.77614 4.5 3.5C4.5 3.22386 4.27614 3 4 3C3.72386 3 3.5 3.22386 3.5 3.5C3.5 3.77614 3.72386 4 4 4Z" fill="black"/>
+<path d="M3.99976 13C4.2759 13 4.49976 12.7761 4.49976 12.5C4.49976 12.2239 4.2759 12 3.99976 12C3.72361 12 3.49976 12.2239 3.49976 12.5C3.49976 12.7761 3.72361 13 3.99976 13Z" fill="black"/>
+<path opacity="0.2" d="M2 12.5C2.27614 12.5 2.5 12.2761 2.5 12C2.5 11.7239 2.27614 11.5 2 11.5C1.72386 11.5 1.5 11.7239 1.5 12C1.5 12.2761 1.72386 12.5 2 12.5Z" fill="black"/>
+<path opacity="0.2" d="M2 4.5C2.27614 4.5 2.5 4.27614 2.5 4C2.5 3.72386 2.27614 3.5 2 3.5C1.72386 3.5 1.5 3.72386 1.5 4C1.5 4.27614 1.72386 4.5 2 4.5Z" fill="black"/>
+<path opacity="0.2" d="M15 12.5C15.2761 12.5 15.5 12.2761 15.5 12C15.5 11.7239 15.2761 11.5 15 11.5C14.7239 11.5 14.5 11.7239 14.5 12C14.5 12.2761 14.7239 12.5 15 12.5Z" fill="black"/>
+<path opacity="0.2" d="M15 4.5C15.2761 4.5 15.5 4.27614 15.5 4C15.5 3.72386 15.2761 3.5 15 3.5C14.7239 3.5 14.5 3.72386 14.5 4C14.5 4.27614 14.7239 4.5 15 4.5Z" fill="black"/>
+<path opacity="0.5" d="M3.99976 15C4.2759 15 4.49976 14.7761 4.49976 14.5C4.49976 14.2239 4.2759 14 3.99976 14C3.72361 14 3.49976 14.2239 3.49976 14.5C3.49976 14.7761 3.72361 15 3.99976 15Z" fill="black"/>
+<path opacity="0.5" d="M4 2C4.27614 2 4.5 1.77614 4.5 1.5C4.5 1.22386 4.27614 1 4 1C3.72386 1 3.5 1.22386 3.5 1.5C3.5 1.77614 3.72386 2 4 2Z" fill="black"/>
+<path opacity="0.5" d="M13 15C13.2761 15 13.5 14.7761 13.5 14.5C13.5 14.2239 13.2761 14 13 14C12.7239 14 12.5 14.2239 12.5 14.5C12.5 14.7761 12.7239 15 13 15Z" fill="black"/>
+<path opacity="0.5" d="M13 2C13.2761 2 13.5 1.77614 13.5 1.5C13.5 1.22386 13.2761 1 13 1C12.7239 1 12.5 1.22386 12.5 1.5C12.5 1.77614 12.7239 2 13 2Z" fill="black"/>
+<path d="M13 4C13.2761 4 13.5 3.77614 13.5 3.5C13.5 3.22386 13.2761 3 13 3C12.7239 3 12.5 3.22386 12.5 3.5C12.5 3.77614 12.7239 4 13 4Z" fill="black"/>
+<path d="M13 13C13.2761 13 13.5 12.7761 13.5 12.5C13.5 12.2239 13.2761 12 13 12C12.7239 12 12.5 12.2239 12.5 12.5C12.5 12.7761 12.7239 13 13 13Z" fill="black"/>
+<path opacity="0.6" d="M11 3C11.2761 3 11.5 2.77614 11.5 2.5C11.5 2.22386 11.2761 2 11 2C10.7239 2 10.5 2.22386 10.5 2.5C10.5 2.77614 10.7239 3 11 3Z" fill="black"/>
+<path d="M8.5 15C8.77614 15 9 14.7761 9 14.5C9 14.2239 8.77614 14 8.5 14C8.22386 14 8 14.2239 8 14.5C8 14.7761 8.22386 15 8.5 15Z" fill="black"/>
+<path opacity="0.6" d="M6 14C6.27614 14 6.5 13.7761 6.5 13.5C6.5 13.2239 6.27614 13 6 13C5.72386 13 5.5 13.2239 5.5 13.5C5.5 13.7761 5.72386 14 6 14Z" fill="black"/>
+<path opacity="0.6" d="M11 14C11.2761 14 11.5 13.7761 11.5 13.5C11.5 13.2239 11.2761 13 11 13C10.7239 13 10.5 13.2239 10.5 13.5C10.5 13.7761 10.7239 14 11 14Z" fill="black"/>
+</svg>

assets/icons/zed_assistant.svg 🔗

@@ -1,5 +1,5 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M8 2.93652L6.9243 6.20697C6.86924 6.37435 6.77565 6.52646 6.65105 6.65105C6.52646 6.77565 6.37435 6.86924 6.20697 6.9243L2.93652 8L6.20697 9.0757C6.37435 9.13076 6.52646 9.22435 6.65105 9.34895C6.77565 9.47354 6.86924 9.62565 6.9243 9.79306L8 13.0635L9.0757 9.79306C9.13076 9.62565 9.22435 9.47354 9.34895 9.34895C9.47354 9.22435 9.62565 9.13076 9.79306 9.0757L13.0635 8L9.79306 6.9243C9.62565 6.86924 9.47354 6.77565 9.34895 6.65105C9.22435 6.52646 9.13076 6.37435 9.0757 6.20697L8 2.93652Z" fill="black" fill-opacity="0.15" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M3.33334 2V4.66666M2 3.33334H4.66666" stroke="black" stroke-opacity="0.75" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M12.6665 11.3333V14M11.3333 12.6666H13.9999" stroke="black" stroke-opacity="0.75" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M8 2.93652L6.9243 6.20697C6.86924 6.37435 6.77565 6.52646 6.65105 6.65105C6.52646 6.77565 6.37435 6.86924 6.20697 6.9243L2.93652 8L6.20697 9.0757C6.37435 9.13076 6.52646 9.22435 6.65105 9.34895C6.77565 9.47354 6.86924 9.62565 6.9243 9.79306L8 13.0635L9.0757 9.79306C9.13076 9.62565 9.22435 9.47354 9.34895 9.34895C9.47354 9.22435 9.62565 9.13076 9.79306 9.0757L13.0635 8L9.79306 6.9243C9.62565 6.86924 9.47354 6.77565 9.34895 6.65105C9.22435 6.52646 9.13076 6.37435 9.0757 6.20697L8 2.93652Z" fill="black" fill-opacity="0.15" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M3.33334 2V4.66666M2 3.33334H4.66666" stroke="black" stroke-opacity="0.75" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M12.6665 11.3333V14M11.3333 12.6666H13.9999" stroke="black" stroke-opacity="0.75" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/zed_assistant_filled.svg 🔗

@@ -1,5 +0,0 @@
-<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M7 1.75L5.88467 5.14092C5.82759 5.31446 5.73055 5.47218 5.60136 5.60136C5.47218 5.73055 5.31446 5.82759 5.14092 5.88467L1.75 7L5.14092 8.11533C5.31446 8.17241 5.47218 8.26945 5.60136 8.39864C5.73055 8.52782 5.82759 8.68554 5.88467 8.85908L7 12.25L8.11533 8.85908C8.17241 8.68554 8.26945 8.52782 8.39864 8.39864C8.52782 8.26945 8.68554 8.17241 8.85908 8.11533L12.25 7L8.85908 5.88467C8.68554 5.82759 8.52782 5.73055 8.39864 5.60136C8.26945 5.47218 8.17241 5.31446 8.11533 5.14092L7 1.75Z" fill="black" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M2.91667 1.75V4.08333M1.75 2.91667H4.08333" stroke="black" stroke-opacity="0.75" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M11.0833 9.91667V12.25M9.91667 11.0833H12.25" stroke="black" stroke-opacity="0.75" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
-</svg>

assets/icons/zed_burn_mode.svg 🔗

@@ -1,3 +1,3 @@
-<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M4.99207 8.14741C5.37246 8.14741 5.73726 7.9963 6.00623 7.72733C6.27521 7.45836 6.42631 7.09355 6.42631 6.71317C6.42631 5.92147 6.13946 5.56578 5.85262 4.99208C5.23761 3.76265 5.72411 2.66631 7.00001 1.5499C7.28686 2.98414 8.1474 4.36101 9.2948 5.27893C10.4422 6.19684 11.0159 7.28687 11.0159 8.43426C11.0159 8.96163 10.912 9.48384 10.7102 9.97107C10.5084 10.4583 10.2126 10.901 9.83967 11.2739C9.46676 11.6468 9.02405 11.9426 8.53682 12.1444C8.04959 12.3463 7.52738 12.4501 7.00001 12.4501C6.47264 12.4501 5.95043 12.3463 5.4632 12.1444C4.97597 11.9426 4.53326 11.6468 4.16035 11.2739C3.78745 10.901 3.49164 10.4583 3.28982 9.97107C3.088 9.48384 2.98413 8.96163 2.98413 8.43426C2.98413 7.77279 3.23254 7.1182 3.55783 6.71317C3.55783 7.09355 3.70894 7.45836 3.97791 7.72733C4.24688 7.9963 4.61169 8.14741 4.99207 8.14741Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M5.70519 9.31137C6.13992 9.31137 6.55683 9.13868 6.86423 8.83128C7.17163 8.52389 7.34432 8.10696 7.34432 7.67224C7.34432 6.76744 7.01649 6.36094 6.68868 5.70528C5.98581 4.30022 6.54181 3.04726 7.99998 1.77136C8.32781 3.41049 9.31128 4.98406 10.6226 6.03311C11.9339 7.08215 12.5896 8.3279 12.5896 9.6392C12.5896 10.2419 12.4708 10.8387 12.2402 11.3956C12.0096 11.9524 11.6715 12.4583 11.2453 12.8845C10.8191 13.3107 10.3132 13.6487 9.75633 13.8794C9.1995 14.1101 8.60269 14.2287 7.99998 14.2287C7.39727 14.2287 6.80046 14.1101 6.24362 13.8794C5.68679 13.6487 5.18083 13.3107 4.75465 12.8845C4.32848 12.4583 3.99041 11.9524 3.75976 11.3956C3.52911 10.8387 3.4104 10.2419 3.4104 9.6392C3.4104 8.88324 3.6943 8.13513 4.06606 7.67224C4.06606 8.10696 4.23875 8.52389 4.54615 8.83128C4.85354 9.13868 5.27047 9.31137 5.70519 9.31137Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/zed_burn_mode_on.svg 🔗

@@ -1,13 +1 @@
-<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
-<g clip-path="url(#clip0_2595_5640)">
-<path d="M4.99207 8.14741C5.37246 8.14741 5.73726 7.9963 6.00623 7.72733C6.27521 7.45836 6.42631 7.09355 6.42631 6.71317C6.42631 5.92147 6.13946 5.56578 5.85262 4.99208C5.23761 3.76265 5.72411 2.66631 7.00001 1.5499C7.28686 2.98414 8.1474 4.36101 9.2948 5.27893C10.4422 6.19684 11.0159 7.28687 11.0159 8.43426C11.0159 8.96163 10.912 9.48384 10.7102 9.97107C10.5084 10.4583 10.2126 10.901 9.83967 11.2739C9.46676 11.6468 9.02405 11.9426 8.53682 12.1444C8.04959 12.3463 7.52738 12.4501 7.00001 12.4501C6.47264 12.4501 5.95043 12.3463 5.4632 12.1444C4.97597 11.9426 4.53326 11.6468 4.16035 11.2739C3.78745 10.901 3.49164 10.4583 3.28982 9.97107C3.088 9.48384 2.98413 8.96163 2.98413 8.43426C2.98413 7.77279 3.23254 7.1182 3.55783 6.71317C3.55783 7.09355 3.70894 7.45836 3.97791 7.72733C4.24688 7.9963 4.61169 8.14741 4.99207 8.14741Z" fill="black" fill-opacity="0.5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M2 4C2.55228 4 3 3.55228 3 3C3 2.44772 2.55228 2 2 2C1.44772 2 1 2.44772 1 3C1 3.55228 1.44772 4 2 4Z" fill="black"/>
-<path d="M10 2C10.5523 2 11 1.55228 11 1C11 0.44772 10.5523 0 10 0C9.44772 0 9 0.44772 9 1C9 1.55228 9.44772 2 10 2Z" fill="black"/>
-<path d="M13 5C13.5522 5 14 4.55228 14 4C14 3.44772 13.5522 3 13 3C12.4478 3 12 3.44772 12 4C12 4.55228 12.4478 5 13 5Z" fill="black"/>
-</g>
-<defs>
-<clipPath id="clip0_2595_5640">
-<rect width="14" height="14" fill="white"/>
-</clipPath>
-</defs>
-</svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><g fill="#000" clip-path="url(#a)"><path fill-opacity=".5" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M5.705 9.311a1.64 1.64 0 0 0 1.64-1.639c0-.905-.328-1.311-.656-1.967C5.986 4.3 6.542 3.047 8 1.771c.328 1.64 1.312 3.213 2.623 4.262 1.311 1.05 1.967 2.295 1.967 3.606a4.59 4.59 0 1 1-9.18 0c0-.756.285-1.504.656-1.967a1.64 1.64 0 0 0 1.64 1.64Z"/><path d="M2.286 4.571a1.143 1.143 0 1 0 0-2.285 1.143 1.143 0 0 0 0 2.285ZM11.429 2.286a1.143 1.143 0 1 0 0-2.286 1.143 1.143 0 0 0 0 2.286ZM14.857 5.714a1.143 1.143 0 1 0 0-2.286 1.143 1.143 0 0 0 0 2.286Z"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

assets/icons/zed_mcp_custom.svg 🔗

@@ -1,4 +1,4 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<rect opacity="0.3" x="2" y="2" width="12" height="12" rx="2" stroke="black" stroke-width="1.5"/>
+<rect opacity="0.3" x="2" y="2" width="12" height="12" rx="2" stroke="black" stroke-width="1.2"/>

assets/icons/zed_mcp_extension.svg 🔗

@@ -1,4 +1,4 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<rect opacity="0.3" x="2" y="2" width="12" height="12" rx="2" stroke="black" stroke-width="1.5"/>
+<rect opacity="0.3" x="2" y="2" width="12" height="12" rx="2" stroke="black" stroke-width="1.2"/>

assets/icons/zed_predict.svg 🔗

@@ -1,5 +1,5 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M12 5L14 8L12 11" stroke="black" stroke-width="1.5"/>
-<path d="M10 6.5L11 8L10 9.5" stroke="black" stroke-width="1.5"/>
-<path d="M7.5 8.9V11C5.43097 11 4.56903 11 2.5 11V10.4L7.5 5.6V5H2.5V7.1" stroke="black" stroke-width="1.5"/>
+<path d="M12 5L14 8L12 11" stroke="black" stroke-width="1.2"/>
+<path d="M10 6.5L11 8L10 9.5" stroke="black" stroke-width="1.2"/>
+<path d="M7.5 8.9V11C5.43097 11 4.56903 11 2.5 11V10.4L7.5 5.6V5H2.5V7.1" stroke="black" stroke-width="1.2"/>
 </svg>

assets/icons/zed_predict_down.svg 🔗

@@ -1,5 +1,5 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M15 9.33333L14.5 9.66667L12.5 11L10.5 9.66667L10 9.33333" stroke="black" stroke-width="1.5"/>
-<path d="M12.5 11V4.5" stroke="black" stroke-width="1.5"/>
-<path d="M7.5 8.9V11C5.43097 11 4.56903 11 2.5 11V10.4L7.5 5.6V5H2.5V7.1" stroke="black" stroke-width="1.5"/>
+<path d="M15 9.33333L14.5 9.66667L12.5 11L10.5 9.66667L10 9.33333" stroke="black" stroke-width="1.2"/>
+<path d="M12.5 11V4.5" stroke="black" stroke-width="1.2"/>
+<path d="M7.5 8.9V11C5.43097 11 4.56903 11 2.5 11V10.4L7.5 5.6V5H2.5V7.1" stroke="black" stroke-width="1.2"/>
 </svg>

assets/icons/zed_predict_error.svg 🔗

@@ -1,4 +1,4 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path opacity="0.6" d="M7.5 8.9V11C5.43097 11 4.56903 11 2.5 11V10.4L7.5 5.6V5H2.5V7.1" stroke="black" stroke-width="1.5"/>
-<path d="M14 8L10 12M14 12L10 8" stroke="black" stroke-width="1.5"/>
+<path opacity="0.6" d="M7.5 8.9V11C5.43097 11 4.56903 11 2.5 11V10.4L7.5 5.6V5H2.5V7.1" stroke="black" stroke-width="1.2"/>
+<path d="M14 8L10 12M14 12L10 8" stroke="black" stroke-width="1.2"/>
 </svg>

assets/icons/zed_predict_up.svg 🔗

@@ -1,5 +1,5 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M10 6.66667L10.5 6.33333L12.5 5L14.5 6.33333L15 6.66667" stroke="black" stroke-width="1.5"/>
-<path d="M12.5 11V5" stroke="black" stroke-width="1.5"/>
-<path d="M7.5 8.9V11C5.43097 11 4.56903 11 2.5 11V10.4L7.5 5.6V5H2.5V7.1" stroke="black" stroke-width="1.5"/>
+<path d="M10 6.66667L10.5 6.33333L12.5 5L14.5 6.33333L15 6.66667" stroke="black" stroke-width="1.2"/>
+<path d="M12.5 11V5" stroke="black" stroke-width="1.2"/>
+<path d="M7.5 8.9V11C5.43097 11 4.56903 11 2.5 11V10.4L7.5 5.6V5H2.5V7.1" stroke="black" stroke-width="1.2"/>
 </svg>

assets/icons/zed_x_copilot.svg 🔗

@@ -1,14 +0,0 @@
-<svg width="93" height="32" viewBox="0 0 93 32" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path fill-rule="evenodd" clip-rule="evenodd" d="M9.03996 7.04962C8.00936 7.67635 7.30396 8.63219 7.30396 10.0149C7.30396 11.6908 7.72425 12.5893 8.2047 13.0744C8.68381 13.5581 9.40526 13.8149 10.4054 13.8149C11.815 13.8149 13.0291 13.5336 13.8802 12.9464C14.6756 12.3977 15.2708 11.5042 15.3438 9.96182C15.3991 8.79382 15.3678 8.01341 15.0568 7.45711C14.8094 7.01449 14.2326 6.47436 12.4901 6.27416C11.4684 6.15678 10.1114 6.39804 9.03996 7.04962ZM7.87312 5.13084C9.39147 4.2075 11.2531 3.87155 12.7464 4.04312C14.8843 4.28874 16.2844 5.05049 17.0171 6.36142C17.6863 7.55867 17.6384 8.98348 17.587 10.068C17.484 12.2439 16.5804 13.8118 15.1554 14.7949C13.7861 15.7396 12.0582 16.0606 10.4054 16.0606C9.04201 16.0606 7.65128 15.7069 6.60913 14.6547C5.56832 13.6038 5.05825 12.0408 5.05825 10.0149C5.05825 7.6958 6.3139 6.07903 7.87312 5.13084Z" fill="white"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M13.983 18.2811C14.6595 18.2811 15.2079 18.8295 15.2079 19.506V22.16C15.2079 22.8365 14.6595 23.385 13.983 23.385C13.3065 23.385 12.758 22.8365 12.758 22.16V19.506C12.758 18.8295 13.3065 18.2811 13.983 18.2811Z" fill="white"/>

assets/keymaps/default-linux.json 🔗

@@ -138,7 +138,7 @@
       "find": "buffer_search::Deploy",
       "ctrl-f": "buffer_search::Deploy",
       "ctrl-h": "buffer_search::DeployReplace",
-      "ctrl->": "assistant::QuoteSelection",
+      "ctrl->": "agent::QuoteSelection",
       "ctrl-<": "assistant::InsertIntoEditor",
       "ctrl-alt-e": "editor::SelectEnclosingSymbol",
       "ctrl-shift-backspace": "editor::GoToPreviousChange",
@@ -239,8 +239,9 @@
       "ctrl-shift-a": "agent::ToggleContextPicker",
       "ctrl-shift-j": "agent::ToggleNavigationMenu",
       "ctrl-shift-i": "agent::ToggleOptionsMenu",
+      "ctrl-alt-shift-n": "agent::ToggleNewThreadMenu",
       "shift-alt-escape": "agent::ExpandMessageEditor",
-      "ctrl->": "assistant::QuoteSelection",
+      "ctrl->": "agent::QuoteSelection",
       "ctrl-alt-e": "agent::RemoveAllContext",
       "ctrl-shift-e": "project_panel::ToggleFocus",
       "ctrl-shift-enter": "agent::ContinueThread",
@@ -326,12 +327,20 @@
     }
   },
   {
-    "context": "AcpThread > Editor",
+    "context": "AcpThread > Editor && !use_modifier_to_send",
     "use_key_equivalents": true,
     "bindings": {
       "enter": "agent::Chat",
-      "up": "agent::PreviousHistoryMessage",
-      "down": "agent::NextHistoryMessage",
+      "shift-ctrl-r": "agent::OpenAgentDiff",
+      "ctrl-shift-y": "agent::KeepAll",
+      "ctrl-shift-n": "agent::RejectAll"
+    }
+  },
+  {
+    "context": "AcpThread > Editor && use_modifier_to_send",
+    "use_key_equivalents": true,
+    "bindings": {
+      "ctrl-enter": "agent::Chat",
       "shift-ctrl-r": "agent::OpenAgentDiff",
       "ctrl-shift-y": "agent::KeepAll",
       "ctrl-shift-n": "agent::RejectAll"
@@ -1187,7 +1196,8 @@
       "ctrl-2": "onboarding::ActivateEditingPage",
       "ctrl-3": "onboarding::ActivateAISetupPage",
       "ctrl-escape": "onboarding::Finish",
-      "alt-tab": "onboarding::SignIn"
+      "alt-tab": "onboarding::SignIn",
+      "alt-shift-a": "onboarding::OpenAccount"
     }
   }
 ]

assets/keymaps/default-macos.json 🔗

@@ -162,7 +162,7 @@
       "cmd-alt-f": "buffer_search::DeployReplace",
       "cmd-alt-l": ["buffer_search::Deploy", { "selection_search_enabled": true }],
       "cmd-e": ["buffer_search::Deploy", { "focus": false }],
-      "cmd->": "assistant::QuoteSelection",
+      "cmd->": "agent::QuoteSelection",
       "cmd-<": "assistant::InsertIntoEditor",
       "cmd-alt-e": "editor::SelectEnclosingSymbol",
       "alt-enter": "editor::OpenSelectionsInMultibuffer"
@@ -279,8 +279,9 @@
       "cmd-shift-a": "agent::ToggleContextPicker",
       "cmd-shift-j": "agent::ToggleNavigationMenu",
       "cmd-shift-i": "agent::ToggleOptionsMenu",
+      "cmd-alt-shift-n": "agent::ToggleNewThreadMenu",
       "shift-alt-escape": "agent::ExpandMessageEditor",
-      "cmd->": "assistant::QuoteSelection",
+      "cmd->": "agent::QuoteSelection",
       "cmd-alt-e": "agent::RemoveAllContext",
       "cmd-shift-e": "project_panel::ToggleFocus",
       "cmd-ctrl-b": "agent::ToggleBurnMode",
@@ -378,12 +379,20 @@
     }
   },
   {
-    "context": "AcpThread > Editor",
+    "context": "AcpThread > Editor && !use_modifier_to_send",
     "use_key_equivalents": true,
     "bindings": {
       "enter": "agent::Chat",
-      "up": "agent::PreviousHistoryMessage",
-      "down": "agent::NextHistoryMessage",
+      "shift-ctrl-r": "agent::OpenAgentDiff",
+      "cmd-shift-y": "agent::KeepAll",
+      "cmd-shift-n": "agent::RejectAll"
+    }
+  },
+  {
+    "context": "AcpThread > Editor && use_modifier_to_send",
+    "use_key_equivalents": true,
+    "bindings": {
+      "cmd-enter": "agent::Chat",
       "shift-ctrl-r": "agent::OpenAgentDiff",
       "cmd-shift-y": "agent::KeepAll",
       "cmd-shift-n": "agent::RejectAll"
@@ -1289,7 +1298,8 @@
       "cmd-2": "onboarding::ActivateEditingPage",
       "cmd-3": "onboarding::ActivateAISetupPage",
       "cmd-escape": "onboarding::Finish",
-      "alt-tab": "onboarding::SignIn"
+      "alt-tab": "onboarding::SignIn",
+      "alt-shift-a": "onboarding::OpenAccount"
     }
   }
 ]

assets/keymaps/linux/cursor.json 🔗

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

assets/keymaps/macos/cursor.json 🔗

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

assets/keymaps/vim.json 🔗

@@ -58,6 +58,8 @@
       "[ space": "vim::InsertEmptyLineAbove",
       "[ e": "editor::MoveLineUp",
       "] e": "editor::MoveLineDown",
+      "[ f": "workspace::FollowNextCollaborator",
+      "] f": "workspace::FollowNextCollaborator",
 
       // Word motions
       "w": "vim::NextWordStart",
@@ -333,10 +335,14 @@
       "ctrl-x ctrl-c": "editor::ShowEditPrediction", // zed specific
       "ctrl-x ctrl-l": "editor::ToggleCodeActions", // zed specific
       "ctrl-x ctrl-z": "editor::Cancel",
+      "ctrl-x ctrl-e": "vim::LineDown",
+      "ctrl-x ctrl-y": "vim::LineUp",
       "ctrl-w": "editor::DeleteToPreviousWordStart",
       "ctrl-u": "editor::DeleteToBeginningOfLine",
       "ctrl-t": "vim::Indent",
       "ctrl-d": "vim::Outdent",
+      "ctrl-y": "vim::InsertFromAbove",
+      "ctrl-e": "vim::InsertFromBelow",
       "ctrl-k": ["vim::PushDigraph", {}],
       "ctrl-v": ["vim::PushLiteral", {}],
       "ctrl-shift-v": "editor::Paste", // note: this is *very* similar to ctrl-v in vim, but ctrl-shift-v on linux is the typical shortcut for paste when ctrl-v is already in use.
@@ -386,7 +392,7 @@
       "right": "vim::WrappingRight",
       "h": "vim::WrappingLeft",
       "l": "vim::WrappingRight",
-      "y": "editor::Copy",
+      "y": "vim::HelixYank",
       "alt-;": "vim::OtherEnd",
       "ctrl-r": "vim::Redo",
       "f": ["vim::PushFindForward", { "before": false, "multiline": true }],
@@ -403,6 +409,7 @@
       "g w": "vim::PushRewrap",
       "insert": "vim::InsertBefore",
       "alt-.": "vim::RepeatFind",
+      "alt-s": ["editor::SplitSelectionIntoLines", { "keep_selections": true }],
       // tree-sitter related commands
       "[ x": "editor::SelectLargerSyntaxNode",
       "] x": "editor::SelectSmallerSyntaxNode",

assets/settings/default.json 🔗

@@ -28,7 +28,9 @@
     "edit_prediction_provider": "zed"
   },
   // The name of a font to use for rendering text in the editor
-  "buffer_font_family": "Zed Plex Mono",
+  // ".ZedMono" currently aliases to Lilex
+  // but this may change in the future.
+  "buffer_font_family": ".ZedMono",
   // Set the buffer text's font fallbacks, this will be merged with
   // the platform's default fallbacks.
   "buffer_font_fallbacks": null,
@@ -54,7 +56,9 @@
   "buffer_line_height": "comfortable",
   // The name of a font to use for rendering text in the UI
   // You can set this to ".SystemUIFont" to use the system font
-  "ui_font_family": "Zed Plex Sans",
+  // ".ZedSans" currently aliases to "IBM Plex Sans", but this may
+  // change in the future
+  "ui_font_family": ".ZedSans",
   // Set the UI's font fallbacks, this will be merged with the platform's
   // default font fallbacks.
   "ui_font_fallbacks": null,
@@ -67,8 +71,8 @@
   "ui_font_weight": 400,
   // The default font size for text in the UI
   "ui_font_size": 16,
-  // The default font size for text in the agent panel
-  "agent_font_size": 16,
+  // The default font size for text in the agent panel. Falls back to the UI font size if unset.
+  "agent_font_size": null,
   // How much to fade out unused code.
   "unnecessary_code_fade": 0.3,
   // Active pane styling settings.
@@ -82,10 +86,10 @@
   // Layout mode of the bottom dock. Defaults to "contained"
   //   choices: contained, full, left_aligned, right_aligned
   "bottom_dock_layout": "contained",
-  // The direction that you want to split panes horizontally. Defaults to "up"
-  "pane_split_direction_horizontal": "up",
-  // The direction that you want to split panes vertically. Defaults to "left"
-  "pane_split_direction_vertical": "left",
+  // The direction that you want to split panes horizontally. Defaults to "down"
+  "pane_split_direction_horizontal": "down",
+  // The direction that you want to split panes vertically. Defaults to "right"
+  "pane_split_direction_vertical": "right",
   // Centered layout related settings.
   "centered_layout": {
     // The relative width of the left padding of the central pane from the
@@ -282,6 +286,8 @@
   // bracket, brace, single or double quote characters.
   // 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,
   // Whether indentation of pasted content should be adjusted based on the context.
   "auto_indent_on_paste": true,
   // Controls how the editor handles the autoclosed characters.
@@ -711,7 +717,7 @@
     // Can be 'never', 'always', or 'when_in_call',
     // or a boolean (interpreted as 'never'/'always').
     "button": "when_in_call",
-    // Where to the chat panel. Can be 'left' or 'right'.
+    // Where to dock the chat panel. Can be 'left' or 'right'.
     "dock": "right",
     // Default width of the chat panel.
     "default_width": 240
@@ -719,7 +725,7 @@
   "git_panel": {
     // Whether to show the git panel button in the status bar.
     "button": true,
-    // Where to show the git panel. Can be 'left' or 'right'.
+    // Where to dock the git panel. Can be 'left' or 'right'.
     "dock": "left",
     // Default width of the git panel.
     "default_width": 360,
@@ -883,11 +889,6 @@
   },
   // The settings for slash commands.
   "slash_commands": {
-    // Settings for the `/docs` slash command.
-    "docs": {
-      // Whether `/docs` is enabled.
-      "enabled": false
-    },
     // Settings for the `/project` slash command.
     "project": {
       // Whether `/project` is enabled.
@@ -1210,7 +1211,18 @@
     // Any addition to this list will be merged with the default list.
     // Globs are matched relative to the worktree root,
     // except when starting with a slash (/) or equivalent in Windows.
-    "disabled_globs": ["**/.env*", "**/*.pem", "**/*.key", "**/*.cert", "**/*.crt", "**/.dev.vars", "**/secrets.yml"],
+    "disabled_globs": [
+      "**/.env*",
+      "**/*.pem",
+      "**/*.key",
+      "**/*.cert",
+      "**/*.crt",
+      "**/.dev.vars",
+      "**/secrets.yml",
+      "**/.zed/settings.json", // zed project settings
+      "/**/zed/settings.json", // zed user settings
+      "/**/zed/keymap.json"
+    ],
     // When to show edit predictions previews in buffer.
     // This setting takes two possible values:
     // 1. Display predictions inline when there are no language server completions available.
@@ -1241,7 +1253,9 @@
   // Status bar-related settings.
   "status_bar": {
     // Whether to show the active language button in the status bar.
-    "active_language_button": true
+    "active_language_button": true,
+    // Whether to show the cursor position button in the status bar.
+    "cursor_position_button": true
   },
   // Settings specific to the terminal
   "terminal": {
@@ -1391,7 +1405,7 @@
     // "font_size": 15,
     // Set the terminal's font family. If this option is not included,
     // the terminal will default to matching the buffer's font family.
-    // "font_family": "Zed Plex Mono",
+    // "font_family": ".ZedMono",
     // Set the terminal's font fallbacks. If this option is not included,
     // the terminal will default to matching the buffer's font fallbacks.
     // This will be merged with the platform's default font fallbacks

assets/themes/one/one.json 🔗

@@ -86,9 +86,9 @@
         "terminal.ansi.blue": "#74ade8ff",
         "terminal.ansi.bright_blue": "#385378ff",
         "terminal.ansi.dim_blue": "#bed5f4ff",
-        "terminal.ansi.magenta": "#be5046ff",
-        "terminal.ansi.bright_magenta": "#5e2b26ff",
-        "terminal.ansi.dim_magenta": "#e6a79eff",
+        "terminal.ansi.magenta": "#b477cfff",
+        "terminal.ansi.bright_magenta": "#d6b4e4ff",
+        "terminal.ansi.dim_magenta": "#612a79ff",
         "terminal.ansi.cyan": "#6eb4bfff",
         "terminal.ansi.bright_cyan": "#3a565bff",
         "terminal.ansi.dim_cyan": "#b9d9dfff",

crates/acp_thread/Cargo.toml 🔗

@@ -13,27 +13,35 @@ path = "src/acp_thread.rs"
 doctest = false
 
 [features]
-test-support = ["gpui/test-support", "project/test-support"]
+test-support = ["gpui/test-support", "project/test-support", "dep:parking_lot"]
 
 [dependencies]
+action_log.workspace = true
 agent-client-protocol.workspace = true
 anyhow.workspace = true
-assistant_tool.workspace = true
 buffer_diff.workspace = true
+collections.workspace = true
 editor.workspace = true
+file_icons.workspace = true
 futures.workspace = true
 gpui.workspace = true
 itertools.workspace = true
 language.workspace = true
 language_model.workspace = true
 markdown.workspace = true
+parking_lot = { workspace = true, optional = true }
 project.workspace = true
+prompt_store.workspace = true
 serde.workspace = true
 serde_json.workspace = true
 settings.workspace = true
 smol.workspace = true
+terminal.workspace = true
 ui.workspace = true
+url.workspace = true
 util.workspace = true
+uuid.workspace = true
+watch.workspace = true
 workspace-hack.workspace = true
 
 [dev-dependencies]

crates/acp_thread/src/acp_thread.rs 🔗

@@ -1,86 +1,68 @@
 mod connection;
-pub use connection::*;
+mod diff;
+mod mention;
+mod terminal;
 
+use collections::HashSet;
+pub use connection::*;
+pub use diff::*;
+use language::language_settings::FormatOnSave;
+pub use mention::*;
+use project::lsp_store::{FormatTrigger, LspFormatTarget};
+use serde::{Deserialize, Serialize};
+pub use terminal::*;
+
+use action_log::ActionLog;
 use agent_client_protocol as acp;
-use anyhow::{Context as _, Result};
-use assistant_tool::ActionLog;
-use buffer_diff::BufferDiff;
-use editor::{Bias, MultiBuffer, PathKey};
+use anyhow::{Context as _, Result, anyhow};
+use editor::Bias;
 use futures::{FutureExt, channel::oneshot, future::BoxFuture};
-use gpui::{AppContext, Context, Entity, EventEmitter, SharedString, Task};
+use gpui::{AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task, WeakEntity};
 use itertools::Itertools;
-use language::{
-    Anchor, Buffer, BufferSnapshot, Capability, LanguageRegistry, OffsetRangeExt as _, Point,
-    text_diff,
-};
+use language::{Anchor, Buffer, BufferSnapshot, LanguageRegistry, Point, ToPoint, text_diff};
 use markdown::Markdown;
-use project::{AgentLocation, Project};
+use project::{AgentLocation, Project, git_store::GitStoreCheckpoint};
 use std::collections::HashMap;
 use std::error::Error;
-use std::fmt::Formatter;
+use std::fmt::{Formatter, Write};
+use std::ops::Range;
 use std::process::ExitStatus;
 use std::rc::Rc;
-use std::{
-    fmt::Display,
-    mem,
-    path::{Path, PathBuf},
-    sync::Arc,
-};
+use std::time::{Duration, Instant};
+use std::{fmt::Display, mem, path::PathBuf, sync::Arc};
 use ui::App;
 use util::ResultExt;
 
 #[derive(Debug)]
 pub struct UserMessage {
+    pub id: Option<UserMessageId>,
     pub content: ContentBlock,
-}
-
-impl UserMessage {
-    pub fn from_acp(
-        message: impl IntoIterator<Item = acp::ContentBlock>,
-        language_registry: Arc<LanguageRegistry>,
-        cx: &mut App,
-    ) -> Self {
-        let mut content = ContentBlock::Empty;
-        for chunk in message {
-            content.append(chunk, &language_registry, cx)
-        }
-        Self { content: content }
-    }
-
-    fn to_markdown(&self, cx: &App) -> String {
-        format!("## User\n\n{}\n\n", self.content.to_markdown(cx))
-    }
+    pub chunks: Vec<acp::ContentBlock>,
+    pub checkpoint: Option<Checkpoint>,
 }
 
 #[derive(Debug)]
-pub struct MentionPath<'a>(&'a Path);
-
-impl<'a> MentionPath<'a> {
-    const PREFIX: &'static str = "@file:";
-
-    pub fn new(path: &'a Path) -> Self {
-        MentionPath(path)
-    }
-
-    pub fn try_parse(url: &'a str) -> Option<Self> {
-        let path = url.strip_prefix(Self::PREFIX)?;
-        Some(MentionPath(Path::new(path)))
-    }
-
-    pub fn path(&self) -> &Path {
-        self.0
-    }
+pub struct Checkpoint {
+    git_checkpoint: GitStoreCheckpoint,
+    pub show: bool,
 }
 
-impl Display for MentionPath<'_> {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        write!(
-            f,
-            "[@{}]({}{})",
-            self.0.file_name().unwrap_or_default().display(),
-            Self::PREFIX,
-            self.0.display()
-        )
+impl UserMessage {
+    fn to_markdown(&self, cx: &App) -> String {
+        let mut markdown = String::new();
+        if self
+            .checkpoint
+            .as_ref()
+            .is_some_and(|checkpoint| checkpoint.show)
+        {
+            writeln!(markdown, "## User (checkpoint)").unwrap();
+        } else {
+            writeln!(markdown, "## User").unwrap();
+        }
+        writeln!(markdown).unwrap();
+        writeln!(markdown, "{}", self.content.to_markdown(cx)).unwrap();
+        writeln!(markdown).unwrap();
+        markdown
     }
 }
 
@@ -132,7 +114,7 @@ pub enum AgentThreadEntry {
 }
 
 impl AgentThreadEntry {
-    fn to_markdown(&self, cx: &App) -> String {
+    pub fn to_markdown(&self, cx: &App) -> String {
         match self {
             Self::UserMessage(message) => message.to_markdown(cx),
             Self::AssistantMessage(message) => message.to_markdown(cx),
@@ -140,7 +122,15 @@ impl AgentThreadEntry {
         }
     }
 
-    pub fn diffs(&self) -> impl Iterator<Item = &Diff> {
+    pub fn user_message(&self) -> Option<&UserMessage> {
+        if let AgentThreadEntry::UserMessage(message) = self {
+            Some(message)
+        } else {
+            None
+        }
+    }
+
+    pub fn diffs(&self) -> impl Iterator<Item = &Entity<Diff>> {
         if let AgentThreadEntry::ToolCall(call) = self {
             itertools::Either::Left(call.diffs())
         } else {
@@ -148,9 +138,25 @@ impl AgentThreadEntry {
         }
     }
 
-    pub fn locations(&self) -> Option<&[acp::ToolCallLocation]> {
-        if let AgentThreadEntry::ToolCall(ToolCall { locations, .. }) = self {
-            Some(locations)
+    pub fn terminals(&self) -> impl Iterator<Item = &Entity<Terminal>> {
+        if let AgentThreadEntry::ToolCall(call) = self {
+            itertools::Either::Left(call.terminals())
+        } else {
+            itertools::Either::Right(std::iter::empty())
+        }
+    }
+
+    pub fn location(&self, ix: usize) -> Option<(acp::ToolCallLocation, AgentLocation)> {
+        if let AgentThreadEntry::ToolCall(ToolCall {
+            locations,
+            resolved_locations,
+            ..
+        }) = self
+        {
+            Some((
+                locations.get(ix)?.clone(),
+                resolved_locations.get(ix)?.clone()?,
+            ))
         } else {
             None
         }
@@ -165,6 +171,7 @@ pub struct ToolCall {
     pub content: Vec<ToolCallContent>,
     pub status: ToolCallStatus,
     pub locations: Vec<acp::ToolCallLocation>,
+    pub resolved_locations: Vec<Option<AgentLocation>>,
     pub raw_input: Option<serde_json::Value>,
     pub raw_output: Option<serde_json::Value>,
 }
@@ -193,13 +200,14 @@ impl ToolCall {
                 .map(|content| ToolCallContent::from_acp(content, language_registry.clone(), cx))
                 .collect(),
             locations: tool_call.locations,
+            resolved_locations: Vec::default(),
             status,
             raw_input: tool_call.raw_input,
             raw_output: tool_call.raw_output,
         }
     }
 
-    fn update(
+    fn update_fields(
         &mut self,
         fields: acp::ToolCallUpdateFields,
         language_registry: Arc<LanguageRegistry>,
@@ -220,7 +228,7 @@ impl ToolCall {
         }
 
         if let Some(status) = status {
-            self.status = ToolCallStatus::Allowed { status };
+            self.status = status.into();
         }
 
         if let Some(title) = title {
@@ -245,14 +253,31 @@ impl ToolCall {
         }
 
         if let Some(raw_output) = raw_output {
+            if self.content.is_empty()
+                && let Some(markdown) = markdown_for_raw_output(&raw_output, &language_registry, cx)
+            {
+                self.content
+                    .push(ToolCallContent::ContentBlock(ContentBlock::Markdown {
+                        markdown,
+                    }));
+            }
             self.raw_output = Some(raw_output);
         }
     }
 
-    pub fn diffs(&self) -> impl Iterator<Item = &Diff> {
+    pub fn diffs(&self) -> impl Iterator<Item = &Entity<Diff>> {
+        self.content.iter().filter_map(|content| match content {
+            ToolCallContent::Diff(diff) => Some(diff),
+            ToolCallContent::ContentBlock(_) => None,
+            ToolCallContent::Terminal(_) => None,
+        })
+    }
+
+    pub fn terminals(&self) -> impl Iterator<Item = &Entity<Terminal>> {
         self.content.iter().filter_map(|content| match content {
-            ToolCallContent::ContentBlock { .. } => None,
-            ToolCallContent::Diff { diff } => Some(diff),
+            ToolCallContent::Terminal(terminal) => Some(terminal),
+            ToolCallContent::ContentBlock(_) => None,
+            ToolCallContent::Diff(_) => None,
         })
     }
 
@@ -268,34 +293,101 @@ impl ToolCall {
         }
         markdown
     }
+
+    async fn resolve_location(
+        location: acp::ToolCallLocation,
+        project: WeakEntity<Project>,
+        cx: &mut AsyncApp,
+    ) -> Option<AgentLocation> {
+        let buffer = project
+            .update(cx, |project, cx| {
+                project
+                    .project_path_for_absolute_path(&location.path, cx)
+                    .map(|path| project.open_buffer(path, cx))
+            })
+            .ok()??;
+        let buffer = buffer.await.log_err()?;
+        let position = buffer
+            .update(cx, |buffer, _| {
+                if let Some(row) = location.line {
+                    let snapshot = buffer.snapshot();
+                    let column = snapshot.indent_size_for_line(row).len;
+                    let point = snapshot.clip_point(Point::new(row, column), Bias::Left);
+                    snapshot.anchor_before(point)
+                } else {
+                    Anchor::MIN
+                }
+            })
+            .ok()?;
+
+        Some(AgentLocation {
+            buffer: buffer.downgrade(),
+            position,
+        })
+    }
+
+    fn resolve_locations(
+        &self,
+        project: Entity<Project>,
+        cx: &mut App,
+    ) -> Task<Vec<Option<AgentLocation>>> {
+        let locations = self.locations.clone();
+        project.update(cx, |_, cx| {
+            cx.spawn(async move |project, cx| {
+                let mut new_locations = Vec::new();
+                for location in locations {
+                    new_locations.push(Self::resolve_location(location, project.clone(), cx).await);
+                }
+                new_locations
+            })
+        })
+    }
 }
 
 #[derive(Debug)]
 pub enum ToolCallStatus {
+    /// The tool call hasn't started running yet, but we start showing it to
+    /// the user.
+    Pending,
+    /// The tool call is waiting for confirmation from the user.
     WaitingForConfirmation {
         options: Vec<acp::PermissionOption>,
         respond_tx: oneshot::Sender<acp::PermissionOptionId>,
     },
-    Allowed {
-        status: acp::ToolCallStatus,
-    },
+    /// The tool call is currently running.
+    InProgress,
+    /// The tool call completed successfully.
+    Completed,
+    /// The tool call failed.
+    Failed,
+    /// The user rejected the tool call.
     Rejected,
+    /// The user canceled generation so the tool call was canceled.
     Canceled,
 }
 
+impl From<acp::ToolCallStatus> for ToolCallStatus {
+    fn from(status: acp::ToolCallStatus) -> Self {
+        match status {
+            acp::ToolCallStatus::Pending => Self::Pending,
+            acp::ToolCallStatus::InProgress => Self::InProgress,
+            acp::ToolCallStatus::Completed => Self::Completed,
+            acp::ToolCallStatus::Failed => Self::Failed,
+        }
+    }
+}
+
 impl Display for ToolCallStatus {
     fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
         write!(
             f,
             "{}",
             match self {
+                ToolCallStatus::Pending => "Pending",
                 ToolCallStatus::WaitingForConfirmation { .. } => "Waiting for confirmation",
-                ToolCallStatus::Allowed { status } => match status {
-                    acp::ToolCallStatus::Pending => "Pending",
-                    acp::ToolCallStatus::InProgress => "In Progress",
-                    acp::ToolCallStatus::Completed => "Completed",
-                    acp::ToolCallStatus::Failed => "Failed",
-                },
+                ToolCallStatus::InProgress => "In Progress",
+                ToolCallStatus::Completed => "Completed",
+                ToolCallStatus::Failed => "Failed",
                 ToolCallStatus::Rejected => "Rejected",
                 ToolCallStatus::Canceled => "Canceled",
             }
@@ -307,6 +399,7 @@ impl Display for ToolCallStatus {
 pub enum ContentBlock {
     Empty,
     Markdown { markdown: Entity<Markdown> },
+    ResourceLink { resource_link: acp::ResourceLink },
 }
 
 impl ContentBlock {
@@ -338,43 +431,78 @@ impl ContentBlock {
         language_registry: &Arc<LanguageRegistry>,
         cx: &mut App,
     ) {
-        let new_content = match block {
-            acp::ContentBlock::Text(text_content) => text_content.text.clone(),
-            acp::ContentBlock::ResourceLink(resource_link) => {
-                if let Some(path) = resource_link.uri.strip_prefix("file://") {
-                    format!("{}", MentionPath(path.as_ref()))
-                } else {
-                    resource_link.uri.clone()
-                }
-            }
-            acp::ContentBlock::Image(_)
-            | acp::ContentBlock::Audio(_)
-            | acp::ContentBlock::Resource(_) => String::new(),
-        };
+        if matches!(self, ContentBlock::Empty)
+            && let acp::ContentBlock::ResourceLink(resource_link) = block
+        {
+            *self = ContentBlock::ResourceLink { resource_link };
+            return;
+        }
+
+        let new_content = self.block_string_contents(block);
 
         match self {
             ContentBlock::Empty => {
-                *self = ContentBlock::Markdown {
-                    markdown: cx.new(|cx| {
-                        Markdown::new(
-                            new_content.into(),
-                            Some(language_registry.clone()),
-                            None,
-                            cx,
-                        )
-                    }),
-                };
+                *self = Self::create_markdown_block(new_content, language_registry, cx);
             }
             ContentBlock::Markdown { markdown } => {
                 markdown.update(cx, |markdown, cx| markdown.append(&new_content, cx));
             }
+            ContentBlock::ResourceLink { resource_link } => {
+                let existing_content = Self::resource_link_md(&resource_link.uri);
+                let combined = format!("{}\n{}", existing_content, new_content);
+
+                *self = Self::create_markdown_block(combined, language_registry, cx);
+            }
+        }
+    }
+
+    fn create_markdown_block(
+        content: String,
+        language_registry: &Arc<LanguageRegistry>,
+        cx: &mut App,
+    ) -> ContentBlock {
+        ContentBlock::Markdown {
+            markdown: cx
+                .new(|cx| Markdown::new(content.into(), Some(language_registry.clone()), None, cx)),
+        }
+    }
+
+    fn block_string_contents(&self, block: acp::ContentBlock) -> String {
+        match block {
+            acp::ContentBlock::Text(text_content) => text_content.text,
+            acp::ContentBlock::ResourceLink(resource_link) => {
+                Self::resource_link_md(&resource_link.uri)
+            }
+            acp::ContentBlock::Resource(acp::EmbeddedResource {
+                resource:
+                    acp::EmbeddedResourceResource::TextResourceContents(acp::TextResourceContents {
+                        uri,
+                        ..
+                    }),
+                ..
+            }) => Self::resource_link_md(&uri),
+            acp::ContentBlock::Image(image) => Self::image_md(&image),
+            acp::ContentBlock::Audio(_) | acp::ContentBlock::Resource(_) => String::new(),
+        }
+    }
+
+    fn resource_link_md(uri: &str) -> String {
+        if let Some(uri) = MentionUri::parse(uri).log_err() {
+            uri.as_link().to_string()
+        } else {
+            uri.to_string()
         }
     }
 
+    fn image_md(_image: &acp::ImageContent) -> String {
+        "`Image`".into()
+    }
+
     fn to_markdown<'a>(&'a self, cx: &'a App) -> &'a str {
         match self {
             ContentBlock::Empty => "",
             ContentBlock::Markdown { markdown } => markdown.read(cx).source(),
+            ContentBlock::ResourceLink { resource_link } => &resource_link.uri,
         }
     }
 
@@ -382,14 +510,23 @@ impl ContentBlock {
         match self {
             ContentBlock::Empty => None,
             ContentBlock::Markdown { markdown } => Some(markdown),
+            ContentBlock::ResourceLink { .. } => None,
+        }
+    }
+
+    pub fn resource_link(&self) -> Option<&acp::ResourceLink> {
+        match self {
+            ContentBlock::ResourceLink { resource_link } => Some(resource_link),
+            _ => None,
         }
     }
 }
 
 #[derive(Debug)]
 pub enum ToolCallContent {
-    ContentBlock { content: ContentBlock },
-    Diff { diff: Diff },
+    ContentBlock(ContentBlock),
+    Diff(Entity<Diff>),
+    Terminal(Entity<Terminal>),
 }
 
 impl ToolCallContent {
@@ -399,118 +536,75 @@ impl ToolCallContent {
         cx: &mut App,
     ) -> Self {
         match content {
-            acp::ToolCallContent::Content { content } => Self::ContentBlock {
-                content: ContentBlock::new(content, &language_registry, cx),
-            },
-            acp::ToolCallContent::Diff { diff } => Self::Diff {
-                diff: Diff::from_acp(diff, language_registry, cx),
-            },
+            acp::ToolCallContent::Content { content } => {
+                Self::ContentBlock(ContentBlock::new(content, &language_registry, cx))
+            }
+            acp::ToolCallContent::Diff { diff } => Self::Diff(cx.new(|cx| {
+                Diff::finalized(
+                    diff.path,
+                    diff.old_text,
+                    diff.new_text,
+                    language_registry,
+                    cx,
+                )
+            })),
         }
     }
 
     pub fn to_markdown(&self, cx: &App) -> String {
         match self {
-            Self::ContentBlock { content } => content.to_markdown(cx).to_string(),
-            Self::Diff { diff } => diff.to_markdown(cx),
+            Self::ContentBlock(content) => content.to_markdown(cx).to_string(),
+            Self::Diff(diff) => diff.read(cx).to_markdown(cx),
+            Self::Terminal(terminal) => terminal.read(cx).to_markdown(cx),
         }
     }
 }
 
-#[derive(Debug)]
-pub struct Diff {
-    pub multibuffer: Entity<MultiBuffer>,
-    pub path: PathBuf,
-    _task: Task<Result<()>>,
+#[derive(Debug, PartialEq)]
+pub enum ToolCallUpdate {
+    UpdateFields(acp::ToolCallUpdate),
+    UpdateDiff(ToolCallUpdateDiff),
+    UpdateTerminal(ToolCallUpdateTerminal),
 }
 
-impl Diff {
-    pub fn from_acp(
-        diff: acp::Diff,
-        language_registry: Arc<LanguageRegistry>,
-        cx: &mut App,
-    ) -> Self {
-        let acp::Diff {
-            path,
-            old_text,
-            new_text,
-        } = diff;
-
-        let multibuffer = cx.new(|_cx| MultiBuffer::without_headers(Capability::ReadOnly));
-
-        let new_buffer = cx.new(|cx| Buffer::local(new_text, cx));
-        let old_buffer = cx.new(|cx| Buffer::local(old_text.unwrap_or("".into()), cx));
-        let new_buffer_snapshot = new_buffer.read(cx).text_snapshot();
-        let buffer_diff = cx.new(|cx| BufferDiff::new(&new_buffer_snapshot, cx));
-
-        let task = cx.spawn({
-            let multibuffer = multibuffer.clone();
-            let path = path.clone();
-            async move |cx| {
-                let language = language_registry
-                    .language_for_file_path(&path)
-                    .await
-                    .log_err();
-
-                new_buffer.update(cx, |buffer, cx| buffer.set_language(language.clone(), cx))?;
-
-                let old_buffer_snapshot = old_buffer.update(cx, |buffer, cx| {
-                    buffer.set_language(language, cx);
-                    buffer.snapshot()
-                })?;
+impl ToolCallUpdate {
+    fn id(&self) -> &acp::ToolCallId {
+        match self {
+            Self::UpdateFields(update) => &update.id,
+            Self::UpdateDiff(diff) => &diff.id,
+            Self::UpdateTerminal(terminal) => &terminal.id,
+        }
+    }
+}
 
-                buffer_diff
-                    .update(cx, |diff, cx| {
-                        diff.set_base_text(
-                            old_buffer_snapshot,
-                            Some(language_registry),
-                            new_buffer_snapshot,
-                            cx,
-                        )
-                    })?
-                    .await?;
+impl From<acp::ToolCallUpdate> for ToolCallUpdate {
+    fn from(update: acp::ToolCallUpdate) -> Self {
+        Self::UpdateFields(update)
+    }
+}
 
-                multibuffer
-                    .update(cx, |multibuffer, cx| {
-                        let hunk_ranges = {
-                            let buffer = new_buffer.read(cx);
-                            let diff = buffer_diff.read(cx);
-                            diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer, cx)
-                                .map(|diff_hunk| diff_hunk.buffer_range.to_point(&buffer))
-                                .collect::<Vec<_>>()
-                        };
-
-                        multibuffer.set_excerpts_for_path(
-                            PathKey::for_buffer(&new_buffer, cx),
-                            new_buffer.clone(),
-                            hunk_ranges,
-                            editor::DEFAULT_MULTIBUFFER_CONTEXT,
-                            cx,
-                        );
-                        multibuffer.add_diff(buffer_diff, cx);
-                    })
-                    .log_err();
+impl From<ToolCallUpdateDiff> for ToolCallUpdate {
+    fn from(diff: ToolCallUpdateDiff) -> Self {
+        Self::UpdateDiff(diff)
+    }
+}
 
-                anyhow::Ok(())
-            }
-        });
+#[derive(Debug, PartialEq)]
+pub struct ToolCallUpdateDiff {
+    pub id: acp::ToolCallId,
+    pub diff: Entity<Diff>,
+}
 
-        Self {
-            multibuffer,
-            path,
-            _task: task,
-        }
+impl From<ToolCallUpdateTerminal> for ToolCallUpdate {
+    fn from(terminal: ToolCallUpdateTerminal) -> Self {
+        Self::UpdateTerminal(terminal)
     }
+}
 
-    fn to_markdown(&self, cx: &App) -> String {
-        let buffer_text = self
-            .multibuffer
-            .read(cx)
-            .all_buffers()
-            .iter()
-            .map(|buffer| buffer.read(cx).text())
-            .join("\n");
-        format!("Diff: {}\n```\n{}\n```\n", self.path.display(), buffer_text)
-    }
+#[derive(Debug, PartialEq)]
+pub struct ToolCallUpdateTerminal {
+    pub id: acp::ToolCallId,
+    pub terminal: Entity<Terminal>,
 }
 
 #[derive(Debug, Default)]
@@ -572,6 +666,52 @@ impl PlanEntry {
     }
 }
 
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct TokenUsage {
+    pub max_tokens: u64,
+    pub used_tokens: u64,
+}
+
+impl TokenUsage {
+    pub fn ratio(&self) -> TokenUsageRatio {
+        #[cfg(debug_assertions)]
+        let warning_threshold: f32 = std::env::var("ZED_THREAD_WARNING_THRESHOLD")
+            .unwrap_or("0.8".to_string())
+            .parse()
+            .unwrap();
+        #[cfg(not(debug_assertions))]
+        let warning_threshold: f32 = 0.8;
+
+        // When the maximum is unknown because there is no selected model,
+        // avoid showing the token limit warning.
+        if self.max_tokens == 0 {
+            TokenUsageRatio::Normal
+        } else if self.used_tokens >= self.max_tokens {
+            TokenUsageRatio::Exceeded
+        } else if self.used_tokens as f32 / self.max_tokens as f32 >= warning_threshold {
+            TokenUsageRatio::Warning
+        } else {
+            TokenUsageRatio::Normal
+        }
+    }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum TokenUsageRatio {
+    Normal,
+    Warning,
+    Exceeded,
+}
+
+#[derive(Debug, Clone)]
+pub struct RetryStatus {
+    pub last_error: SharedString,
+    pub attempt: usize,
+    pub max_attempts: usize,
+    pub started_at: Instant,
+    pub duration: Duration,
+}
+
 pub struct AcpThread {
     title: SharedString,
     entries: Vec<AgentThreadEntry>,
@@ -582,15 +722,21 @@ pub struct AcpThread {
     send_task: Option<Task<()>>,
     connection: Rc<dyn AgentConnection>,
     session_id: acp::SessionId,
+    token_usage: Option<TokenUsage>,
 }
 
+#[derive(Debug)]
 pub enum AcpThreadEvent {
     NewEntry,
+    TitleUpdated,
+    TokenUsageUpdated,
     EntryUpdated(usize),
+    EntriesRemoved(Range<usize>),
     ToolAuthorizationRequired,
+    Retry(RetryStatus),
     Stopped,
     Error,
-    ServerExited(ExitStatus),
+    LoadError(LoadError),
 }
 
 impl EventEmitter<AcpThreadEvent> for AcpThread {}
@@ -604,20 +750,30 @@ pub enum ThreadStatus {
 
 #[derive(Debug, Clone)]
 pub enum LoadError {
+    NotInstalled {
+        error_message: SharedString,
+        install_message: SharedString,
+        install_command: String,
+    },
     Unsupported {
         error_message: SharedString,
         upgrade_message: SharedString,
         upgrade_command: String,
     },
-    Exited(i32),
+    Exited {
+        status: ExitStatus,
+    },
     Other(SharedString),
 }
 
 impl Display for LoadError {
     fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
         match self {
-            LoadError::Unsupported { error_message, .. } => write!(f, "{}", error_message),
-            LoadError::Exited(status) => write!(f, "Server exited with status {}", status),
+            LoadError::NotInstalled { error_message, .. }
+            | LoadError::Unsupported { error_message, .. } => {
+                write!(f, "{error_message}")
+            }
+            LoadError::Exited { status } => write!(f, "Server exited with status {status}"),
             LoadError::Other(msg) => write!(f, "{}", msg),
         }
     }
@@ -630,11 +786,9 @@ impl AcpThread {
         title: impl Into<SharedString>,
         connection: Rc<dyn AgentConnection>,
         project: Entity<Project>,
+        action_log: Entity<ActionLog>,
         session_id: acp::SessionId,
-        cx: &mut Context<Self>,
     ) -> Self {
-        let action_log = cx.new(|_| ActionLog::new(project.clone()));
-
         Self {
             action_log,
             shared_buffers: Default::default(),
@@ -645,9 +799,14 @@ impl AcpThread {
             send_task: None,
             connection,
             session_id,
+            token_usage: None,
         }
     }
 
+    pub fn connection(&self) -> &Rc<dyn AgentConnection> {
+        &self.connection
+    }
+
     pub fn action_log(&self) -> &Entity<ActionLog> {
         &self.action_log
     }
@@ -680,17 +839,17 @@ impl AcpThread {
         }
     }
 
+    pub fn token_usage(&self) -> Option<&TokenUsage> {
+        self.token_usage.as_ref()
+    }
+
     pub fn has_pending_edit_tool_calls(&self) -> bool {
         for entry in self.entries.iter().rev() {
             match entry {
                 AgentThreadEntry::UserMessage(_) => return false,
                 AgentThreadEntry::ToolCall(
                     call @ ToolCall {
-                        status:
-                            ToolCallStatus::Allowed {
-                                status:
-                                    acp::ToolCallStatus::InProgress | acp::ToolCallStatus::Pending,
-                            },
+                        status: ToolCallStatus::InProgress | ToolCallStatus::Pending,
                         ..
                     },
                 ) if call.diffs().next().is_some() => {
@@ -719,10 +878,10 @@ impl AcpThread {
         &mut self,
         update: acp::SessionUpdate,
         cx: &mut Context<Self>,
-    ) -> Result<()> {
+    ) -> Result<(), acp::Error> {
         match update {
             acp::SessionUpdate::UserMessageChunk { content } => {
-                self.push_user_content_block(content, cx);
+                self.push_user_content_block(None, content, cx);
             }
             acp::SessionUpdate::AgentMessageChunk { content } => {
                 self.push_assistant_content_block(content, false, cx);
@@ -731,7 +890,7 @@ impl AcpThread {
                 self.push_assistant_content_block(content, true, cx);
             }
             acp::SessionUpdate::ToolCall(tool_call) => {
-                self.upsert_tool_call(tool_call, cx);
+                self.upsert_tool_call(tool_call, cx)?;
             }
             acp::SessionUpdate::ToolCallUpdate(tool_call_update) => {
                 self.update_tool_call(tool_call_update, cx)?;
@@ -743,18 +902,39 @@ impl AcpThread {
         Ok(())
     }
 
-    pub fn push_user_content_block(&mut self, chunk: acp::ContentBlock, cx: &mut Context<Self>) {
+    pub fn push_user_content_block(
+        &mut self,
+        message_id: Option<UserMessageId>,
+        chunk: acp::ContentBlock,
+        cx: &mut Context<Self>,
+    ) {
         let language_registry = self.project.read(cx).languages().clone();
         let entries_len = self.entries.len();
 
         if let Some(last_entry) = self.entries.last_mut()
-            && let AgentThreadEntry::UserMessage(UserMessage { content }) = last_entry
+            && let AgentThreadEntry::UserMessage(UserMessage {
+                id,
+                content,
+                chunks,
+                ..
+            }) = last_entry
         {
-            content.append(chunk, &language_registry, cx);
-            cx.emit(AcpThreadEvent::EntryUpdated(entries_len - 1));
+            *id = message_id.or(id.take());
+            content.append(chunk.clone(), &language_registry, cx);
+            chunks.push(chunk);
+            let idx = entries_len - 1;
+            cx.emit(AcpThreadEvent::EntryUpdated(idx));
         } else {
-            let content = ContentBlock::new(chunk, &language_registry, cx);
-            self.push_entry(AgentThreadEntry::UserMessage(UserMessage { content }), cx);
+            let content = ContentBlock::new(chunk.clone(), &language_registry, cx);
+            self.push_entry(
+                AgentThreadEntry::UserMessage(UserMessage {
+                    id: message_id,
+                    content,
+                    chunks: vec![chunk],
+                    checkpoint: None,
+                }),
+                cx,
+            );
         }
     }
 
@@ -769,7 +949,8 @@ impl AcpThread {
         if let Some(last_entry) = self.entries.last_mut()
             && let AgentThreadEntry::AssistantMessage(AssistantMessage { chunks }) = last_entry
         {
-            cx.emit(AcpThreadEvent::EntryUpdated(entries_len - 1));
+            let idx = entries_len - 1;
+            cx.emit(AcpThreadEvent::EntryUpdated(idx));
             match (chunks.last_mut(), is_thought) {
                 (Some(AssistantMessageChunk::Message { block }), false)
                 | (Some(AssistantMessageChunk::Thought { block }), true) => {
@@ -806,17 +987,53 @@ impl AcpThread {
         cx.emit(AcpThreadEvent::NewEntry);
     }
 
+    pub fn update_title(&mut self, title: SharedString, cx: &mut Context<Self>) -> Result<()> {
+        self.title = title;
+        cx.emit(AcpThreadEvent::TitleUpdated);
+        Ok(())
+    }
+
+    pub fn update_token_usage(&mut self, usage: Option<TokenUsage>, cx: &mut Context<Self>) {
+        self.token_usage = usage;
+        cx.emit(AcpThreadEvent::TokenUsageUpdated);
+    }
+
+    pub fn update_retry_status(&mut self, status: RetryStatus, cx: &mut Context<Self>) {
+        cx.emit(AcpThreadEvent::Retry(status));
+    }
+
     pub fn update_tool_call(
         &mut self,
-        update: acp::ToolCallUpdate,
+        update: impl Into<ToolCallUpdate>,
         cx: &mut Context<Self>,
     ) -> Result<()> {
+        let update = update.into();
         let languages = self.project.read(cx).languages().clone();
 
         let (ix, current_call) = self
-            .tool_call_mut(&update.id)
+            .tool_call_mut(update.id())
             .context("Tool call not found")?;
-        current_call.update(update.fields, languages, cx);
+        match update {
+            ToolCallUpdate::UpdateFields(update) => {
+                let location_updated = update.fields.locations.is_some();
+                current_call.update_fields(update.fields, languages, cx);
+                if location_updated {
+                    self.resolve_locations(update.id, cx);
+                }
+            }
+            ToolCallUpdate::UpdateDiff(update) => {
+                current_call.content.clear();
+                current_call
+                    .content
+                    .push(ToolCallContent::Diff(update.diff));
+            }
+            ToolCallUpdate::UpdateTerminal(update) => {
+                current_call.content.clear();
+                current_call
+                    .content
+                    .push(ToolCallContent::Terminal(update.terminal));
+            }
+        }
 
         cx.emit(AcpThreadEvent::EntryUpdated(ix));
 
@@ -824,35 +1041,38 @@ impl AcpThread {
     }
 
     /// Updates a tool call if id matches an existing entry, otherwise inserts a new one.
-    pub fn upsert_tool_call(&mut self, tool_call: acp::ToolCall, cx: &mut Context<Self>) {
-        let status = ToolCallStatus::Allowed {
-            status: tool_call.status,
-        };
-        self.upsert_tool_call_inner(tool_call, status, cx)
+    pub fn upsert_tool_call(
+        &mut self,
+        tool_call: acp::ToolCall,
+        cx: &mut Context<Self>,
+    ) -> Result<(), acp::Error> {
+        let status = tool_call.status.into();
+        self.upsert_tool_call_inner(tool_call.into(), status, cx)
     }
 
+    /// Fails if id does not match an existing entry.
     pub fn upsert_tool_call_inner(
         &mut self,
-        tool_call: acp::ToolCall,
+        tool_call_update: acp::ToolCallUpdate,
         status: ToolCallStatus,
         cx: &mut Context<Self>,
-    ) {
+    ) -> Result<(), acp::Error> {
         let language_registry = self.project.read(cx).languages().clone();
-        let call = ToolCall::from_acp(tool_call, status, language_registry, cx);
-
-        let location = call.locations.last().cloned();
+        let id = tool_call_update.id.clone();
 
-        if let Some((ix, current_call)) = self.tool_call_mut(&call.id) {
-            *current_call = call;
+        if let Some((ix, current_call)) = self.tool_call_mut(&id) {
+            current_call.update_fields(tool_call_update.fields, language_registry, cx);
+            current_call.status = status;
 
             cx.emit(AcpThreadEvent::EntryUpdated(ix));
         } else {
+            let call =
+                ToolCall::from_acp(tool_call_update.try_into()?, status, language_registry, cx);
             self.push_entry(AgentThreadEntry::ToolCall(call), cx);
-        }
+        };
 
-        if let Some(location) = location {
-            self.set_project_location(location, cx)
-        }
+        self.resolve_locations(id, cx);
+        Ok(())
     }
 
     fn tool_call_mut(&mut self, id: &acp::ToolCallId) -> Option<(usize, &mut ToolCall)> {

crates/acp_thread/src/connection.rs 🔗

@@ -1,18 +1,141 @@
-use std::{error::Error, fmt, path::Path, rc::Rc, sync::Arc};
-
+use crate::AcpThread;
 use agent_client_protocol::{self as acp};
 use anyhow::Result;
-use gpui::{AsyncApp, Entity, Task};
-use language_model::LanguageModel;
+use collections::IndexMap;
+use gpui::{Entity, SharedString, Task};
+use language_model::LanguageModelProviderId;
 use project::Project;
-use ui::App;
+use serde::{Deserialize, Serialize};
+use std::{any::Any, error::Error, fmt, path::Path, rc::Rc, sync::Arc};
+use ui::{App, IconName};
+use uuid::Uuid;
 
-use crate::AcpThread;
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
+pub struct UserMessageId(Arc<str>);
+
+impl UserMessageId {
+    pub fn new() -> Self {
+        Self(Uuid::new_v4().to_string().into())
+    }
+}
+
+pub trait AgentConnection {
+    fn new_thread(
+        self: Rc<Self>,
+        project: Entity<Project>,
+        cwd: &Path,
+        cx: &mut App,
+    ) -> Task<Result<Entity<AcpThread>>>;
+
+    fn auth_methods(&self) -> &[acp::AuthMethod];
+
+    fn authenticate(&self, method: acp::AuthMethodId, cx: &mut App) -> Task<Result<()>>;
+
+    fn prompt(
+        &self,
+        user_message_id: Option<UserMessageId>,
+        params: acp::PromptRequest,
+        cx: &mut App,
+    ) -> Task<Result<acp::PromptResponse>>;
+
+    fn prompt_capabilities(&self) -> acp::PromptCapabilities;
+
+    fn resume(
+        &self,
+        _session_id: &acp::SessionId,
+        _cx: &mut App,
+    ) -> Option<Rc<dyn AgentSessionResume>> {
+        None
+    }
+
+    fn cancel(&self, session_id: &acp::SessionId, cx: &mut App);
+
+    fn session_editor(
+        &self,
+        _session_id: &acp::SessionId,
+        _cx: &mut App,
+    ) -> Option<Rc<dyn AgentSessionEditor>> {
+        None
+    }
+
+    /// Returns this agent as an [Rc<dyn ModelSelector>] if the model selection capability is supported.
+    ///
+    /// If the agent does not support model selection, returns [None].
+    /// This allows sharing the selector in UI components.
+    fn model_selector(&self) -> Option<Rc<dyn AgentModelSelector>> {
+        None
+    }
+
+    fn telemetry(&self) -> Option<Rc<dyn AgentTelemetry>> {
+        None
+    }
+
+    fn into_any(self: Rc<Self>) -> Rc<dyn Any>;
+}
+
+impl dyn AgentConnection {
+    pub fn downcast<T: 'static + AgentConnection + Sized>(self: Rc<Self>) -> Option<Rc<T>> {
+        self.into_any().downcast().ok()
+    }
+}
+
+pub trait AgentSessionEditor {
+    fn truncate(&self, message_id: UserMessageId, cx: &mut App) -> Task<Result<()>>;
+}
+
+pub trait AgentSessionResume {
+    fn run(&self, cx: &mut App) -> Task<Result<acp::PromptResponse>>;
+}
+
+pub trait AgentTelemetry {
+    /// The name of the agent used for telemetry.
+    fn agent_name(&self) -> String;
+
+    /// A representation of the current thread state that can be serialized for
+    /// storage with telemetry events.
+    fn thread_data(
+        &self,
+        session_id: &acp::SessionId,
+        cx: &mut App,
+    ) -> Task<Result<serde_json::Value>>;
+}
+
+#[derive(Debug)]
+pub struct AuthRequired {
+    pub description: Option<String>,
+    pub provider_id: Option<LanguageModelProviderId>,
+}
+
+impl AuthRequired {
+    pub fn new() -> Self {
+        Self {
+            description: None,
+            provider_id: None,
+        }
+    }
+
+    pub fn with_description(mut self, description: String) -> Self {
+        self.description = Some(description);
+        self
+    }
+
+    pub fn with_language_model_provider(mut self, provider_id: LanguageModelProviderId) -> Self {
+        self.provider_id = Some(provider_id);
+        self
+    }
+}
+
+impl Error for AuthRequired {}
+impl fmt::Display for AuthRequired {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "Authentication required")
+    }
+}
 
 /// Trait for agents that support listing, selecting, and querying language models.
 ///
 /// This is an optional capability; agents indicate support via [AgentConnection::model_selector].
-pub trait ModelSelector: 'static {
+pub trait AgentModelSelector: 'static {
     /// Lists all available language models for this agent.
     ///
     /// # Parameters
@@ -20,7 +143,7 @@ pub trait ModelSelector: 'static {
     ///
     /// # Returns
     /// A task resolving to the list of models or an error (e.g., if no models are configured).
-    fn list_models(&self, cx: &mut AsyncApp) -> Task<Result<Vec<Arc<dyn LanguageModel>>>>;
+    fn list_models(&self, cx: &mut App) -> Task<Result<AgentModelList>>;
 
     /// Selects a model for a specific session (thread).
     ///
@@ -37,8 +160,8 @@ pub trait ModelSelector: 'static {
     fn select_model(
         &self,
         session_id: acp::SessionId,
-        model: Arc<dyn LanguageModel>,
-        cx: &mut AsyncApp,
+        model_id: AgentModelId,
+        cx: &mut App,
     ) -> Task<Result<()>>;
 
     /// Retrieves the currently selected model for a specific session (thread).
@@ -52,42 +175,276 @@ pub trait ModelSelector: 'static {
     fn selected_model(
         &self,
         session_id: &acp::SessionId,
-        cx: &mut AsyncApp,
-    ) -> Task<Result<Arc<dyn LanguageModel>>>;
+        cx: &mut App,
+    ) -> Task<Result<AgentModelInfo>>;
+
+    /// Whenever the model list is updated the receiver will be notified.
+    fn watch(&self, cx: &mut App) -> watch::Receiver<()>;
 }
 
-pub trait AgentConnection {
-    fn new_thread(
-        self: Rc<Self>,
-        project: Entity<Project>,
-        cwd: &Path,
-        cx: &mut AsyncApp,
-    ) -> Task<Result<Entity<AcpThread>>>;
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub struct AgentModelId(pub SharedString);
 
-    fn auth_methods(&self) -> &[acp::AuthMethod];
+impl std::ops::Deref for AgentModelId {
+    type Target = SharedString;
 
-    fn authenticate(&self, method: acp::AuthMethodId, cx: &mut App) -> Task<Result<()>>;
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
 
-    fn prompt(&self, params: acp::PromptRequest, cx: &mut App)
-    -> Task<Result<acp::PromptResponse>>;
+impl fmt::Display for AgentModelId {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        self.0.fmt(f)
+    }
+}
 
-    fn cancel(&self, session_id: &acp::SessionId, cx: &mut App);
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct AgentModelInfo {
+    pub id: AgentModelId,
+    pub name: SharedString,
+    pub icon: Option<IconName>,
+}
 
-    /// Returns this agent as an [Rc<dyn ModelSelector>] if the model selection capability is supported.
-    ///
-    /// If the agent does not support model selection, returns [None].
-    /// This allows sharing the selector in UI components.
-    fn model_selector(&self) -> Option<Rc<dyn ModelSelector>> {
-        None // Default impl for agents that don't support it
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub struct AgentModelGroupName(pub SharedString);
+
+#[derive(Debug, Clone)]
+pub enum AgentModelList {
+    Flat(Vec<AgentModelInfo>),
+    Grouped(IndexMap<AgentModelGroupName, Vec<AgentModelInfo>>),
+}
+
+impl AgentModelList {
+    pub fn is_empty(&self) -> bool {
+        match self {
+            AgentModelList::Flat(models) => models.is_empty(),
+            AgentModelList::Grouped(groups) => groups.is_empty(),
+        }
     }
 }
 
-#[derive(Debug)]
-pub struct AuthRequired;
+#[cfg(feature = "test-support")]
+mod test_support {
+    use std::sync::Arc;
 
-impl Error for AuthRequired {}
-impl fmt::Display for AuthRequired {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        write!(f, "AuthRequired")
+    use action_log::ActionLog;
+    use collections::HashMap;
+    use futures::{channel::oneshot, future::try_join_all};
+    use gpui::{AppContext as _, WeakEntity};
+    use parking_lot::Mutex;
+
+    use super::*;
+
+    #[derive(Clone, Default)]
+    pub struct StubAgentConnection {
+        sessions: Arc<Mutex<HashMap<acp::SessionId, Session>>>,
+        permission_requests: HashMap<acp::ToolCallId, Vec<acp::PermissionOption>>,
+        next_prompt_updates: Arc<Mutex<Vec<acp::SessionUpdate>>>,
+    }
+
+    struct Session {
+        thread: WeakEntity<AcpThread>,
+        response_tx: Option<oneshot::Sender<acp::StopReason>>,
+    }
+
+    impl StubAgentConnection {
+        pub fn new() -> Self {
+            Self {
+                next_prompt_updates: Default::default(),
+                permission_requests: HashMap::default(),
+                sessions: Arc::default(),
+            }
+        }
+
+        pub fn set_next_prompt_updates(&self, updates: Vec<acp::SessionUpdate>) {
+            *self.next_prompt_updates.lock() = updates;
+        }
+
+        pub fn with_permission_requests(
+            mut self,
+            permission_requests: HashMap<acp::ToolCallId, Vec<acp::PermissionOption>>,
+        ) -> Self {
+            self.permission_requests = permission_requests;
+            self
+        }
+
+        pub fn send_update(
+            &self,
+            session_id: acp::SessionId,
+            update: acp::SessionUpdate,
+            cx: &mut App,
+        ) {
+            assert!(
+                self.next_prompt_updates.lock().is_empty(),
+                "Use either send_update or set_next_prompt_updates"
+            );
+
+            self.sessions
+                .lock()
+                .get(&session_id)
+                .unwrap()
+                .thread
+                .update(cx, |thread, cx| {
+                    thread.handle_session_update(update, cx).unwrap();
+                })
+                .unwrap();
+        }
+
+        pub fn end_turn(&self, session_id: acp::SessionId, stop_reason: acp::StopReason) {
+            self.sessions
+                .lock()
+                .get_mut(&session_id)
+                .unwrap()
+                .response_tx
+                .take()
+                .expect("No pending turn")
+                .send(stop_reason)
+                .unwrap();
+        }
+    }
+
+    impl AgentConnection for StubAgentConnection {
+        fn auth_methods(&self) -> &[acp::AuthMethod] {
+            &[]
+        }
+
+        fn new_thread(
+            self: Rc<Self>,
+            project: Entity<Project>,
+            _cwd: &Path,
+            cx: &mut gpui::App,
+        ) -> Task<gpui::Result<Entity<AcpThread>>> {
+            let session_id = acp::SessionId(self.sessions.lock().len().to_string().into());
+            let action_log = cx.new(|_| ActionLog::new(project.clone()));
+            let thread = cx.new(|_cx| {
+                AcpThread::new(
+                    "Test",
+                    self.clone(),
+                    project,
+                    action_log,
+                    session_id.clone(),
+                )
+            });
+            self.sessions.lock().insert(
+                session_id,
+                Session {
+                    thread: thread.downgrade(),
+                    response_tx: None,
+                },
+            );
+            Task::ready(Ok(thread))
+        }
+
+        fn prompt_capabilities(&self) -> acp::PromptCapabilities {
+            acp::PromptCapabilities {
+                image: true,
+                audio: true,
+                embedded_context: true,
+            }
+        }
+
+        fn authenticate(
+            &self,
+            _method_id: acp::AuthMethodId,
+            _cx: &mut App,
+        ) -> Task<gpui::Result<()>> {
+            unimplemented!()
+        }
+
+        fn prompt(
+            &self,
+            _id: Option<UserMessageId>,
+            params: acp::PromptRequest,
+            cx: &mut App,
+        ) -> Task<gpui::Result<acp::PromptResponse>> {
+            let mut sessions = self.sessions.lock();
+            let Session {
+                thread,
+                response_tx,
+            } = sessions.get_mut(&params.session_id).unwrap();
+            let mut tasks = vec![];
+            if self.next_prompt_updates.lock().is_empty() {
+                let (tx, rx) = oneshot::channel();
+                response_tx.replace(tx);
+                cx.spawn(async move |_| {
+                    let stop_reason = rx.await?;
+                    Ok(acp::PromptResponse { stop_reason })
+                })
+            } else {
+                for update in self.next_prompt_updates.lock().drain(..) {
+                    let thread = thread.clone();
+                    let update = update.clone();
+                    let permission_request = if let acp::SessionUpdate::ToolCall(tool_call) =
+                        &update
+                        && let Some(options) = self.permission_requests.get(&tool_call.id)
+                    {
+                        Some((tool_call.clone(), options.clone()))
+                    } else {
+                        None
+                    };
+                    let task = cx.spawn(async move |cx| {
+                        if let Some((tool_call, options)) = permission_request {
+                            let permission = thread.update(cx, |thread, cx| {
+                                thread.request_tool_call_authorization(
+                                    tool_call.clone().into(),
+                                    options.clone(),
+                                    cx,
+                                )
+                            })?;
+                            permission?.await?;
+                        }
+                        thread.update(cx, |thread, cx| {
+                            thread.handle_session_update(update.clone(), cx).unwrap();
+                        })?;
+                        anyhow::Ok(())
+                    });
+                    tasks.push(task);
+                }
+
+                cx.spawn(async move |_| {
+                    try_join_all(tasks).await?;
+                    Ok(acp::PromptResponse {
+                        stop_reason: acp::StopReason::EndTurn,
+                    })
+                })
+            }
+        }
+
+        fn cancel(&self, session_id: &acp::SessionId, _cx: &mut App) {
+            if let Some(end_turn_tx) = self
+                .sessions
+                .lock()
+                .get_mut(session_id)
+                .unwrap()
+                .response_tx
+                .take()
+            {
+                end_turn_tx.send(acp::StopReason::Cancelled).unwrap();
+            }
+        }
+
+        fn session_editor(
+            &self,
+            _session_id: &agent_client_protocol::SessionId,
+            _cx: &mut App,
+        ) -> Option<Rc<dyn AgentSessionEditor>> {
+            Some(Rc::new(StubAgentSessionEditor))
+        }
+
+        fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
+            self
+        }
+    }
+
+    struct StubAgentSessionEditor;
+
+    impl AgentSessionEditor for StubAgentSessionEditor {
+        fn truncate(&self, _: UserMessageId, _: &mut App) -> Task<Result<()>> {
+            Task::ready(Ok(()))
+        }
     }
 }
+
+#[cfg(feature = "test-support")]
+pub use test_support::*;

crates/acp_thread/src/diff.rs 🔗

@@ -0,0 +1,385 @@
+use anyhow::Result;
+use buffer_diff::{BufferDiff, BufferDiffSnapshot};
+use editor::{MultiBuffer, PathKey};
+use gpui::{App, AppContext, AsyncApp, Context, Entity, Subscription, Task};
+use itertools::Itertools;
+use language::{
+    Anchor, Buffer, Capability, LanguageRegistry, OffsetRangeExt as _, Point, Rope, TextBuffer,
+};
+use std::{
+    cmp::Reverse,
+    ops::Range,
+    path::{Path, PathBuf},
+    sync::Arc,
+};
+use util::ResultExt;
+
+pub enum Diff {
+    Pending(PendingDiff),
+    Finalized(FinalizedDiff),
+}
+
+impl Diff {
+    pub fn finalized(
+        path: PathBuf,
+        old_text: Option<String>,
+        new_text: String,
+        language_registry: Arc<LanguageRegistry>,
+        cx: &mut Context<Self>,
+    ) -> Self {
+        let multibuffer = cx.new(|_cx| MultiBuffer::without_headers(Capability::ReadOnly));
+        let buffer = cx.new(|cx| Buffer::local(new_text, cx));
+        let task = cx.spawn({
+            let multibuffer = multibuffer.clone();
+            let path = path.clone();
+            async move |_, cx| {
+                let language = language_registry
+                    .language_for_file_path(&path)
+                    .await
+                    .log_err();
+
+                buffer.update(cx, |buffer, cx| buffer.set_language(language.clone(), cx))?;
+
+                let diff = build_buffer_diff(
+                    old_text.unwrap_or("".into()).into(),
+                    &buffer,
+                    Some(language_registry.clone()),
+                    cx,
+                )
+                .await?;
+
+                multibuffer
+                    .update(cx, |multibuffer, cx| {
+                        let hunk_ranges = {
+                            let buffer = buffer.read(cx);
+                            let diff = diff.read(cx);
+                            diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, buffer, cx)
+                                .map(|diff_hunk| diff_hunk.buffer_range.to_point(buffer))
+                                .collect::<Vec<_>>()
+                        };
+
+                        multibuffer.set_excerpts_for_path(
+                            PathKey::for_buffer(&buffer, cx),
+                            buffer.clone(),
+                            hunk_ranges,
+                            editor::DEFAULT_MULTIBUFFER_CONTEXT,
+                            cx,
+                        );
+                        multibuffer.add_diff(diff, cx);
+                    })
+                    .log_err();
+
+                anyhow::Ok(())
+            }
+        });
+
+        Self::Finalized(FinalizedDiff {
+            multibuffer,
+            path,
+            _update_diff: task,
+        })
+    }
+
+    pub fn new(buffer: Entity<Buffer>, cx: &mut Context<Self>) -> Self {
+        let buffer_snapshot = buffer.read(cx).snapshot();
+        let base_text = buffer_snapshot.text();
+        let language_registry = buffer.read(cx).language_registry();
+        let text_snapshot = buffer.read(cx).text_snapshot();
+        let buffer_diff = cx.new(|cx| {
+            let mut diff = BufferDiff::new(&text_snapshot, cx);
+            let _ = diff.set_base_text(
+                buffer_snapshot.clone(),
+                language_registry,
+                text_snapshot,
+                cx,
+            );
+            let snapshot = diff.snapshot(cx);
+
+            let secondary_diff = cx.new(|cx| {
+                let mut diff = BufferDiff::new(&buffer_snapshot, cx);
+                diff.set_snapshot(snapshot, &buffer_snapshot, cx);
+                diff
+            });
+            diff.set_secondary_diff(secondary_diff);
+
+            diff
+        });
+
+        let multibuffer = cx.new(|cx| {
+            let mut multibuffer = MultiBuffer::without_headers(Capability::ReadOnly);
+            multibuffer.add_diff(buffer_diff.clone(), cx);
+            multibuffer
+        });
+
+        Self::Pending(PendingDiff {
+            multibuffer,
+            base_text: Arc::new(base_text),
+            _subscription: cx.observe(&buffer, |this, _, cx| {
+                if let Diff::Pending(diff) = this {
+                    diff.update(cx);
+                }
+            }),
+            buffer,
+            diff: buffer_diff,
+            revealed_ranges: Vec::new(),
+            update_diff: Task::ready(Ok(())),
+        })
+    }
+
+    pub fn reveal_range(&mut self, range: Range<Anchor>, cx: &mut Context<Self>) {
+        if let Self::Pending(diff) = self {
+            diff.reveal_range(range, cx);
+        }
+    }
+
+    pub fn finalize(&mut self, cx: &mut Context<Self>) {
+        if let Self::Pending(diff) = self {
+            *self = Self::Finalized(diff.finalize(cx));
+        }
+    }
+
+    pub fn multibuffer(&self) -> &Entity<MultiBuffer> {
+        match self {
+            Self::Pending(PendingDiff { multibuffer, .. }) => multibuffer,
+            Self::Finalized(FinalizedDiff { multibuffer, .. }) => multibuffer,
+        }
+    }
+
+    pub fn to_markdown(&self, cx: &App) -> String {
+        let buffer_text = self
+            .multibuffer()
+            .read(cx)
+            .all_buffers()
+            .iter()
+            .map(|buffer| buffer.read(cx).text())
+            .join("\n");
+        let path = match self {
+            Diff::Pending(PendingDiff { buffer, .. }) => {
+                buffer.read(cx).file().map(|file| file.path().as_ref())
+            }
+            Diff::Finalized(FinalizedDiff { path, .. }) => Some(path.as_path()),
+        };
+        format!(
+            "Diff: {}\n```\n{}\n```\n",
+            path.unwrap_or(Path::new("untitled")).display(),
+            buffer_text
+        )
+    }
+
+    pub fn has_revealed_range(&self, cx: &App) -> bool {
+        self.multibuffer().read(cx).excerpt_paths().next().is_some()
+    }
+}
+
+pub struct PendingDiff {
+    multibuffer: Entity<MultiBuffer>,
+    base_text: Arc<String>,
+    buffer: Entity<Buffer>,
+    diff: Entity<BufferDiff>,
+    revealed_ranges: Vec<Range<Anchor>>,
+    _subscription: Subscription,
+    update_diff: Task<Result<()>>,
+}
+
+impl PendingDiff {
+    pub fn update(&mut self, cx: &mut Context<Diff>) {
+        let buffer = self.buffer.clone();
+        let buffer_diff = self.diff.clone();
+        let base_text = self.base_text.clone();
+        self.update_diff = cx.spawn(async move |diff, cx| {
+            let text_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot())?;
+            let diff_snapshot = BufferDiff::update_diff(
+                buffer_diff.clone(),
+                text_snapshot.clone(),
+                Some(base_text),
+                false,
+                false,
+                None,
+                None,
+                cx,
+            )
+            .await?;
+            buffer_diff.update(cx, |diff, cx| {
+                diff.set_snapshot(diff_snapshot.clone(), &text_snapshot, cx);
+                diff.secondary_diff().unwrap().update(cx, |diff, cx| {
+                    diff.set_snapshot(diff_snapshot.clone(), &text_snapshot, cx);
+                });
+            })?;
+            diff.update(cx, |diff, cx| {
+                if let Diff::Pending(diff) = diff {
+                    diff.update_visible_ranges(cx);
+                }
+            })
+        });
+    }
+
+    pub fn reveal_range(&mut self, range: Range<Anchor>, cx: &mut Context<Diff>) {
+        self.revealed_ranges.push(range);
+        self.update_visible_ranges(cx);
+    }
+
+    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.buffer.read(cx).language_registry();
+
+        let path = self
+            .buffer
+            .read(cx)
+            .file()
+            .map(|file| file.path().as_ref())
+            .unwrap_or(Path::new("untitled"))
+            .into();
+
+        // Replace the buffer in the multibuffer with the snapshot
+        let buffer = cx.new(|cx| {
+            let language = self.buffer.read(cx).language().cloned();
+            let buffer = TextBuffer::new_normalized(
+                0,
+                cx.entity_id().as_non_zero_u64().into(),
+                self.buffer.read(cx).line_ending(),
+                self.buffer.read(cx).as_rope().clone(),
+            );
+            let mut buffer = Buffer::build(buffer, None, Capability::ReadWrite);
+            buffer.set_language(language, cx);
+            buffer
+        });
+
+        let buffer_diff = cx.spawn({
+            let buffer = buffer.clone();
+            async move |_this, cx| {
+                build_buffer_diff(base_text, &buffer, language_registry, cx).await
+            }
+        });
+
+        let update_diff = cx.spawn(async move |this, cx| {
+            let buffer_diff = buffer_diff.await?;
+            this.update(cx, |this, cx| {
+                this.multibuffer().update(cx, |multibuffer, cx| {
+                    let path_key = PathKey::for_buffer(&buffer, cx);
+                    multibuffer.clear(cx);
+                    multibuffer.set_excerpts_for_path(
+                        path_key,
+                        buffer,
+                        ranges,
+                        editor::DEFAULT_MULTIBUFFER_CONTEXT,
+                        cx,
+                    );
+                    multibuffer.add_diff(buffer_diff.clone(), cx);
+                });
+
+                cx.notify();
+            })
+        });
+
+        FinalizedDiff {
+            path,
+            multibuffer: self.multibuffer.clone(),
+            _update_diff: update_diff,
+        }
+    }
+
+    fn update_visible_ranges(&mut self, cx: &mut Context<Diff>) {
+        let ranges = self.excerpt_ranges(cx);
+        self.multibuffer.update(cx, |multibuffer, cx| {
+            multibuffer.set_excerpts_for_path(
+                PathKey::for_buffer(&self.buffer, cx),
+                self.buffer.clone(),
+                ranges,
+                editor::DEFAULT_MULTIBUFFER_CONTEXT,
+                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 buffer = self.buffer.read(cx);
+        let diff = self.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 struct FinalizedDiff {
+    path: PathBuf,
+    multibuffer: Entity<MultiBuffer>,
+    _update_diff: Task<Result<()>>,
+}
+
+async fn build_buffer_diff(
+    old_text: Arc<String>,
+    buffer: &Entity<Buffer>,
+    language_registry: Option<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(),
+                language_registry,
+                cx,
+            )
+        })?
+        .await;
+
+    let diff_snapshot = cx
+        .update(|cx| {
+            BufferDiffSnapshot::new_with_base_buffer(
+                buffer.text.clone(),
+                Some(old_text),
+                base_buffer,
+                cx,
+            )
+        })?
+        .await;
+
+    let secondary_diff = cx.new(|cx| {
+        let mut diff = BufferDiff::new(&buffer, cx);
+        diff.set_snapshot(diff_snapshot.clone(), &buffer, cx);
+        diff
+    })?;
+
+    cx.new(|cx| {
+        let mut diff = BufferDiff::new(&buffer.text, cx);
+        diff.set_snapshot(diff_snapshot, &buffer, cx);
+        diff.set_secondary_diff(secondary_diff);
+        diff
+    })
+}

crates/acp_thread/src/mention.rs 🔗

@@ -0,0 +1,438 @@
+use agent_client_protocol as acp;
+use anyhow::{Context as _, Result, bail};
+use file_icons::FileIcons;
+use prompt_store::{PromptId, UserPromptId};
+use serde::{Deserialize, Serialize};
+use std::{
+    fmt,
+    ops::Range,
+    path::{Path, PathBuf},
+    str::FromStr,
+};
+use ui::{App, IconName, SharedString};
+use url::Url;
+
+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Hash)]
+pub enum MentionUri {
+    File {
+        abs_path: PathBuf,
+    },
+    Directory {
+        abs_path: PathBuf,
+    },
+    Symbol {
+        path: PathBuf,
+        name: String,
+        line_range: Range<u32>,
+    },
+    Thread {
+        id: acp::SessionId,
+        name: String,
+    },
+    TextThread {
+        path: PathBuf,
+        name: String,
+    },
+    Rule {
+        id: PromptId,
+        name: String,
+    },
+    Selection {
+        path: PathBuf,
+        line_range: Range<u32>,
+    },
+    Fetch {
+        url: Url,
+    },
+}
+
+impl MentionUri {
+    pub fn parse(input: &str) -> Result<Self> {
+        let url = url::Url::parse(input)?;
+        let path = url.path();
+        match url.scheme() {
+            "file" => {
+                let path = url.to_file_path().ok().context("Extracting file path")?;
+                if let Some(fragment) = url.fragment() {
+                    let range = fragment
+                        .strip_prefix("L")
+                        .context("Line range must start with \"L\"")?;
+                    let (start, end) = range
+                        .split_once(":")
+                        .context("Line range must use colon as separator")?;
+                    let line_range = start
+                        .parse::<u32>()
+                        .context("Parsing line range start")?
+                        .checked_sub(1)
+                        .context("Line numbers should be 1-based")?
+                        ..end
+                            .parse::<u32>()
+                            .context("Parsing line range end")?
+                            .checked_sub(1)
+                            .context("Line numbers should be 1-based")?;
+                    if let Some(name) = single_query_param(&url, "symbol")? {
+                        Ok(Self::Symbol {
+                            name,
+                            path,
+                            line_range,
+                        })
+                    } else {
+                        Ok(Self::Selection { path, line_range })
+                    }
+                } else if input.ends_with("/") {
+                    Ok(Self::Directory { abs_path: path })
+                } else {
+                    Ok(Self::File { abs_path: path })
+                }
+            }
+            "zed" => {
+                if let Some(thread_id) = path.strip_prefix("/agent/thread/") {
+                    let name = single_query_param(&url, "name")?.context("Missing thread name")?;
+                    Ok(Self::Thread {
+                        id: acp::SessionId(thread_id.into()),
+                        name,
+                    })
+                } else if let Some(path) = path.strip_prefix("/agent/text-thread/") {
+                    let name = single_query_param(&url, "name")?.context("Missing thread name")?;
+                    Ok(Self::TextThread {
+                        path: path.into(),
+                        name,
+                    })
+                } else if let Some(rule_id) = path.strip_prefix("/agent/rule/") {
+                    let name = single_query_param(&url, "name")?.context("Missing rule name")?;
+                    let rule_id = UserPromptId(rule_id.parse()?);
+                    Ok(Self::Rule {
+                        id: rule_id.into(),
+                        name,
+                    })
+                } else {
+                    bail!("invalid zed url: {:?}", input);
+                }
+            }
+            "http" | "https" => Ok(MentionUri::Fetch { url }),
+            other => bail!("unrecognized scheme {:?}", other),
+        }
+    }
+
+    pub fn name(&self) -> String {
+        match self {
+            MentionUri::File { abs_path, .. } | MentionUri::Directory { abs_path, .. } => abs_path
+                .file_name()
+                .unwrap_or_default()
+                .to_string_lossy()
+                .into_owned(),
+            MentionUri::Symbol { name, .. } => name.clone(),
+            MentionUri::Thread { name, .. } => name.clone(),
+            MentionUri::TextThread { name, .. } => name.clone(),
+            MentionUri::Rule { name, .. } => name.clone(),
+            MentionUri::Selection {
+                path, line_range, ..
+            } => selection_name(path, line_range),
+            MentionUri::Fetch { url } => url.to_string(),
+        }
+    }
+
+    pub fn icon_path(&self, cx: &mut App) -> SharedString {
+        match self {
+            MentionUri::File { abs_path } => {
+                FileIcons::get_icon(abs_path, cx).unwrap_or_else(|| IconName::File.path().into())
+            }
+            MentionUri::Directory { .. } => FileIcons::get_folder_icon(false, cx)
+                .unwrap_or_else(|| IconName::Folder.path().into()),
+            MentionUri::Symbol { .. } => IconName::Code.path().into(),
+            MentionUri::Thread { .. } => IconName::Thread.path().into(),
+            MentionUri::TextThread { .. } => IconName::Thread.path().into(),
+            MentionUri::Rule { .. } => IconName::Reader.path().into(),
+            MentionUri::Selection { .. } => IconName::Reader.path().into(),
+            MentionUri::Fetch { .. } => IconName::ToolWeb.path().into(),
+        }
+    }
+
+    pub fn as_link<'a>(&'a self) -> MentionLink<'a> {
+        MentionLink(self)
+    }
+
+    pub fn to_uri(&self) -> Url {
+        match self {
+            MentionUri::File { abs_path } => {
+                Url::from_file_path(abs_path).expect("mention path should be absolute")
+            }
+            MentionUri::Directory { abs_path } => {
+                Url::from_directory_path(abs_path).expect("mention path should be absolute")
+            }
+            MentionUri::Symbol {
+                path,
+                name,
+                line_range,
+            } => {
+                let mut url = Url::from_file_path(path).expect("mention path should be absolute");
+                url.query_pairs_mut().append_pair("symbol", name);
+                url.set_fragment(Some(&format!(
+                    "L{}:{}",
+                    line_range.start + 1,
+                    line_range.end + 1
+                )));
+                url
+            }
+            MentionUri::Selection { path, line_range } => {
+                let mut url = Url::from_file_path(path).expect("mention path should be absolute");
+                url.set_fragment(Some(&format!(
+                    "L{}:{}",
+                    line_range.start + 1,
+                    line_range.end + 1
+                )));
+                url
+            }
+            MentionUri::Thread { name, id } => {
+                let mut url = Url::parse("zed:///").unwrap();
+                url.set_path(&format!("/agent/thread/{id}"));
+                url.query_pairs_mut().append_pair("name", name);
+                url
+            }
+            MentionUri::TextThread { path, name } => {
+                let mut url = Url::parse("zed:///").unwrap();
+                url.set_path(&format!("/agent/text-thread/{}", path.to_string_lossy()));
+                url.query_pairs_mut().append_pair("name", name);
+                url
+            }
+            MentionUri::Rule { name, id } => {
+                let mut url = Url::parse("zed:///").unwrap();
+                url.set_path(&format!("/agent/rule/{id}"));
+                url.query_pairs_mut().append_pair("name", name);
+                url
+            }
+            MentionUri::Fetch { url } => url.clone(),
+        }
+    }
+}
+
+impl FromStr for MentionUri {
+    type Err = anyhow::Error;
+
+    fn from_str(s: &str) -> anyhow::Result<Self> {
+        Self::parse(s)
+    }
+}
+
+pub struct MentionLink<'a>(&'a MentionUri);
+
+impl fmt::Display for MentionLink<'_> {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "[@{}]({})", self.0.name(), self.0.to_uri())
+    }
+}
+
+fn single_query_param(url: &Url, name: &'static str) -> Result<Option<String>> {
+    let pairs = url.query_pairs().collect::<Vec<_>>();
+    match pairs.as_slice() {
+        [] => Ok(None),
+        [(k, v)] => {
+            if k != name {
+                bail!("invalid query parameter")
+            }
+
+            Ok(Some(v.to_string()))
+        }
+        _ => bail!("too many query pairs"),
+    }
+}
+
+pub fn selection_name(path: &Path, line_range: &Range<u32>) -> String {
+    format!(
+        "{} ({}:{})",
+        path.file_name().unwrap_or_default().display(),
+        line_range.start + 1,
+        line_range.end + 1
+    )
+}
+
+#[cfg(test)]
+mod tests {
+    use util::{path, uri};
+
+    use super::*;
+
+    #[test]
+    fn test_parse_file_uri() {
+        let file_uri = uri!("file:///path/to/file.rs");
+        let parsed = MentionUri::parse(file_uri).unwrap();
+        match &parsed {
+            MentionUri::File { abs_path } => {
+                assert_eq!(abs_path.to_str().unwrap(), path!("/path/to/file.rs"));
+            }
+            _ => panic!("Expected File variant"),
+        }
+        assert_eq!(parsed.to_uri().to_string(), file_uri);
+    }
+
+    #[test]
+    fn test_parse_directory_uri() {
+        let file_uri = uri!("file:///path/to/dir/");
+        let parsed = MentionUri::parse(file_uri).unwrap();
+        match &parsed {
+            MentionUri::Directory { abs_path } => {
+                assert_eq!(abs_path.to_str().unwrap(), path!("/path/to/dir/"));
+            }
+            _ => panic!("Expected Directory variant"),
+        }
+        assert_eq!(parsed.to_uri().to_string(), file_uri);
+    }
+
+    #[test]
+    fn test_to_directory_uri_with_slash() {
+        let uri = MentionUri::Directory {
+            abs_path: PathBuf::from(path!("/path/to/dir/")),
+        };
+        let expected = uri!("file:///path/to/dir/");
+        assert_eq!(uri.to_uri().to_string(), expected);
+    }
+
+    #[test]
+    fn test_to_directory_uri_without_slash() {
+        let uri = MentionUri::Directory {
+            abs_path: PathBuf::from(path!("/path/to/dir")),
+        };
+        let expected = uri!("file:///path/to/dir/");
+        assert_eq!(uri.to_uri().to_string(), expected);
+    }
+
+    #[test]
+    fn test_parse_symbol_uri() {
+        let symbol_uri = uri!("file:///path/to/file.rs?symbol=MySymbol#L10:20");
+        let parsed = MentionUri::parse(symbol_uri).unwrap();
+        match &parsed {
+            MentionUri::Symbol {
+                path,
+                name,
+                line_range,
+            } => {
+                assert_eq!(path.to_str().unwrap(), path!("/path/to/file.rs"));
+                assert_eq!(name, "MySymbol");
+                assert_eq!(line_range.start, 9);
+                assert_eq!(line_range.end, 19);
+            }
+            _ => panic!("Expected Symbol variant"),
+        }
+        assert_eq!(parsed.to_uri().to_string(), symbol_uri);
+    }
+
+    #[test]
+    fn test_parse_selection_uri() {
+        let selection_uri = uri!("file:///path/to/file.rs#L5:15");
+        let parsed = MentionUri::parse(selection_uri).unwrap();
+        match &parsed {
+            MentionUri::Selection { path, line_range } => {
+                assert_eq!(path.to_str().unwrap(), path!("/path/to/file.rs"));
+                assert_eq!(line_range.start, 4);
+                assert_eq!(line_range.end, 14);
+            }
+            _ => panic!("Expected Selection variant"),
+        }
+        assert_eq!(parsed.to_uri().to_string(), selection_uri);
+    }
+
+    #[test]
+    fn test_parse_thread_uri() {
+        let thread_uri = "zed:///agent/thread/session123?name=Thread+name";
+        let parsed = MentionUri::parse(thread_uri).unwrap();
+        match &parsed {
+            MentionUri::Thread {
+                id: thread_id,
+                name,
+            } => {
+                assert_eq!(thread_id.to_string(), "session123");
+                assert_eq!(name, "Thread name");
+            }
+            _ => panic!("Expected Thread variant"),
+        }
+        assert_eq!(parsed.to_uri().to_string(), thread_uri);
+    }
+
+    #[test]
+    fn test_parse_rule_uri() {
+        let rule_uri = "zed:///agent/rule/d8694ff2-90d5-4b6f-be33-33c1763acd52?name=Some+rule";
+        let parsed = MentionUri::parse(rule_uri).unwrap();
+        match &parsed {
+            MentionUri::Rule { id, name } => {
+                assert_eq!(id.to_string(), "d8694ff2-90d5-4b6f-be33-33c1763acd52");
+                assert_eq!(name, "Some rule");
+            }
+            _ => panic!("Expected Rule variant"),
+        }
+        assert_eq!(parsed.to_uri().to_string(), rule_uri);
+    }
+
+    #[test]
+    fn test_parse_fetch_http_uri() {
+        let http_uri = "http://example.com/path?query=value#fragment";
+        let parsed = MentionUri::parse(http_uri).unwrap();
+        match &parsed {
+            MentionUri::Fetch { url } => {
+                assert_eq!(url.to_string(), http_uri);
+            }
+            _ => panic!("Expected Fetch variant"),
+        }
+        assert_eq!(parsed.to_uri().to_string(), http_uri);
+    }
+
+    #[test]
+    fn test_parse_fetch_https_uri() {
+        let https_uri = "https://example.com/api/endpoint";
+        let parsed = MentionUri::parse(https_uri).unwrap();
+        match &parsed {
+            MentionUri::Fetch { url } => {
+                assert_eq!(url.to_string(), https_uri);
+            }
+            _ => panic!("Expected Fetch variant"),
+        }
+        assert_eq!(parsed.to_uri().to_string(), https_uri);
+    }
+
+    #[test]
+    fn test_invalid_scheme() {
+        assert!(MentionUri::parse("ftp://example.com").is_err());
+        assert!(MentionUri::parse("ssh://example.com").is_err());
+        assert!(MentionUri::parse("unknown://example.com").is_err());
+    }
+
+    #[test]
+    fn test_invalid_zed_path() {
+        assert!(MentionUri::parse("zed:///invalid/path").is_err());
+        assert!(MentionUri::parse("zed:///agent/unknown/test").is_err());
+    }
+
+    #[test]
+    fn test_invalid_line_range_format() {
+        // Missing L prefix
+        assert!(MentionUri::parse(uri!("file:///path/to/file.rs#10:20")).is_err());
+
+        // Missing colon separator
+        assert!(MentionUri::parse(uri!("file:///path/to/file.rs#L1020")).is_err());
+
+        // Invalid numbers
+        assert!(MentionUri::parse(uri!("file:///path/to/file.rs#L10:abc")).is_err());
+        assert!(MentionUri::parse(uri!("file:///path/to/file.rs#Labc:20")).is_err());
+    }
+
+    #[test]
+    fn test_invalid_query_parameters() {
+        // Invalid query parameter name
+        assert!(MentionUri::parse(uri!("file:///path/to/file.rs#L10:20?invalid=test")).is_err());
+
+        // Too many query parameters
+        assert!(
+            MentionUri::parse(uri!(
+                "file:///path/to/file.rs#L10:20?symbol=test&another=param"
+            ))
+            .is_err()
+        );
+    }
+
+    #[test]
+    fn test_zero_based_line_numbers() {
+        // Test that 0-based line numbers are rejected (should be 1-based)
+        assert!(MentionUri::parse(uri!("file:///path/to/file.rs#L0:10")).is_err());
+        assert!(MentionUri::parse(uri!("file:///path/to/file.rs#L1:0")).is_err());
+        assert!(MentionUri::parse(uri!("file:///path/to/file.rs#L0:0")).is_err());
+    }
+}

crates/acp_thread/src/terminal.rs 🔗

@@ -0,0 +1,93 @@
+use gpui::{App, AppContext, Context, Entity};
+use language::LanguageRegistry;
+use markdown::Markdown;
+use std::{path::PathBuf, process::ExitStatus, sync::Arc, time::Instant};
+
+pub struct Terminal {
+    command: Entity<Markdown>,
+    working_dir: Option<PathBuf>,
+    terminal: Entity<terminal::Terminal>,
+    started_at: Instant,
+    output: Option<TerminalOutput>,
+}
+
+pub struct TerminalOutput {
+    pub ended_at: Instant,
+    pub exit_status: Option<ExitStatus>,
+    pub was_content_truncated: bool,
+    pub original_content_len: usize,
+    pub content_line_count: usize,
+    pub finished_with_empty_output: bool,
+}
+
+impl Terminal {
+    pub fn new(
+        command: String,
+        working_dir: Option<PathBuf>,
+        terminal: Entity<terminal::Terminal>,
+        language_registry: Arc<LanguageRegistry>,
+        cx: &mut Context<Self>,
+    ) -> Self {
+        Self {
+            command: cx.new(|cx| {
+                Markdown::new(
+                    format!("```\n{}\n```", command).into(),
+                    Some(language_registry.clone()),
+                    None,
+                    cx,
+                )
+            }),
+            working_dir,
+            terminal,
+            started_at: Instant::now(),
+            output: None,
+        }
+    }
+
+    pub fn finish(
+        &mut self,
+        exit_status: Option<ExitStatus>,
+        original_content_len: usize,
+        truncated_content_len: usize,
+        content_line_count: usize,
+        finished_with_empty_output: bool,
+        cx: &mut Context<Self>,
+    ) {
+        self.output = Some(TerminalOutput {
+            ended_at: Instant::now(),
+            exit_status,
+            was_content_truncated: truncated_content_len < original_content_len,
+            original_content_len,
+            content_line_count,
+            finished_with_empty_output,
+        });
+        cx.notify();
+    }
+
+    pub fn command(&self) -> &Entity<Markdown> {
+        &self.command
+    }
+
+    pub fn working_dir(&self) -> &Option<PathBuf> {
+        &self.working_dir
+    }
+
+    pub fn started_at(&self) -> Instant {
+        self.started_at
+    }
+
+    pub fn output(&self) -> Option<&TerminalOutput> {
+        self.output.as_ref()
+    }
+
+    pub fn inner(&self) -> &Entity<terminal::Terminal> {
+        &self.terminal
+    }
+
+    pub fn to_markdown(&self, cx: &App) -> String {
+        format!(
+            "Terminal:\n```\n{}\n```\n",
+            self.terminal.read(cx).get_content()
+        )
+    }
+}

crates/action_log/Cargo.toml 🔗

@@ -0,0 +1,45 @@
+[package]
+name = "action_log"
+version = "0.1.0"
+edition.workspace = true
+publish.workspace = true
+license = "GPL-3.0-or-later"
+
+[lib]
+path = "src/action_log.rs"
+
+[lints]
+workspace = true
+
+[dependencies]
+anyhow.workspace = true
+buffer_diff.workspace = true
+clock.workspace = true
+collections.workspace = true
+futures.workspace = true
+gpui.workspace = true
+language.workspace = true
+project.workspace = true
+text.workspace = true
+util.workspace = true
+watch.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"] }
+log.workspace = true
+pretty_assertions.workspace = true
+project = { workspace = true, features = ["test-support"] }
+rand.workspace = true
+serde_json.workspace = true
+settings = { workspace = true, features = ["test-support"] }
+text = { workspace = true, features = ["test-support"] }
+util = { workspace = true, features = ["test-support"] }
+zlog.workspace = true

crates/assistant_tool/src/action_log.rs → crates/action_log/src/action_log.rs 🔗

@@ -17,8 +17,6 @@ use util::{
 pub struct ActionLog {
     /// Buffers that we want to notify the model about when they change.
     tracked_buffers: BTreeMap<Entity<Buffer>, TrackedBuffer>,
-    /// Has the model edited a file since it last checked diagnostics?
-    edited_since_project_diagnostics_check: bool,
     /// The project this action log is associated with
     project: Entity<Project>,
 }
@@ -28,7 +26,6 @@ impl ActionLog {
     pub fn new(project: Entity<Project>) -> Self {
         Self {
             tracked_buffers: BTreeMap::default(),
-            edited_since_project_diagnostics_check: false,
             project,
         }
     }
@@ -37,16 +34,6 @@ impl ActionLog {
         &self.project
     }
 
-    /// Notifies a diagnostics check
-    pub fn checked_project_diagnostics(&mut self) {
-        self.edited_since_project_diagnostics_check = false;
-    }
-
-    /// Returns true if any files have been edited since the last project diagnostics check
-    pub fn has_edited_files_since_project_diagnostics_check(&self) -> bool {
-        self.edited_since_project_diagnostics_check
-    }
-
     pub fn latest_snapshot(&self, buffer: &Entity<Buffer>) -> Option<text::BufferSnapshot> {
         Some(self.tracked_buffers.get(buffer)?.snapshot.clone())
     }
@@ -129,7 +116,7 @@ impl ActionLog {
             } else if buffer
                 .read(cx)
                 .file()
-                .map_or(false, |file| file.disk_state().exists())
+                .is_some_and(|file| file.disk_state().exists())
             {
                 TrackedBufferStatus::Created {
                     existing_file_content: Some(buffer.read(cx).as_rope().clone()),
@@ -174,7 +161,7 @@ impl ActionLog {
                     diff_base,
                     last_seen_base,
                     unreviewed_edits,
-                    snapshot: text_snapshot.clone(),
+                    snapshot: text_snapshot,
                     status,
                     version: buffer.read(cx).version(),
                     diff,
@@ -203,7 +190,7 @@ impl ActionLog {
         cx: &mut Context<Self>,
     ) {
         match event {
-            BufferEvent::Edited { .. } => self.handle_buffer_edited(buffer, cx),
+            BufferEvent::Edited => self.handle_buffer_edited(buffer, cx),
             BufferEvent::FileHandleChanged => {
                 self.handle_buffer_file_changed(buffer, cx);
             }
@@ -228,7 +215,7 @@ impl ActionLog {
                 if buffer
                     .read(cx)
                     .file()
-                    .map_or(false, |file| file.disk_state() == DiskState::Deleted)
+                    .is_some_and(|file| file.disk_state() == DiskState::Deleted)
                 {
                     // If the buffer had been edited by a tool, but it got
                     // deleted externally, we want to stop tracking it.
@@ -240,7 +227,7 @@ impl ActionLog {
                 if buffer
                     .read(cx)
                     .file()
-                    .map_or(false, |file| file.disk_state() != DiskState::Deleted)
+                    .is_some_and(|file| file.disk_state() != DiskState::Deleted)
                 {
                     // If the buffer had been deleted by a tool, but it got
                     // resurrected externally, we want to clear the edits we
@@ -277,15 +264,14 @@ impl ActionLog {
             if let Some((git_diff, (buffer_repo, _))) = git_diff.as_ref().zip(buffer_repo) {
                 cx.update(|cx| {
                     let mut old_head = buffer_repo.read(cx).head_commit.clone();
-                    Some(cx.subscribe(git_diff, move |_, event, cx| match event {
-                        buffer_diff::BufferDiffEvent::DiffChanged { .. } => {
+                    Some(cx.subscribe(git_diff, move |_, event, cx| {
+                        if let buffer_diff::BufferDiffEvent::DiffChanged { .. } = event {
                             let new_head = buffer_repo.read(cx).head_commit.clone();
                             if new_head != old_head {
                                 old_head = new_head;
                                 git_diff_updates_tx.send(()).ok();
                             }
                         }
-                        _ => {}
                     }))
                 })?
             } else {
@@ -303,7 +289,7 @@ impl ActionLog {
                 }
                 _ = git_diff_updates_rx.changed().fuse() => {
                     if let Some(git_diff) = git_diff.as_ref() {
-                        Self::keep_committed_edits(&this, &buffer, &git_diff, cx).await?;
+                        Self::keep_committed_edits(&this, &buffer, git_diff, cx).await?;
                     }
                 }
             }
@@ -475,7 +461,7 @@ impl ActionLog {
             anyhow::Ok((
                 tracked_buffer.diff.clone(),
                 buffer.read(cx).language().cloned(),
-                buffer.read(cx).language_registry().clone(),
+                buffer.read(cx).language_registry(),
             ))
         })??;
         let diff_snapshot = BufferDiff::update_diff(
@@ -511,7 +497,7 @@ impl ActionLog {
                                     new: new_range,
                                 },
                                 &new_diff_base,
-                                &buffer_snapshot.as_rope(),
+                                buffer_snapshot.as_rope(),
                             ));
                         }
                         unreviewed_edits
@@ -543,15 +529,12 @@ impl ActionLog {
 
     /// Mark a buffer as created by agent, so we can refresh it in the context
     pub fn buffer_created(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
-        self.edited_since_project_diagnostics_check = true;
-        self.track_buffer_internal(buffer.clone(), true, cx);
+        self.track_buffer_internal(buffer, true, cx);
     }
 
     /// Mark a buffer as edited by agent, so we can refresh it in the context
     pub fn buffer_edited(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
-        self.edited_since_project_diagnostics_check = true;
-
-        let tracked_buffer = self.track_buffer_internal(buffer.clone(), false, cx);
+        let tracked_buffer = self.track_buffer_internal(buffer, false, cx);
         if let TrackedBufferStatus::Deleted = tracked_buffer.status {
             tracked_buffer.status = TrackedBufferStatus::Modified;
         }
@@ -630,10 +613,10 @@ impl ActionLog {
                         false
                     }
                 });
-                if tracked_buffer.unreviewed_edits.is_empty() {
-                    if let TrackedBufferStatus::Created { .. } = &mut tracked_buffer.status {
-                        tracked_buffer.status = TrackedBufferStatus::Modified;
-                    }
+                if tracked_buffer.unreviewed_edits.is_empty()
+                    && let TrackedBufferStatus::Created { .. } = &mut tracked_buffer.status
+                {
+                    tracked_buffer.status = TrackedBufferStatus::Modified;
                 }
                 tracked_buffer.schedule_diff_update(ChangeAuthor::User, cx);
             }
@@ -827,7 +810,7 @@ impl ActionLog {
                 tracked.version != buffer.version
                     && buffer
                         .file()
-                        .map_or(false, |file| file.disk_state() != DiskState::Deleted)
+                        .is_some_and(|file| file.disk_state() != DiskState::Deleted)
             })
             .map(|(buffer, _)| buffer)
     }
@@ -863,7 +846,7 @@ fn apply_non_conflicting_edits(
                 conflict = true;
                 if new_edits
                     .peek()
-                    .map_or(false, |next_edit| next_edit.old.overlaps(&old_edit.new))
+                    .is_some_and(|next_edit| next_edit.old.overlaps(&old_edit.new))
                 {
                     new_edit = new_edits.next().unwrap();
                 } else {
@@ -980,7 +963,7 @@ impl TrackedBuffer {
     fn has_edits(&self, cx: &App) -> bool {
         self.diff
             .read(cx)
-            .hunks(&self.buffer.read(cx), cx)
+            .hunks(self.buffer.read(cx), cx)
             .next()
             .is_some()
     }
@@ -2284,7 +2267,7 @@ mod tests {
             log::info!("quiescing...");
             cx.run_until_parked();
             action_log.update(cx, |log, cx| {
-                let tracked_buffer = log.tracked_buffers.get(&buffer).unwrap();
+                let tracked_buffer = log.tracked_buffers.get(buffer).unwrap();
                 let mut old_text = tracked_buffer.diff_base.clone();
                 let new_text = buffer.read(cx).as_rope();
                 for edit in tracked_buffer.unreviewed_edits.edits() {
@@ -2442,7 +2425,7 @@ mod tests {
         assert_eq!(
             unreviewed_hunks(&action_log, cx),
             vec![(
-                buffer.clone(),
+                buffer,
                 vec![
                     HunkStatus {
                         range: Point::new(6, 0)..Point::new(7, 0),

crates/activity_indicator/src/activity_indicator.rs 🔗

@@ -103,26 +103,21 @@ impl ActivityIndicator {
             cx.subscribe_in(
                 &workspace_handle,
                 window,
-                |activity_indicator, _, event, window, cx| match event {
-                    workspace::Event::ClearActivityIndicator { .. } => {
-                        if activity_indicator.statuses.pop().is_some() {
-                            activity_indicator.dismiss_error_message(
-                                &DismissErrorMessage,
-                                window,
-                                cx,
-                            );
-                            cx.notify();
-                        }
+                |activity_indicator, _, event, window, cx| {
+                    if let workspace::Event::ClearActivityIndicator = event
+                        && activity_indicator.statuses.pop().is_some()
+                    {
+                        activity_indicator.dismiss_error_message(&DismissErrorMessage, window, cx);
+                        cx.notify();
                     }
-                    _ => {}
                 },
             )
             .detach();
 
             cx.subscribe(
                 &project.read(cx).lsp_store(),
-                |activity_indicator, _, event, cx| match event {
-                    LspStoreEvent::LanguageServerUpdate { name, message, .. } => {
+                |activity_indicator, _, event, cx| {
+                    if let LspStoreEvent::LanguageServerUpdate { name, message, .. } = event {
                         if let proto::update_language_server::Variant::StatusUpdate(status_update) =
                             message
                         {
@@ -191,7 +186,6 @@ impl ActivityIndicator {
                         }
                         cx.notify()
                     }
-                    _ => {}
                 },
             )
             .detach();
@@ -206,9 +200,10 @@ impl ActivityIndicator {
 
             cx.subscribe(
                 &project.read(cx).git_store().clone(),
-                |_, _, event: &GitStoreEvent, cx| match event {
-                    project::git_store::GitStoreEvent::JobsUpdated => cx.notify(),
-                    _ => {}
+                |_, _, event: &GitStoreEvent, cx| {
+                    if let project::git_store::GitStoreEvent::JobsUpdated = event {
+                        cx.notify()
+                    }
                 },
             )
             .detach();
@@ -458,26 +453,24 @@ impl ActivityIndicator {
             .map(|r| r.read(cx))
             .and_then(Repository::current_job);
         // Show any long-running git command
-        if let Some(job_info) = current_job {
-            if Instant::now() - job_info.start >= GIT_OPERATION_DELAY {
-                return Some(Content {
-                    icon: Some(
-                        Icon::new(IconName::ArrowCircle)
-                            .size(IconSize::Small)
-                            .with_animation(
-                                "arrow-circle",
-                                Animation::new(Duration::from_secs(2)).repeat(),
-                                |icon, delta| {
-                                    icon.transform(Transformation::rotate(percentage(delta)))
-                                },
-                            )
-                            .into_any_element(),
-                    ),
-                    message: job_info.message.into(),
-                    on_click: None,
-                    tooltip_message: None,
-                });
-            }
+        if let Some(job_info) = current_job
+            && Instant::now() - job_info.start >= GIT_OPERATION_DELAY
+        {
+            return Some(Content {
+                icon: Some(
+                    Icon::new(IconName::ArrowCircle)
+                        .size(IconSize::Small)
+                        .with_animation(
+                            "arrow-circle",
+                            Animation::new(Duration::from_secs(2)).repeat(),
+                            |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
+                        )
+                        .into_any_element(),
+                ),
+                message: job_info.message.into(),
+                on_click: None,
+                tooltip_message: None,
+            });
         }
 
         // Show any language server installation info.
@@ -702,7 +695,7 @@ impl ActivityIndicator {
                     on_click: Some(Arc::new(|this, window, cx| {
                         this.dismiss_error_message(&DismissErrorMessage, window, cx)
                     })),
-                    tooltip_message: Some(Self::version_tooltip_message(&version)),
+                    tooltip_message: Some(Self::version_tooltip_message(version)),
                 }),
                 AutoUpdateStatus::Installing { version } => Some(Content {
                     icon: Some(
@@ -714,21 +707,13 @@ impl ActivityIndicator {
                     on_click: Some(Arc::new(|this, window, cx| {
                         this.dismiss_error_message(&DismissErrorMessage, window, cx)
                     })),
-                    tooltip_message: Some(Self::version_tooltip_message(&version)),
+                    tooltip_message: Some(Self::version_tooltip_message(version)),
                 }),
-                AutoUpdateStatus::Updated {
-                    binary_path,
-                    version,
-                } => Some(Content {
+                AutoUpdateStatus::Updated { version } => Some(Content {
                     icon: None,
                     message: "Click to restart and update Zed".to_string(),
-                    on_click: Some(Arc::new({
-                        let reload = workspace::Reload {
-                            binary_path: Some(binary_path.clone()),
-                        };
-                        move |_, _, cx| workspace::reload(&reload, cx)
-                    })),
-                    tooltip_message: Some(Self::version_tooltip_message(&version)),
+                    on_click: Some(Arc::new(move |_, _, cx| workspace::reload(cx))),
+                    tooltip_message: Some(Self::version_tooltip_message(version)),
                 }),
                 AutoUpdateStatus::Errored => Some(Content {
                     icon: Some(
@@ -748,21 +733,20 @@ impl ActivityIndicator {
 
         if let Some(extension_store) =
             ExtensionStore::try_global(cx).map(|extension_store| extension_store.read(cx))
+            && let Some(extension_id) = extension_store.outstanding_operations().keys().next()
         {
-            if let Some(extension_id) = extension_store.outstanding_operations().keys().next() {
-                return Some(Content {
-                    icon: Some(
-                        Icon::new(IconName::Download)
-                            .size(IconSize::Small)
-                            .into_any_element(),
-                    ),
-                    message: format!("Updating {extension_id} extension…"),
-                    on_click: Some(Arc::new(|this, window, cx| {
-                        this.dismiss_error_message(&DismissErrorMessage, window, cx)
-                    })),
-                    tooltip_message: None,
-                });
-            }
+            return Some(Content {
+                icon: Some(
+                    Icon::new(IconName::Download)
+                        .size(IconSize::Small)
+                        .into_any_element(),
+                ),
+                message: format!("Updating {extension_id} extension…"),
+                on_click: Some(Arc::new(|this, window, cx| {
+                    this.dismiss_error_message(&DismissErrorMessage, window, cx)
+                })),
+                tooltip_message: None,
+            });
         }
 
         None

crates/agent/Cargo.toml 🔗

@@ -19,6 +19,7 @@ test-support = [
 ]
 
 [dependencies]
+action_log.workspace = true
 agent_settings.workspace = true
 anyhow.workspace = true
 assistant_context.workspace = true
@@ -30,7 +31,6 @@ collections.workspace = true
 component.workspace = true
 context_server.workspace = true
 convert_case.workspace = true
-feature_flags.workspace = true
 fs.workspace = true
 futures.workspace = true
 git.workspace = true

crates/agent/src/agent_profile.rs 🔗

@@ -90,7 +90,7 @@ impl AgentProfile {
             return false;
         };
 
-        return Self::is_enabled(settings, source, tool_name);
+        Self::is_enabled(settings, source, tool_name)
     }
 
     fn is_enabled(settings: &AgentProfileSettings, source: ToolSource, name: String) -> bool {
@@ -132,7 +132,7 @@ mod tests {
         });
         let tool_set = default_tool_set(cx);
 
-        let profile = AgentProfile::new(id.clone(), tool_set);
+        let profile = AgentProfile::new(id, tool_set);
 
         let mut enabled_tools = cx
             .read(|cx| profile.enabled_tools(cx))
@@ -169,7 +169,7 @@ mod tests {
         });
         let tool_set = default_tool_set(cx);
 
-        let profile = AgentProfile::new(id.clone(), tool_set);
+        let profile = AgentProfile::new(id, tool_set);
 
         let mut enabled_tools = cx
             .read(|cx| profile.enabled_tools(cx))
@@ -202,7 +202,7 @@ mod tests {
         });
         let tool_set = default_tool_set(cx);
 
-        let profile = AgentProfile::new(id.clone(), tool_set);
+        let profile = AgentProfile::new(id, tool_set);
 
         let mut enabled_tools = cx
             .read(|cx| profile.enabled_tools(cx))
@@ -326,7 +326,7 @@ mod tests {
             _input: serde_json::Value,
             _request: Arc<language_model::LanguageModelRequest>,
             _project: Entity<Project>,
-            _action_log: Entity<assistant_tool::ActionLog>,
+            _action_log: Entity<action_log::ActionLog>,
             _model: Arc<dyn language_model::LanguageModel>,
             _window: Option<gpui::AnyWindowHandle>,
             _cx: &mut App,

crates/agent/src/context.rs 🔗

@@ -20,7 +20,7 @@ use text::{Anchor, OffsetRangeExt as _};
 use util::markdown::MarkdownCodeBlock;
 use util::{ResultExt as _, post_inc};
 
-pub const RULES_ICON: IconName = IconName::Context;
+pub const RULES_ICON: IconName = IconName::Reader;
 
 pub enum ContextKind {
     File,
@@ -40,8 +40,8 @@ impl ContextKind {
             ContextKind::File => IconName::File,
             ContextKind::Directory => IconName::Folder,
             ContextKind::Symbol => IconName::Code,
-            ContextKind::Selection => IconName::Context,
-            ContextKind::FetchedUrl => IconName::Globe,
+            ContextKind::Selection => IconName::Reader,
+            ContextKind::FetchedUrl => IconName::ToolWeb,
             ContextKind::Thread => IconName::Thread,
             ContextKind::TextThread => IconName::TextThread,
             ContextKind::Rules => RULES_ICON,
@@ -201,24 +201,24 @@ impl FileContextHandle {
                         parse_status.changed().await.log_err();
                     }
 
-                    if let Ok(snapshot) = buffer.read_with(cx, |buffer, _| buffer.snapshot()) {
-                        if let Some(outline) = snapshot.outline(None) {
-                            let items = outline
-                                .items
-                                .into_iter()
-                                .map(|item| item.to_point(&snapshot));
-
-                            if let Ok(outline_text) =
-                                outline::render_outline(items, None, 0, usize::MAX).await
-                            {
-                                let context = AgentContext::File(FileContext {
-                                    handle: self,
-                                    full_path,
-                                    text: outline_text.into(),
-                                    is_outline: true,
-                                });
-                                return Some((context, vec![buffer]));
-                            }
+                    if let Ok(snapshot) = buffer.read_with(cx, |buffer, _| buffer.snapshot())
+                        && let Some(outline) = snapshot.outline(None)
+                    {
+                        let items = outline
+                            .items
+                            .into_iter()
+                            .map(|item| item.to_point(&snapshot));
+
+                        if let Ok(outline_text) =
+                            outline::render_outline(items, None, 0, usize::MAX).await
+                        {
+                            let context = AgentContext::File(FileContext {
+                                handle: self,
+                                full_path,
+                                text: outline_text.into(),
+                                is_outline: true,
+                            });
+                            return Some((context, vec![buffer]));
                         }
                     }
                 }
@@ -362,7 +362,7 @@ impl Display for DirectoryContext {
         let mut is_first = true;
         for descendant in &self.descendants {
             if !is_first {
-                write!(f, "\n")?;
+                writeln!(f)?;
             } else {
                 is_first = false;
             }
@@ -650,7 +650,7 @@ impl TextThreadContextHandle {
 impl Display for TextThreadContext {
     fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
         // TODO: escape title?
-        write!(f, "<text_thread title=\"{}\">\n", self.title)?;
+        writeln!(f, "<text_thread title=\"{}\">", self.title)?;
         write!(f, "{}", self.text.trim())?;
         write!(f, "\n</text_thread>")
     }
@@ -716,7 +716,7 @@ impl RulesContextHandle {
 impl Display for RulesContext {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         if let Some(title) = &self.title {
-            write!(f, "Rules title: {}\n", title)?;
+            writeln!(f, "Rules title: {}", title)?;
         }
         let code_block = MarkdownCodeBlock {
             tag: "",

crates/agent/src/context_server_tool.rs 🔗

@@ -1,7 +1,8 @@
 use std::sync::Arc;
 
+use action_log::ActionLog;
 use anyhow::{Result, anyhow, bail};
-use assistant_tool::{ActionLog, Tool, ToolResult, ToolSource};
+use assistant_tool::{Tool, ToolResult, ToolSource};
 use context_server::{ContextServerId, types};
 use gpui::{AnyWindowHandle, App, Entity, Task};
 use icons::IconName;
@@ -85,15 +86,13 @@ impl Tool for ContextServerTool {
     ) -> ToolResult {
         if let Some(server) = self.store.read(cx).get_running_server(&self.server_id) {
             let tool_name = self.tool.name.clone();
-            let server_clone = server.clone();
-            let input_clone = input.clone();
 
             cx.spawn(async move |_cx| {
-                let Some(protocol) = server_clone.client() else {
+                let Some(protocol) = server.client() else {
                     bail!("Context server not initialized");
                 };
 
-                let arguments = if let serde_json::Value::Object(map) = input_clone {
+                let arguments = if let serde_json::Value::Object(map) = input {
                     Some(map.into_iter().collect())
                 } else {
                     None

crates/agent/src/context_store.rs 🔗

@@ -338,11 +338,9 @@ impl ContextStore {
             image_task,
             context_id: self.next_context_id.post_inc(),
         });
-        if self.has_context(&context) {
-            if remove_if_exists {
-                self.remove_context(&context, cx);
-                return None;
-            }
+        if self.has_context(&context) && remove_if_exists {
+            self.remove_context(&context, cx);
+            return None;
         }
 
         self.insert_context(context.clone(), cx);

crates/agent/src/history_store.rs 🔗

@@ -212,7 +212,16 @@ impl HistoryStore {
     fn load_recently_opened_entries(cx: &AsyncApp) -> Task<Result<Vec<HistoryEntryId>>> {
         cx.background_spawn(async move {
             let path = paths::data_dir().join(NAVIGATION_HISTORY_PATH);
-            let contents = smol::fs::read_to_string(path).await?;
+            let contents = match smol::fs::read_to_string(path).await {
+                Ok(it) => it,
+                Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
+                    return Ok(Vec::new());
+                }
+                Err(e) => {
+                    return Err(e)
+                        .context("deserializing persisted agent panel navigation history");
+                }
+            };
             let entries = serde_json::from_str::<Vec<SerializedRecentOpen>>(&contents)
                 .context("deserializing persisted agent panel navigation history")?
                 .into_iter()
@@ -245,10 +254,9 @@ impl HistoryStore {
     }
 
     pub fn remove_recently_opened_thread(&mut self, id: ThreadId, cx: &mut Context<Self>) {
-        self.recently_opened_entries.retain(|entry| match entry {
-            HistoryEntryId::Thread(thread_id) if thread_id == &id => false,
-            _ => true,
-        });
+        self.recently_opened_entries.retain(
+            |entry| !matches!(entry, HistoryEntryId::Thread(thread_id) if thread_id == &id),
+        );
         self.save_recently_opened_entries(cx);
     }
 

crates/agent/src/thread.rs 🔗

@@ -8,14 +8,17 @@ use crate::{
     },
     tool_use::{PendingToolUse, ToolUse, ToolUseMetadata, ToolUseState},
 };
-use agent_settings::{AgentProfileId, AgentSettings, CompletionMode, SUMMARIZE_THREAD_PROMPT};
+use action_log::ActionLog;
+use agent_settings::{
+    AgentProfileId, AgentSettings, CompletionMode, SUMMARIZE_THREAD_DETAILED_PROMPT,
+    SUMMARIZE_THREAD_PROMPT,
+};
 use anyhow::{Result, anyhow};
-use assistant_tool::{ActionLog, AnyToolCard, Tool, ToolWorkingSet};
+use assistant_tool::{AnyToolCard, Tool, ToolWorkingSet};
 use chrono::{DateTime, Utc};
 use client::{ModelRequestUsage, RequestUsage};
 use cloud_llm_client::{CompletionIntent, CompletionRequestStatus, Plan, UsageLimit};
 use collections::HashMap;
-use feature_flags::{self, FeatureFlagAppExt};
 use futures::{FutureExt, StreamExt as _, future::Shared};
 use git::repository::DiffType;
 use gpui::{
@@ -107,7 +110,7 @@ impl std::fmt::Display for PromptId {
 }
 
 #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize)]
-pub struct MessageId(pub(crate) usize);
+pub struct MessageId(pub usize);
 
 impl MessageId {
     fn post_inc(&mut self) -> Self {
@@ -178,7 +181,7 @@ impl Message {
         }
     }
 
-    pub fn to_string(&self) -> String {
+    pub fn to_message_content(&self) -> String {
         let mut result = String::new();
 
         if !self.loaded_context.text.is_empty() {
@@ -384,10 +387,8 @@ pub struct Thread {
     cumulative_token_usage: TokenUsage,
     exceeded_window_error: Option<ExceededWindowError>,
     tool_use_limit_reached: bool,
-    feedback: Option<ThreadFeedback>,
     retry_state: Option<RetryState>,
     message_feedback: HashMap<MessageId, ThreadFeedback>,
-    last_auto_capture_at: Option<Instant>,
     last_received_chunk_at: Option<Instant>,
     request_callback: Option<
         Box<dyn FnMut(&LanguageModelRequest, &[Result<LanguageModelCompletionEvent, String>])>,
@@ -485,15 +486,13 @@ impl Thread {
             cumulative_token_usage: TokenUsage::default(),
             exceeded_window_error: None,
             tool_use_limit_reached: false,
-            feedback: None,
             retry_state: None,
             message_feedback: HashMap::default(),
-            last_auto_capture_at: None,
             last_error_context: None,
             last_received_chunk_at: None,
             request_callback: None,
             remaining_turns: u32::MAX,
-            configured_model: configured_model.clone(),
+            configured_model,
             profile: AgentProfile::new(profile_id, tools),
         }
     }
@@ -531,7 +530,7 @@ impl Thread {
                 .and_then(|model| {
                     let model = SelectedModel {
                         provider: model.provider.clone().into(),
-                        model: model.model.clone().into(),
+                        model: model.model.into(),
                     };
                     registry.select_model(&model, cx)
                 })
@@ -611,9 +610,7 @@ impl Thread {
             cumulative_token_usage: serialized.cumulative_token_usage,
             exceeded_window_error: None,
             tool_use_limit_reached: serialized.tool_use_limit_reached,
-            feedback: None,
             message_feedback: HashMap::default(),
-            last_auto_capture_at: None,
             last_error_context: None,
             last_received_chunk_at: None,
             request_callback: None,
@@ -843,11 +840,17 @@ impl Thread {
                     .await
                     .unwrap_or(false);
 
-                if !equal {
-                    this.update(cx, |this, cx| {
-                        this.insert_checkpoint(pending_checkpoint, 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,
+                        })
+                    }
+                })?;
 
                 Ok(())
             }
@@ -1026,8 +1029,6 @@ impl Thread {
             });
         }
 
-        self.auto_capture_telemetry(cx);
-
         message_id
     }
 
@@ -1642,17 +1643,15 @@ impl Thread {
         };
 
         self.tool_use
-            .request_tool_use(tool_message_id, tool_use, tool_use_metadata.clone(), cx);
+            .request_tool_use(tool_message_id, tool_use, tool_use_metadata, cx);
 
-        let pending_tool_use = self.tool_use.insert_tool_output(
-            tool_use_id.clone(),
+        self.tool_use.insert_tool_output(
+            tool_use_id,
             tool_name,
             tool_output,
             self.configured_model.as_ref(),
             self.completion_mode,
-        );
-
-        pending_tool_use
+        )
     }
 
     pub fn stream_completion(
@@ -1685,7 +1684,7 @@ impl Thread {
         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 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 {
@@ -1817,7 +1816,7 @@ impl Thread {
                                 let streamed_input = if tool_use.is_input_complete {
                                     None
                                 } else {
-                                    Some((&tool_use.input).clone())
+                                    Some(tool_use.input.clone())
                                 };
 
                                 let ui_text = thread.tool_use.request_tool_use(
@@ -1899,7 +1898,6 @@ impl Thread {
                         cx.emit(ThreadEvent::StreamedCompletion);
                         cx.notify();
 
-                        thread.auto_capture_telemetry(cx);
                         Ok(())
                     })??;
 
@@ -1967,11 +1965,9 @@ impl Thread {
 
                                                 if let Some(prev_message) =
                                                     thread.messages.get(ix - 1)
-                                                {
-                                                    if prev_message.role == Role::Assistant {
+                                                    && prev_message.role == Role::Assistant {
                                                         break;
                                                     }
-                                                }
                                             }
                                         }
 
@@ -2044,7 +2040,7 @@ impl Thread {
 
                                             retry_scheduled = thread
                                                 .handle_retryable_error_with_delay(
-                                                    &completion_error,
+                                                    completion_error,
                                                     Some(retry_strategy),
                                                     model.clone(),
                                                     intent,
@@ -2074,8 +2070,6 @@ impl Thread {
                         request_callback(request, response_events);
                     }
 
-                    thread.auto_capture_telemetry(cx);
-
                     if let Ok(initial_usage) = initial_token_usage {
                         let usage = thread.cumulative_token_usage - initial_usage;
 
@@ -2123,7 +2117,7 @@ impl Thread {
 
         self.pending_summary = cx.spawn(async move |this, cx| {
             let result = async {
-                let mut messages = model.model.stream_completion(request, &cx).await?;
+                let mut messages = model.model.stream_completion(request, cx).await?;
 
                 let mut new_summary = String::new();
                 while let Some(event) = messages.next().await {
@@ -2267,6 +2261,15 @@ impl Thread {
                     max_attempts: 3,
                 })
             }
+            Other(err)
+                if err.is::<PaymentRequiredError>()
+                    || err.is::<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,
@@ -2422,12 +2425,10 @@ impl Thread {
             return;
         }
 
-        let added_user_message = include_str!("./prompts/summarize_thread_detailed_prompt.txt");
-
         let request = self.to_summarize_request(
             &model,
             CompletionIntent::ThreadContextSummarization,
-            added_user_message.into(),
+            SUMMARIZE_THREAD_DETAILED_PROMPT.into(),
             cx,
         );
 
@@ -2440,7 +2441,7 @@ impl Thread {
         // which result to prefer (the old task could complete after the new one, resulting in a
         // stale summary).
         self.detailed_summary_task = cx.spawn(async move |thread, cx| {
-            let stream = model.stream_completion_text(request, &cx);
+            let stream = model.stream_completion_text(request, cx);
             let Some(mut messages) = stream.await.log_err() else {
                 thread
                     .update(cx, |thread, _cx| {
@@ -2469,13 +2470,13 @@ impl Thread {
                 .ok()?;
 
             // Save thread so its summary can be reused later
-            if let Some(thread) = thread.upgrade() {
-                if let Ok(Ok(save_task)) = cx.update(|cx| {
+            if let Some(thread) = thread.upgrade()
+                && let Ok(Ok(save_task)) = cx.update(|cx| {
                     thread_store
                         .update(cx, |thread_store, cx| thread_store.save_thread(&thread, cx))
-                }) {
-                    save_task.await.log_err();
-                }
+                })
+            {
+                save_task.await.log_err();
             }
 
             Some(())
@@ -2520,7 +2521,6 @@ impl Thread {
         model: Arc<dyn LanguageModel>,
         cx: &mut Context<Self>,
     ) -> Vec<PendingToolUse> {
-        self.auto_capture_telemetry(cx);
         let request =
             Arc::new(self.to_completion_request(model.clone(), CompletionIntent::ToolResults, cx));
         let pending_tool_uses = self
@@ -2724,13 +2724,11 @@ impl Thread {
         window: Option<AnyWindowHandle>,
         cx: &mut Context<Self>,
     ) {
-        if self.all_tools_finished() {
-            if let Some(ConfiguredModel { model, .. }) = self.configured_model.as_ref() {
-                if !canceled {
-                    self.send_to_model(model.clone(), CompletionIntent::ToolResults, window, cx);
-                }
-                self.auto_capture_telemetry(cx);
-            }
+        if self.all_tools_finished()
+            && let Some(ConfiguredModel { model, .. }) = self.configured_model.as_ref()
+            && !canceled
+        {
+            self.send_to_model(model.clone(), CompletionIntent::ToolResults, window, cx);
         }
 
         cx.emit(ThreadEvent::ToolFinished {
@@ -2786,10 +2784,6 @@ impl Thread {
         cx.emit(ThreadEvent::CancelEditing);
     }
 
-    pub fn feedback(&self) -> Option<ThreadFeedback> {
-        self.feedback
-    }
-
     pub fn message_feedback(&self, message_id: MessageId) -> Option<ThreadFeedback> {
         self.message_feedback.get(&message_id).copied()
     }
@@ -2822,7 +2816,7 @@ impl Thread {
 
         let message_content = self
             .message(message_id)
-            .map(|msg| msg.to_string())
+            .map(|msg| msg.to_message_content())
             .unwrap_or_default();
 
         cx.background_spawn(async move {
@@ -2851,52 +2845,6 @@ impl Thread {
         })
     }
 
-    pub fn report_feedback(
-        &mut self,
-        feedback: ThreadFeedback,
-        cx: &mut Context<Self>,
-    ) -> Task<Result<()>> {
-        let last_assistant_message_id = self
-            .messages
-            .iter()
-            .rev()
-            .find(|msg| msg.role == Role::Assistant)
-            .map(|msg| msg.id);
-
-        if let Some(message_id) = last_assistant_message_id {
-            self.report_message_feedback(message_id, feedback, cx)
-        } else {
-            let final_project_snapshot = Self::project_snapshot(self.project.clone(), cx);
-            let serialized_thread = self.serialize(cx);
-            let thread_id = self.id().clone();
-            let client = self.project.read(cx).client();
-            self.feedback = Some(feedback);
-            cx.notify();
-
-            cx.background_spawn(async move {
-                let final_project_snapshot = final_project_snapshot.await;
-                let serialized_thread = serialized_thread.await?;
-                let thread_data = serde_json::to_value(serialized_thread)
-                    .unwrap_or_else(|_| serde_json::Value::Null);
-
-                let rating = match feedback {
-                    ThreadFeedback::Positive => "positive",
-                    ThreadFeedback::Negative => "negative",
-                };
-                telemetry::event!(
-                    "Assistant Thread Rated",
-                    rating,
-                    thread_id,
-                    thread_data,
-                    final_project_snapshot
-                );
-                client.telemetry().flush_events().await;
-
-                Ok(())
-            })
-        }
-    }
-
     /// Create a snapshot of the current project state including git information and unsaved buffers.
     fn project_snapshot(
         project: Entity<Project>,
@@ -2917,11 +2865,11 @@ impl Thread {
                 let buffer_store = project.read(app_cx).buffer_store();
                 for buffer_handle in buffer_store.read(app_cx).buffers() {
                     let buffer = buffer_handle.read(app_cx);
-                    if buffer.is_dirty() {
-                        if let Some(file) = buffer.file() {
-                            let path = file.path().to_string_lossy().to_string();
-                            unsaved_buffers.push(path);
-                        }
+                    if buffer.is_dirty()
+                        && let Some(file) = buffer.file()
+                    {
+                        let path = file.path().to_string_lossy().to_string();
+                        unsaved_buffers.push(path);
                     }
                 }
             })
@@ -3131,50 +3079,6 @@ impl Thread {
         &self.project
     }
 
-    pub fn auto_capture_telemetry(&mut self, cx: &mut Context<Self>) {
-        if !cx.has_flag::<feature_flags::ThreadAutoCaptureFeatureFlag>() {
-            return;
-        }
-
-        let now = Instant::now();
-        if let Some(last) = self.last_auto_capture_at {
-            if now.duration_since(last).as_secs() < 10 {
-                return;
-            }
-        }
-
-        self.last_auto_capture_at = Some(now);
-
-        let thread_id = self.id().clone();
-        let github_login = self
-            .project
-            .read(cx)
-            .user_store()
-            .read(cx)
-            .current_user()
-            .map(|user| user.github_login.clone());
-        let client = self.project.read(cx).client();
-        let serialize_task = self.serialize(cx);
-
-        cx.background_executor()
-            .spawn(async move {
-                if let Ok(serialized_thread) = serialize_task.await {
-                    if let Ok(thread_data) = serde_json::to_value(serialized_thread) {
-                        telemetry::event!(
-                            "Agent Thread Auto-Captured",
-                            thread_id = thread_id.to_string(),
-                            thread_data = thread_data,
-                            auto_capture_reason = "tracked_user",
-                            github_login = github_login
-                        );
-
-                        client.telemetry().flush_events().await;
-                    }
-                }
-            })
-            .detach();
-    }
-
     pub fn cumulative_token_usage(&self) -> TokenUsage {
         self.cumulative_token_usage
     }
@@ -3217,13 +3121,13 @@ impl Thread {
             .model
             .max_token_count_for_mode(self.completion_mode().into());
 
-        if let Some(exceeded_error) = &self.exceeded_window_error {
-            if model.model.id() == exceeded_error.model_id {
-                return Some(TotalTokenUsage {
-                    total: exceeded_error.token_count,
-                    max,
-                });
-            }
+        if let Some(exceeded_error) = &self.exceeded_window_error
+            && model.model.id() == exceeded_error.model_id
+        {
+            return Some(TotalTokenUsage {
+                total: exceeded_error.token_count,
+                max,
+            });
         }
 
         let total = self
@@ -3284,7 +3188,7 @@ impl Thread {
             self.configured_model.as_ref(),
             self.completion_mode,
         );
-        self.tool_finished(tool_use_id.clone(), None, true, window, cx);
+        self.tool_finished(tool_use_id, None, true, window, cx);
     }
 }
 
@@ -3916,7 +3820,7 @@ fn main() {{
                 AgentSettings {
                     model_parameters: vec![LanguageModelParameters {
                         provider: Some(model.provider_id().0.to_string().into()),
-                        model: Some(model.id().0.clone()),
+                        model: Some(model.id().0),
                         temperature: Some(0.66),
                     }],
                     ..AgentSettings::get_global(cx).clone()
@@ -3936,7 +3840,7 @@ fn main() {{
                 AgentSettings {
                     model_parameters: vec![LanguageModelParameters {
                         provider: None,
-                        model: Some(model.id().0.clone()),
+                        model: Some(model.id().0),
                         temperature: Some(0.66),
                     }],
                     ..AgentSettings::get_global(cx).clone()
@@ -3976,7 +3880,7 @@ fn main() {{
                 AgentSettings {
                     model_parameters: vec![LanguageModelParameters {
                         provider: Some("anthropic".into()),
-                        model: Some(model.id().0.clone()),
+                        model: Some(model.id().0),
                         temperature: Some(0.66),
                     }],
                     ..AgentSettings::get_global(cx).clone()
@@ -4027,7 +3931,7 @@ fn main() {{
         });
 
         let fake_model = model.as_fake();
-        simulate_successful_response(&fake_model, cx);
+        simulate_successful_response(fake_model, cx);
 
         // Should start generating summary when there are >= 2 messages
         thread.read_with(cx, |thread, _| {
@@ -4122,7 +4026,7 @@ fn main() {{
         });
 
         let fake_model = model.as_fake();
-        simulate_successful_response(&fake_model, cx);
+        simulate_successful_response(fake_model, cx);
 
         thread.read_with(cx, |thread, _| {
             // State is still Error, not Generating
@@ -5321,7 +5225,7 @@ fn main() {{
     }
 
     #[gpui::test]
-    async fn test_retry_cancelled_on_stop(cx: &mut TestAppContext) {
+    async fn test_retry_canceled_on_stop(cx: &mut TestAppContext) {
         init_test_settings(cx);
 
         let project = create_test_project(cx, json!({})).await;
@@ -5377,7 +5281,7 @@ fn main() {{
             "Should have no pending completions after cancellation"
         );
 
-        // Verify the retry was cancelled by checking retry state
+        // Verify the retry was canceled by checking retry state
         thread.read_with(cx, |thread, _| {
             if let Some(retry_state) = &thread.retry_state {
                 panic!(
@@ -5404,7 +5308,7 @@ fn main() {{
         });
 
         let fake_model = model.as_fake();
-        simulate_successful_response(&fake_model, cx);
+        simulate_successful_response(fake_model, cx);
 
         thread.read_with(cx, |thread, _| {
             assert!(matches!(thread.summary(), ThreadSummary::Generating));

crates/agent/src/thread_store.rs 🔗

@@ -42,7 +42,7 @@ use std::{
 use util::ResultExt as _;
 
 pub static ZED_STATELESS: std::sync::LazyLock<bool> =
-    std::sync::LazyLock::new(|| std::env::var("ZED_STATELESS").map_or(false, |v| !v.is_empty()));
+    std::sync::LazyLock::new(|| std::env::var("ZED_STATELESS").is_ok_and(|v| !v.is_empty()));
 
 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
 pub enum DataType {
@@ -74,7 +74,7 @@ impl Column for DataType {
     }
 }
 
-const RULES_FILE_NAMES: [&'static str; 9] = [
+const RULES_FILE_NAMES: [&str; 9] = [
     ".rules",
     ".cursorrules",
     ".windsurfrules",
@@ -205,6 +205,22 @@ impl ThreadStore {
         (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>,
@@ -565,33 +581,32 @@ impl ThreadStore {
                 return;
             };
 
-            if protocol.capable(context_server::protocol::ServerCapability::Tools) {
-                if let Some(response) = protocol
+            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();
-                    }
+            {
+                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();
                 }
             }
         })
@@ -681,13 +696,14 @@ impl SerializedThreadV0_1_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() {
-                if let Some(last_message) = messages.last_mut() {
-                    debug_assert!(last_message.role == Role::Assistant);
-
-                    last_message.tool_results = message.tool_results;
-                    continue;
-                }
+            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);
@@ -877,7 +893,7 @@ impl ThreadsDatabase {
 
         let needs_migration_from_heed = mdb_path.exists();
 
-        let connection = if *ZED_STATELESS {
+        let connection = if *ZED_STATELESS || cfg!(any(feature = "test-support", test)) {
             Connection::open_memory(Some("THREAD_FALLBACK_DB"))
         } else {
             Connection::open_file(&sqlite_path.to_string_lossy())

crates/agent/src/tool_use.rs 🔗

@@ -112,19 +112,13 @@ impl ToolUseState {
                                 },
                             );
 
-                            if let Some(window) = &mut window {
-                                if let Some(tool) = this.tools.read(cx).tool(tool_use, cx) {
-                                    if let Some(output) = tool_result.output.clone() {
-                                        if let Some(card) = tool.deserialize_card(
-                                            output,
-                                            project.clone(),
-                                            window,
-                                            cx,
-                                        ) {
-                                            this.tool_result_cards.insert(tool_use_id, card);
-                                        }
-                                    }
-                                }
+                            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);
                             }
                         }
                     }
@@ -137,7 +131,7 @@ impl ToolUseState {
     }
 
     pub fn cancel_pending(&mut self) -> Vec<PendingToolUse> {
-        let mut cancelled_tool_uses = Vec::new();
+        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 { .. }) {
@@ -155,10 +149,10 @@ impl ToolUseState {
                         is_error: true,
                     },
                 );
-                cancelled_tool_uses.push(tool_use.clone());
+                canceled_tool_uses.push(tool_use.clone());
                 false
             });
-        cancelled_tool_uses
+        canceled_tool_uses
     }
 
     pub fn pending_tool_uses(&self) -> Vec<&PendingToolUse> {
@@ -281,7 +275,7 @@ impl ToolUseState {
     pub fn message_has_tool_results(&self, assistant_message_id: MessageId) -> bool {
         self.tool_uses_by_assistant_message
             .get(&assistant_message_id)
-            .map_or(false, |results| !results.is_empty())
+            .is_some_and(|results| !results.is_empty())
     }
 
     pub fn tool_result(

crates/agent2/Cargo.toml 🔗

@@ -1,34 +1,54 @@
 [package]
 name = "agent2"
 version = "0.1.0"
-edition = "2021"
+edition.workspace = true
+publish.workspace = true
 license = "GPL-3.0-or-later"
-publish = false
 
 [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
+portable-pty.workspace = true
 project.workspace = true
 prompt_store.workspace = true
 rust-embed.workspace = true
@@ -37,24 +57,46 @@ 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
+text.workspace = true
 ui.workspace = true
 util.workspace = true
 uuid.workspace = true
 watch.workspace = true
+web_search.workspace = true
+which.workspace = true
 workspace-hack.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"] }
-pretty_assertions.workspace = true
+zlog.workspace = true

crates/agent2/src/agent.rs 🔗

@@ -1,25 +1,34 @@
-use crate::{templates::Templates, AgentResponseEvent, Thread};
-use crate::{FindPathTool, ReadFileTool, ThinkingTool, ToolCallAuthorization};
-use acp_thread::ModelSelector;
+use crate::{
+    ContextServerRegistry, Thread, ThreadEvent, ThreadsDatabase, ToolCallAuthorization,
+    UserMessageContent, templates::Templates,
+};
+use crate::{HistoryStore, TokenUsageUpdated};
+use acp_thread::{AcpThread, AgentModelSelector};
+use action_log::ActionLog;
 use agent_client_protocol as acp;
-use anyhow::{anyhow, Context as _, Result};
-use futures::{future, StreamExt};
+use agent_settings::AgentSettings;
+use anyhow::{Context as _, Result, anyhow};
+use collections::{HashSet, IndexMap};
+use fs::Fs;
+use futures::channel::mpsc;
+use futures::{StreamExt, future};
 use gpui::{
     App, AppContext, AsyncApp, Context, Entity, SharedString, Subscription, Task, WeakEntity,
 };
-use language_model::{LanguageModel, LanguageModelRegistry};
+use language_model::{LanguageModel, LanguageModelProvider, LanguageModelRegistry};
 use project::{Project, ProjectItem, ProjectPath, Worktree};
 use prompt_store::{
     ProjectContext, PromptId, PromptStore, RulesFileContext, UserRulesContext, WorktreeContext,
 };
-use std::cell::RefCell;
+use settings::update_settings_file;
+use std::any::Any;
 use std::collections::HashMap;
 use std::path::Path;
 use std::rc::Rc;
 use std::sync::Arc;
 use util::ResultExt;
 
-const RULES_FILE_NAMES: [&'static str; 9] = [
+const RULES_FILE_NAMES: [&str; 9] = [
     ".rules",
     ".cursorrules",
     ".windsurfrules",
@@ -41,28 +50,134 @@ struct Session {
     thread: Entity<Thread>,
     /// The ACP thread that handles protocol communication
     acp_thread: WeakEntity<acp_thread::AcpThread>,
-    _subscription: Subscription,
+    pending_save: Task<()>,
+    _subscriptions: Vec<Subscription>,
+}
+
+pub struct LanguageModels {
+    /// Access language model by ID
+    models: HashMap<acp_thread::AgentModelId, 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<()>,
+}
+
+impl LanguageModels {
+    fn new(cx: &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,
+        };
+        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.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.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_thread::AgentModelId,
+    ) -> 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,
+            icon: Some(provider.icon()),
+        }
+    }
+
+    fn model_id(model: &Arc<dyn LanguageModel>) -> acp_thread::AgentModelId {
+        acp_thread::AgentModelId(format!("{}/{}", model.provider_id().0, model.id().0).into())
+    }
 }
 
 pub struct NativeAgent {
     /// Session ID -> Session mapping
     sessions: HashMap<acp::SessionId, Session>,
+    history: Entity<HistoryStore>,
     /// Shared project context for all threads
-    project_context: Rc<RefCell<ProjectContext>>,
+    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::info!("Creating new NativeAgent");
@@ -72,7 +187,13 @@ impl NativeAgent {
             .await;
 
         cx.new(|cx| {
-            let mut subscriptions = vec![cx.subscribe(&project, Self::handle_project_event)];
+            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))
             }
@@ -81,19 +202,79 @@ impl NativeAgent {
                 watch::channel(());
             Self {
                 sessions: HashMap::new(),
-                project_context: Rc::new(RefCell::new(project_context)),
+                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 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(cx)
+        });
+
+        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 acp_thread = cx.new(|_cx| {
+            acp_thread::AcpThread::new(
+                title,
+                connection,
+                project.clone(),
+                action_log.clone(),
+                session_id.clone(),
+            )
+        });
+        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_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<()>,
@@ -105,7 +286,9 @@ impl NativeAgent {
                     Self::build_project_context(&this.project, this.prompt_store.as_ref(), cx)
                 })?
                 .await;
-            this.update(cx, |this, _| this.project_context.replace(project_context))?;
+            this.update(cx, |this, cx| {
+                this.project_context = cx.new(|_| project_context);
+            })?;
         }
 
         Ok(())
@@ -258,6 +441,23 @@ impl NativeAgent {
         })
     }
 
+    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>,
@@ -289,244 +489,202 @@ impl NativeAgent {
     ) {
         self.project_context_needs_refresh.send(()).ok();
     }
-}
-
-/// Wrapper struct that implements the AgentConnection trait
-#[derive(Clone)]
-pub struct NativeAgentConnection(pub Entity<NativeAgent>);
 
-impl ModelSelector for NativeAgentConnection {
-    fn list_models(&self, cx: &mut AsyncApp) -> Task<Result<Vec<Arc<dyn LanguageModel>>>> {
-        log::debug!("NativeAgentConnection::list_models called");
-        cx.spawn(async move |cx| {
-            cx.update(|cx| {
-                let registry = LanguageModelRegistry::read_global(cx);
-                let models = registry.available_models(cx).collect::<Vec<_>>();
-                log::info!("Found {} available models", models.len());
-                if models.is_empty() {
-                    Err(anyhow::anyhow!("No models available"))
-                } else {
-                    Ok(models)
-                }
-            })?
-        })
-    }
+    fn handle_models_updated_event(
+        &mut self,
+        _registry: Entity<LanguageModelRegistry>,
+        _event: &language_model::Event,
+        cx: &mut Context<Self>,
+    ) {
+        self.models.refresh_list(cx);
 
-    fn select_model(
-        &self,
-        session_id: acp::SessionId,
-        model: Arc<dyn LanguageModel>,
-        cx: &mut AsyncApp,
-    ) -> Task<Result<()>> {
-        log::info!(
-            "Setting model for session {}: {:?}",
-            session_id,
-            model.name()
-        );
-        let agent = self.0.clone();
+        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);
 
-        cx.spawn(async move |cx| {
-            agent.update(cx, |agent, cx| {
-                if let Some(session) = agent.sessions.get(&session_id) {
-                    session.thread.update(cx, |thread, _cx| {
-                        thread.selected_model = model;
-                    });
-                    Ok(())
-                } else {
-                    Err(anyhow!("Session not found"))
+        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();
                 }
-            })?
-        })
-    }
-
-    fn selected_model(
-        &self,
-        session_id: &acp::SessionId,
-        cx: &mut AsyncApp,
-    ) -> Task<Result<Arc<dyn LanguageModel>>> {
-        let agent = self.0.clone();
-        let session_id = session_id.clone();
-        cx.spawn(async move |cx| {
-            let thread = agent
-                .read_with(cx, |agent, _| {
-                    agent
-                        .sessions
-                        .get(&session_id)
-                        .map(|session| session.thread.clone())
-                })?
-                .ok_or_else(|| anyhow::anyhow!("Session not found"))?;
-            let selected = thread.read_with(cx, |thread, _| thread.selected_model.clone())?;
-            Ok(selected)
-        })
+                thread.set_summarization_model(summarization_model.clone(), cx);
+            });
+        }
     }
-}
-
-impl acp_thread::AgentConnection for NativeAgentConnection {
-    fn new_thread(
-        self: Rc<Self>,
-        project: Entity<Project>,
-        cwd: &Path,
-        cx: &mut AsyncApp,
-    ) -> Task<Result<Entity<acp_thread::AcpThread>>> {
-        let agent = self.0.clone();
-        log::info!("Creating new thread for project at: {:?}", cwd);
-
-        cx.spawn(async move |cx| {
-            log::debug!("Starting thread creation in async context");
 
-            // Generate session ID
-            let session_id = acp::SessionId(uuid::Uuid::new_v4().to_string().into());
-            log::info!("Created session with ID: {}", session_id);
+    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:?}"))?;
 
-            // Create AcpThread
-            let acp_thread = cx.update(|cx| {
+            let thread = this.update(cx, |this, cx| {
+                let action_log = cx.new(|_cx| ActionLog::new(this.project.clone()));
                 cx.new(|cx| {
-                    acp_thread::AcpThread::new("agent2", self.clone(), project.clone(), session_id.clone(), 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 action_log = cx.update(|cx| acp_thread.read(cx).action_log().clone())?;
-
-            // 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()
-                        .map(|configured| {
-                            log::info!(
-                                "Using configured default model: {:?} from provider: {:?}",
-                                configured.model.name(),
-                                configured.provider.name()
-                            );
-                            configured.model
-                        })
-                        .ok_or_else(|| {
-                            log::warn!("No default model configured in settings");
-                            anyhow!("No default model configured. Please configure a default model in settings.")
-                        })?;
-
-                    let thread = cx.new(|_| {
-                        let mut thread = Thread::new(project.clone(), agent.project_context.clone(), action_log.clone(), agent.templates.clone(), default_model);
-                        thread.add_tool(ThinkingTool);
-                        thread.add_tool(FindPathTool::new(project.clone()));
-                        thread.add_tool(ReadFileTool::new(project.clone(), action_log));
-                        thread
-                    });
-
-                    Ok(thread)
-                },
-            )??;
-
-            // Store the session
-            agent.update(cx, |agent, cx| {
-                agent.sessions.insert(
-                    session_id,
-                    Session {
-                        thread,
-                        acp_thread: acp_thread.downgrade(),
-                        _subscription: cx.observe_release(&acp_thread, |this, acp_thread, _cx| {
-                            this.sessions.remove(acp_thread.session_id());
-                        })
-                    },
-                );
-            })?;
-
+            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)
         })
     }
 
-    fn auth_methods(&self) -> &[acp::AuthMethod] {
-        &[] // No auth for in-process
+    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 authenticate(&self, _method: acp::AuthMethodId, _cx: &mut App) -> Task<Result<()>> {
-        Task::ready(Ok(()))
+    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();
+        });
     }
+}
 
-    fn model_selector(&self) -> Option<Rc<dyn ModelSelector>> {
-        Some(Rc::new(self.clone()) as Rc<dyn ModelSelector>)
+/// 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 prompt(
+    fn run_turn(
         &self,
-        params: acp::PromptRequest,
+        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 session_id = params.session_id.clone();
-        let agent = self.0.clone();
-        log::info!("Received prompt request for session: {}", session_id);
-        log::debug!("Prompt blocks count: {}", params.prompt.len());
+        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);
 
-        cx.spawn(async move |cx| {
-            // Get session
-            let (thread, acp_thread) = agent
-                .update(cx, |agent, _| {
-                    agent
-                        .sessions
-                        .get_mut(&session_id)
-                        .map(|s| (s.thread.clone(), s.acp_thread.clone()))
-                })?
-                .ok_or_else(|| {
-                    log::error!("Session not found: {}", session_id);
-                    anyhow::anyhow!("Session not found")
-                })?;
-            log::debug!("Found session for: {}", session_id);
-
-            // Convert prompt to message
-            let message = convert_prompt_to_message(params.prompt);
-            log::info!("Converted prompt to message: {} chars", message.len());
-            log::debug!("Message content: {}", message);
-
-            // Get model using the ModelSelector capability (always available for agent2)
-            // Get the selected model from the thread directly
-            let model = thread.read_with(cx, |thread, _| thread.selected_model.clone())?;
-
-            // Send to thread
-            log::info!("Sending message to thread with model: {:?}", model.name());
-            let mut response_stream =
-                thread.update(cx, |thread, cx| thread.send(model, message, cx))?;
+        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) = response_stream.next().await {
+            while let Some(result) = events.next().await {
                 match result {
                     Ok(event) => {
                         log::trace!("Received completion event: {:?}", event);
 
                         match event {
-                            AgentResponseEvent::Text(text) => {
+                            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.handle_session_update(
-                                        acp::SessionUpdate::AgentMessageChunk {
-                                            content: acp::ContentBlock::Text(acp::TextContent {
-                                                text,
-                                                annotations: None,
-                                            }),
-                                        },
+                                    thread.push_assistant_content_block(
+                                        acp::ContentBlock::Text(acp::TextContent {
+                                            text,
+                                            annotations: None,
+                                        }),
+                                        false,
                                         cx,
                                     )
-                                })??;
+                                })?;
                             }
-                            AgentResponseEvent::Thinking(text) => {
+                            ThreadEvent::AgentThinking(text) => {
                                 acp_thread.update(cx, |thread, cx| {
-                                    thread.handle_session_update(
-                                        acp::SessionUpdate::AgentThoughtChunk {
-                                            content: acp::ContentBlock::Text(acp::TextContent {
-                                                text,
-                                                annotations: None,
-                                            }),
-                                        },
+                                    thread.push_assistant_content_block(
+                                        acp::ContentBlock::Text(acp::TextContent {
+                                            text,
+                                            annotations: None,
+                                        }),
+                                        true,
                                         cx,
                                     )
-                                })??;
+                                })?;
                             }
-                            AgentResponseEvent::ToolCallAuthorization(ToolCallAuthorization {
+                            ThreadEvent::ToolCallAuthorization(ToolCallAuthorization {
                                 tool_call,
                                 options,
                                 response,
@@ -535,10 +693,11 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
                                     thread.request_tool_call_authorization(tool_call, options, cx)
                                 })?;
                                 cx.background_spawn(async move {
-                                    if let Some(option) = recv
-                                        .await
-                                        .context("authorization sender was dropped")
-                                        .log_err()
+                                    if let Some(recv) = recv.log_err()
+                                        && let Some(option) = recv
+                                            .await
+                                            .context("authorization sender was dropped")
+                                            .log_err()
                                     {
                                         response
                                             .send(option)
@@ -548,23 +707,26 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
                                 })
                                 .detach();
                             }
-                            AgentResponseEvent::ToolCall(tool_call) => {
+                            ThreadEvent::ToolCall(tool_call) => {
                                 acp_thread.update(cx, |thread, cx| {
-                                    thread.handle_session_update(
-                                        acp::SessionUpdate::ToolCall(tool_call),
-                                        cx,
-                                    )
+                                    thread.upsert_tool_call(tool_call, cx)
                                 })??;
                             }
-                            AgentResponseEvent::ToolCallUpdate(tool_call_update) => {
+                            ThreadEvent::ToolCallUpdate(update) => {
                                 acp_thread.update(cx, |thread, cx| {
-                                    thread.handle_session_update(
-                                        acp::SessionUpdate::ToolCallUpdate(tool_call_update),
-                                        cx,
-                                    )
+                                    thread.update_tool_call(update, cx)
                                 })??;
                             }
-                            AgentResponseEvent::Stop(stop_reason) => {
+                            ThreadEvent::TitleUpdate(title) => {
+                                acp_thread
+                                    .update(cx, |thread, cx| thread.update_title(title, cx))??;
+                            }
+                            ThreadEvent::Retry(status) => {
+                                acp_thread.update(cx, |thread, cx| {
+                                    thread.update_retry_status(status, cx)
+                                })?;
+                            }
+                            ThreadEvent::Stop(stop_reason) => {
                                 log::debug!("Assistant message complete: {:?}", stop_reason);
                                 return Ok(acp::PromptResponse { stop_reason });
                             }
@@ -572,8 +734,7 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
                     }
                     Err(e) => {
                         log::error!("Error in model response stream: {:?}", e);
-                        // TODO: Consider sending an error message to the UI
-                        break;
+                        return Err(e);
                     }
                 }
             }
@@ -584,57 +745,300 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
             })
         })
     }
+}
+
+impl AgentModelSelector for NativeAgentConnection {
+    fn list_models(&self, cx: &mut App) -> Task<Result<acp_thread::AgentModelList>> {
+        log::debug!("NativeAgentConnection::list_models called");
+        let list = self.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,
+        session_id: acp::SessionId,
+        model_id: acp_thread::AgentModelId,
+        cx: &mut App,
+    ) -> Task<Result<()>> {
+        log::info!("Setting model for session {}: {}", session_id, model_id);
+        let Some(thread) = self
+            .0
+            .read(cx)
+            .sessions
+            .get(&session_id)
+            .map(|session| session.thread.clone())
+        else {
+            return Task::ready(Err(anyhow!("Session not found")));
+        };
+
+        let Some(model) = self.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::<AgentSettings>(
+            self.0.read(cx).fs.clone(),
+            cx,
+            move |settings, _cx| {
+                settings.set_model(model);
+            },
+        );
+
+        Task::ready(Ok(()))
+    }
+
+    fn selected_model(
+        &self,
+        session_id: &acp::SessionId,
+        cx: &mut App,
+    ) -> Task<Result<acp_thread::AgentModelInfo>> {
+        let session_id = session_id.clone();
+
+        let Some(thread) = self
+            .0
+            .read(cx)
+            .sessions
+            .get(&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) -> watch::Receiver<()> {
+        self.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::info!("Creating new thread for project at: {:?}", cwd);
+
+        cx.spawn(async move |cx| {
+            log::debug!("Starting thread creation in async context");
+
+            let action_log = cx.new(|_cx| ActionLog::new(project.clone()))?;
+            // Create Thread
+            let thread = agent.update(
+                cx,
+                |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))
+                    });
+
+                    let thread = cx.new(|cx| {
+                        Thread::new(
+                            project.clone(),
+                            agent.project_context.clone(),
+                            agent.context_server_registry.clone(),
+                            action_log.clone(),
+                            agent.templates.clone(),
+                            default_model,
+                            cx,
+                        )
+                    });
+
+                    Ok(thread)
+                },
+            )??;
+            agent.update(cx, |agent, cx| agent.register_session(thread, cx))
+        })
+    }
+
+    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) -> Option<Rc<dyn AgentModelSelector>> {
+        Some(Rc::new(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::info!("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 prompt_capabilities(&self) -> acp::PromptCapabilities {
+        acp::PromptCapabilities {
+            image: true,
+            audio: false,
+            embedded_context: true,
+        }
+    }
+
+    fn resume(
+        &self,
+        session_id: &acp::SessionId,
+        _cx: &mut 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());
+                agent.thread.update(cx, |thread, cx| thread.cancel(cx));
             }
         });
     }
+
+    fn session_editor(
+        &self,
+        session_id: &agent_client_protocol::SessionId,
+        cx: &mut App,
+    ) -> Option<Rc<dyn acp_thread::AgentSessionEditor>> {
+        self.0.update(cx, |agent, _cx| {
+            agent.sessions.get(session_id).map(|session| {
+                Rc::new(NativeAgentSessionEditor {
+                    thread: session.thread.clone(),
+                    acp_thread: session.acp_thread.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
+    }
 }
 
-/// Convert ACP content blocks to a message string
-fn convert_prompt_to_message(blocks: Vec<acp::ContentBlock>) -> String {
-    log::debug!("Converting {} content blocks to message", blocks.len());
-    let mut message = String::new();
+impl acp_thread::AgentTelemetry for NativeAgentConnection {
+    fn agent_name(&self) -> String {
+        "Zed".into()
+    }
 
-    for block in blocks {
-        match block {
-            acp::ContentBlock::Text(text) => {
-                log::trace!("Processing text block: {} chars", text.text.len());
-                message.push_str(&text.text);
-            }
-            acp::ContentBlock::ResourceLink(link) => {
-                log::trace!("Processing resource link: {}", link.uri);
-                message.push_str(&format!(" @{} ", link.uri));
-            }
-            acp::ContentBlock::Image(_) => {
-                log::trace!("Processing image block");
-                message.push_str(" [image] ");
-            }
-            acp::ContentBlock::Audio(_) => {
-                log::trace!("Processing audio block");
-                message.push_str(" [audio] ");
-            }
-            acp::ContentBlock::Resource(resource) => {
-                log::trace!("Processing resource block: {:?}", resource.resource);
-                message.push_str(&format!(" [resource: {:?}] ", resource.resource));
+    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 NativeAgentSessionEditor {
+    thread: Entity<Thread>,
+    acp_thread: WeakEntity<AcpThread>,
+}
+
+impl acp_thread::AgentSessionEditor for NativeAgentSessionEditor {
+    fn truncate(&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)),
         }
     }
+}
 
-    message
+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))
+            })
+    }
 }
 
 #[cfg(test)]
 mod tests {
+    use crate::HistoryEntryId;
+
     use super::*;
+    use acp_thread::{
+        AgentConnection, AgentModelGroupName, AgentModelId, AgentModelInfo, MentionUri,
+    };
     use fs::FakeFs;
     use gpui::TestAppContext;
+    use indoc::indoc;
+    use language_model::fake_provider::FakeLanguageModel;
     use serde_json::json;
     use settings::SettingsStore;
+    use util::path;
 
     #[gpui::test]
     async fn test_maintaining_project_context(cx: &mut TestAppContext) {

crates/agent2/src/agent2.rs 🔗

@@ -1,13 +1,19 @@
 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/db.rs 🔗

@@ -0,0 +1,488 @@
+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};
+use chrono::{DateTime, Utc};
+use collections::{HashMap, IndexMap};
+use futures::{FutureExt, future::Shared};
+use gpui::{BackgroundExecutor, Global, Task};
+use indoc::indoc;
+use parking_lot::Mutex;
+use serde::{Deserialize, Serialize};
+use sqlez::{
+    bindable::{Bind, Column},
+    connection::Connection,
+    statement::Statement,
+};
+use std::sync::Arc;
+use ui::{App, SharedString};
+
+pub type DbMessage = crate::Message;
+pub type DbSummary = DetailedSummaryState;
+pub type DbLanguageModel = thread_store::SerializedLanguageModel;
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct DbThreadMetadata {
+    pub id: acp::SessionId,
+    #[serde(alias = "summary")]
+    pub title: SharedString,
+    pub updated_at: DateTime<Utc>,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct DbThread {
+    pub title: SharedString,
+    pub messages: Vec<DbMessage>,
+    pub updated_at: DateTime<Utc>,
+    #[serde(default)]
+    pub detailed_summary: Option<SharedString>,
+    #[serde(default)]
+    pub initial_project_snapshot: Option<Arc<agent::thread::ProjectSnapshot>>,
+    #[serde(default)]
+    pub cumulative_token_usage: language_model::TokenUsage,
+    #[serde(default)]
+    pub request_token_usage: HashMap<acp_thread::UserMessageId, language_model::TokenUsage>,
+    #[serde(default)]
+    pub model: Option<DbLanguageModel>,
+    #[serde(default)]
+    pub completion_mode: Option<CompletionMode>,
+    #[serde(default)]
+    pub profile: Option<AgentProfileId>,
+}
+
+impl DbThread {
+    pub const VERSION: &'static str = "0.3.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() {
+                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(agent::SerializedThread::from_json(json)?),
+        }
+    }
+
+    fn upgrade_from_agent_1(thread: agent::SerializedThread) -> Result<Self> {
+        let mut messages = Vec::new();
+        let mut request_token_usage = HashMap::default();
+
+        let mut last_user_message_id = None;
+        for (ix, msg) in thread.messages.into_iter().enumerate() {
+            let message = match msg.role {
+                language_model::Role::User => {
+                    let mut content = Vec::new();
+
+                    // Convert segments to content
+                    for segment in msg.segments {
+                        match segment {
+                            thread_store::SerializedMessageSegment::Text { text } => {
+                                content.push(UserMessageContent::Text(text));
+                            }
+                            thread_store::SerializedMessageSegment::Thinking { text, .. } => {
+                                // User messages don't have thinking segments, but handle gracefully
+                                content.push(UserMessageContent::Text(text));
+                            }
+                            thread_store::SerializedMessageSegment::RedactedThinking { .. } => {
+                                // User messages don't have redacted thinking, skip.
+                            }
+                        }
+                    }
+
+                    // If no content was added, add context as text if available
+                    if content.is_empty() && !msg.context.is_empty() {
+                        content.push(UserMessageContent::Text(msg.context));
+                    }
+
+                    let id = UserMessageId::new();
+                    last_user_message_id = Some(id.clone());
+
+                    crate::Message::User(UserMessage {
+                        // MessageId from old format can't be meaningfully converted, so generate a new one
+                        id,
+                        content,
+                    })
+                }
+                language_model::Role::Assistant => {
+                    let mut content = Vec::new();
+
+                    // Convert segments to content
+                    for segment in msg.segments {
+                        match segment {
+                            thread_store::SerializedMessageSegment::Text { text } => {
+                                content.push(AgentMessageContent::Text(text));
+                            }
+                            thread_store::SerializedMessageSegment::Thinking {
+                                text,
+                                signature,
+                            } => {
+                                content.push(AgentMessageContent::Thinking { text, signature });
+                            }
+                            thread_store::SerializedMessageSegment::RedactedThinking { data } => {
+                                content.push(AgentMessageContent::RedactedThinking(data));
+                            }
+                        }
+                    }
+
+                    // Convert tool uses
+                    let mut tool_names_by_id = HashMap::default();
+                    for tool_use in msg.tool_uses {
+                        tool_names_by_id.insert(tool_use.id.clone(), tool_use.name.clone());
+                        content.push(AgentMessageContent::ToolUse(
+                            language_model::LanguageModelToolUse {
+                                id: tool_use.id,
+                                name: tool_use.name.into(),
+                                raw_input: serde_json::to_string(&tool_use.input)
+                                    .unwrap_or_default(),
+                                input: tool_use.input,
+                                is_input_complete: true,
+                            },
+                        ));
+                    }
+
+                    // Convert tool results
+                    let mut tool_results = IndexMap::default();
+                    for tool_result in msg.tool_results {
+                        let name = tool_names_by_id
+                            .remove(&tool_result.tool_use_id)
+                            .unwrap_or_else(|| SharedString::from("unknown"));
+                        tool_results.insert(
+                            tool_result.tool_use_id.clone(),
+                            language_model::LanguageModelToolResult {
+                                tool_use_id: tool_result.tool_use_id,
+                                tool_name: name.into(),
+                                is_error: tool_result.is_error,
+                                content: tool_result.content,
+                                output: tool_result.output,
+                            },
+                        );
+                    }
+
+                    if let Some(last_user_message_id) = &last_user_message_id
+                        && let Some(token_usage) = thread.request_token_usage.get(ix).copied()
+                    {
+                        request_token_usage.insert(last_user_message_id.clone(), token_usage);
+                    }
+
+                    crate::Message::Agent(AgentMessage {
+                        content,
+                        tool_results,
+                    })
+                }
+                language_model::Role::System => {
+                    // Skip system messages as they're not supported in the new format
+                    continue;
+                }
+            };
+
+            messages.push(message);
+        }
+
+        Ok(Self {
+            title: thread.summary,
+            messages,
+            updated_at: thread.updated_at,
+            detailed_summary: match thread.detailed_summary_state {
+                DetailedSummaryState::NotGenerated | DetailedSummaryState::Generating { .. } => {
+                    None
+                }
+                DetailedSummaryState::Generated { text, .. } => Some(text),
+            },
+            initial_project_snapshot: thread.initial_project_snapshot,
+            cumulative_token_usage: thread.cumulative_token_usage,
+            request_token_usage,
+            model: thread.model,
+            completion_mode: thread.completion_mode,
+            profile: thread.profile,
+        })
+    }
+}
+
+pub static ZED_STATELESS: std::sync::LazyLock<bool> =
+    std::sync::LazyLock::new(|| std::env::var("ZED_STATELESS").is_ok_and(|v| !v.is_empty()));
+
+#[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))
+    }
+}
+
+pub(crate) struct ThreadsDatabase {
+    executor: BackgroundExecutor,
+    connection: Arc<Mutex<Connection>>,
+}
+
+struct GlobalThreadsDatabase(Shared<Task<Result<Arc<ThreadsDatabase>, Arc<anyhow::Error>>>>);
+
+impl Global for GlobalThreadsDatabase {}
+
+impl ThreadsDatabase {
+    pub fn connect(cx: &mut App) -> Shared<Task<Result<Arc<ThreadsDatabase>, Arc<anyhow::Error>>>> {
+        if cx.has_global::<GlobalThreadsDatabase>() {
+            return cx.global::<GlobalThreadsDatabase>().0.clone();
+        }
+        let executor = cx.background_executor().clone();
+        let task = executor
+            .spawn({
+                let executor = executor.clone();
+                async move {
+                    match ThreadsDatabase::new(executor) {
+                        Ok(db) => Ok(Arc::new(db)),
+                        Err(err) => Err(Arc::new(err)),
+                    }
+                }
+            })
+            .shared();
+
+        cx.set_global(GlobalThreadsDatabase(task.clone()));
+        task
+    }
+
+    pub fn new(executor: BackgroundExecutor) -> Result<Self> {
+        let connection = if *ZED_STATELESS || cfg!(any(feature = "test-support", test)) {
+            Connection::open_memory(Some("THREAD_FALLBACK_DB"))
+        } else {
+            let threads_dir = paths::data_dir().join("threads");
+            std::fs::create_dir_all(&threads_dir)?;
+            let sqlite_path = threads_dir.join("threads.db");
+            Connection::open_file(&sqlite_path.to_string_lossy())
+        };
+
+        connection.exec(indoc! {"
+            CREATE TABLE IF NOT EXISTS threads (
+                id TEXT PRIMARY KEY,
+                summary TEXT NOT NULL,
+                updated_at TEXT NOT NULL,
+                data_type TEXT NOT NULL,
+                data BLOB NOT NULL
+            )
+        "})?()
+        .map_err(|e| anyhow!("Failed to create threads table: {}", e))?;
+
+        let db = Self {
+            executor,
+            connection: Arc::new(Mutex::new(connection)),
+        };
+
+        Ok(db)
+    }
+
+    fn save_thread_sync(
+        connection: &Arc<Mutex<Connection>>,
+        id: acp::SessionId,
+        thread: DbThread,
+    ) -> Result<()> {
+        const COMPRESSION_LEVEL: i32 = 3;
+
+        #[derive(Serialize)]
+        struct SerializedThread {
+            #[serde(flatten)]
+            thread: DbThread,
+            version: &'static str,
+        }
+
+        let title = thread.title.to_string();
+        let updated_at = thread.updated_at.to_rfc3339();
+        let json_data = serde_json::to_string(&SerializedThread {
+            thread,
+            version: DbThread::VERSION,
+        })?;
+
+        let connection = connection.lock();
+
+        let compressed = zstd::encode_all(json_data.as_bytes(), COMPRESSION_LEVEL)?;
+        let data_type = DataType::Zstd;
+        let data = compressed;
+
+        let mut insert = connection.exec_bound::<(Arc<str>, String, String, DataType, Vec<u8>)>(indoc! {"
+            INSERT OR REPLACE INTO threads (id, summary, updated_at, data_type, data) VALUES (?, ?, ?, ?, ?)
+        "})?;
+
+        insert((id.0, title, updated_at, data_type, data))?;
+
+        Ok(())
+    }
+
+    pub fn list_threads(&self) -> Task<Result<Vec<DbThreadMetadata>>> {
+        let connection = self.connection.clone();
+
+        self.executor.spawn(async move {
+            let connection = connection.lock();
+
+            let mut select =
+                connection.select_bound::<(), (Arc<str>, String, String)>(indoc! {"
+                SELECT id, summary, updated_at FROM threads ORDER BY updated_at DESC
+            "})?;
+
+            let rows = select(())?;
+            let mut threads = Vec::new();
+
+            for (id, summary, updated_at) in rows {
+                threads.push(DbThreadMetadata {
+                    id: acp::SessionId(id),
+                    title: summary.into(),
+                    updated_at: DateTime::parse_from_rfc3339(&updated_at)?.with_timezone(&Utc),
+                });
+            }
+
+            Ok(threads)
+        })
+    }
+
+    pub fn load_thread(&self, id: acp::SessionId) -> Task<Result<Option<DbThread>>> {
+        let connection = self.connection.clone();
+
+        self.executor.spawn(async move {
+            let connection = connection.lock();
+            let mut select = connection.select_bound::<Arc<str>, (DataType, Vec<u8>)>(indoc! {"
+                SELECT data_type, data FROM threads WHERE id = ? LIMIT 1
+            "})?;
+
+            let rows = select(id.0)?;
+            if let Some((data_type, data)) = rows.into_iter().next() {
+                let json_data = match data_type {
+                    DataType::Zstd => {
+                        let decompressed = zstd::decode_all(&data[..])?;
+                        String::from_utf8(decompressed)?
+                    }
+                    DataType::Json => String::from_utf8(data)?,
+                };
+                let thread = DbThread::from_json(json_data.as_bytes())?;
+                Ok(Some(thread))
+            } else {
+                Ok(None)
+            }
+        })
+    }
+
+    pub fn save_thread(&self, id: acp::SessionId, thread: DbThread) -> Task<Result<()>> {
+        let connection = self.connection.clone();
+
+        self.executor
+            .spawn(async move { Self::save_thread_sync(&connection, id, thread) })
+    }
+
+    pub fn delete_thread(&self, id: acp::SessionId) -> Task<Result<()>> {
+        let connection = self.connection.clone();
+
+        self.executor.spawn(async move {
+            let connection = connection.lock();
+
+            let mut delete = connection.exec_bound::<Arc<str>>(indoc! {"
+                DELETE FROM threads WHERE id = ?
+            "})?;
+
+            delete(id.0)?;
+
+            Ok(())
+        })
+    }
+}
+
+#[cfg(test)]
+mod tests {
+
+    use super::*;
+    use agent::MessageSegment;
+    use agent::context::LoadedContext;
+    use client::Client;
+    use fs::FakeFs;
+    use gpui::AppContext;
+    use gpui::TestAppContext;
+    use http_client::FakeHttpClient;
+    use language_model::Role;
+    use project::Project;
+    use settings::SettingsStore;
+
+    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);
+            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(cx);
+            agent_settings::init(cx);
+            language_model::init(client, cx);
+        });
+    }
+
+    #[gpui::test]
+    async fn test_retrieving_old_thread(cx: &mut TestAppContext) {
+        init_test(cx);
+        let fs = FakeFs::new(cx.executor());
+        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/agent2/src/history_store.rs 🔗

@@ -0,0 +1,362 @@
+use crate::{DbThreadMetadata, ThreadsDatabase};
+use acp_thread::MentionUri;
+use agent_client_protocol as acp;
+use anyhow::{Context as _, Result, anyhow};
+use assistant_context::{AssistantContext, SavedContextMetadata};
+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 serde::{Deserialize, Serialize};
+use std::{collections::VecDeque, path::Path, sync::Arc, time::Duration};
+use ui::ElementId;
+use util::ResultExt as _;
+
+const MAX_RECENTLY_OPENED_ENTRIES: usize = 6;
+const RECENTLY_OPENED_THREADS_KEY: &str = "recent-agent-threads";
+const SAVE_RECENTLY_OPENED_ENTRIES_DEBOUNCE: Duration = Duration::from_millis(50);
+
+const DEFAULT_TITLE: &SharedString = &SharedString::new_static("New Thread");
+
+#[derive(Clone, Debug)]
+pub enum HistoryEntry {
+    AcpThread(DbThreadMetadata),
+    TextThread(SavedContextMetadata),
+}
+
+impl HistoryEntry {
+    pub fn updated_at(&self) -> DateTime<Utc> {
+        match self {
+            HistoryEntry::AcpThread(thread) => thread.updated_at,
+            HistoryEntry::TextThread(context) => context.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()),
+        }
+    }
+
+    pub fn mention_uri(&self) -> MentionUri {
+        match self {
+            HistoryEntry::AcpThread(thread) => MentionUri::Thread {
+                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(),
+            },
+        }
+    }
+
+    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,
+        }
+    }
+}
+
+/// Generic identifier for a history entry.
+#[derive(Clone, PartialEq, Eq, Debug, Hash)]
+pub enum HistoryEntryId {
+    AcpThread(acp::SessionId),
+    TextThread(Arc<Path>),
+}
+
+impl Into<ElementId> for HistoryEntryId {
+    fn into(self) -> ElementId {
+        match self {
+            HistoryEntryId::AcpThread(session_id) => ElementId::Name(session_id.0.into()),
+            HistoryEntryId::TextThread(path) => ElementId::Path(path),
+        }
+    }
+}
+
+#[derive(Serialize, Deserialize, Debug)]
+enum SerializedRecentOpen {
+    AcpThread(String),
+    TextThread(String),
+}
+
+pub struct HistoryStore {
+    threads: Vec<DbThreadMetadata>,
+    context_store: Entity<assistant_context::ContextStore>,
+    recently_opened_entries: VecDeque<HistoryEntryId>,
+    _subscriptions: Vec<gpui::Subscription>,
+    _save_recently_opened_entries_task: Task<()>,
+}
+
+impl HistoryStore {
+    pub fn new(
+        context_store: Entity<assistant_context::ContextStore>,
+        cx: &mut Context<Self>,
+    ) -> Self {
+        let subscriptions = vec![cx.observe(&context_store, |_, _, cx| cx.notify())];
+
+        cx.spawn(async move |this, cx| {
+            let entries = Self::load_recently_opened_entries(cx).await;
+            this.update(cx, |this, cx| {
+                if let Some(entries) = entries.log_err() {
+                    this.recently_opened_entries = entries;
+                }
+
+                this.reload(cx);
+            })
+            .ok();
+        })
+        .detach();
+
+        Self {
+            context_store,
+            recently_opened_entries: VecDeque::default(),
+            threads: Vec::default(),
+            _subscriptions: subscriptions,
+            _save_recently_opened_entries_task: Task::ready(()),
+        }
+    }
+
+    pub fn thread_from_session_id(&self, session_id: &acp::SessionId) -> Option<&DbThreadMetadata> {
+        self.threads.iter().find(|thread| &thread.id == session_id)
+    }
+
+    pub fn delete_thread(
+        &mut self,
+        id: acp::SessionId,
+        cx: &mut Context<Self>,
+    ) -> Task<Result<()>> {
+        let database_future = ThreadsDatabase::connect(cx);
+        cx.spawn(async move |this, cx| {
+            let database = database_future.await.map_err(|err| anyhow!(err))?;
+            database.delete_thread(id.clone()).await?;
+            this.update(cx, |this, cx| this.reload(cx))
+        })
+    }
+
+    pub fn delete_text_thread(
+        &mut self,
+        path: Arc<Path>,
+        cx: &mut Context<Self>,
+    ) -> Task<Result<()>> {
+        self.context_store.update(cx, |context_store, cx| {
+            context_store.delete_local_context(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)
+        })
+    }
+
+    pub fn reload(&self, cx: &mut Context<Self>) {
+        let database_future = ThreadsDatabase::connect(cx);
+        cx.spawn(async move |this, cx| {
+            let threads = database_future
+                .await
+                .map_err(|err| anyhow!(err))?
+                .list_threads()
+                .await?;
+
+            this.update(cx, |this, cx| {
+                if this.recently_opened_entries.len() < MAX_RECENTLY_OPENED_ENTRIES {
+                    for thread in threads
+                        .iter()
+                        .take(MAX_RECENTLY_OPENED_ENTRIES - this.recently_opened_entries.len())
+                        .rev()
+                    {
+                        this.push_recently_opened_entry(
+                            HistoryEntryId::AcpThread(thread.id.clone()),
+                            cx,
+                        )
+                    }
+                }
+                this.threads = threads;
+                cx.notify();
+            })
+        })
+        .detach_and_log_err(cx);
+    }
+
+    pub fn entries(&self, cx: &App) -> Vec<HistoryEntry> {
+        let mut history_entries = Vec::new();
+
+        #[cfg(debug_assertions)]
+        if std::env::var("ZED_SIMULATE_NO_THREAD_HISTORY").is_ok() {
+            return history_entries;
+        }
+
+        history_entries.extend(self.threads.iter().cloned().map(HistoryEntry::AcpThread));
+        history_entries.extend(
+            self.context_store
+                .read(cx)
+                .unordered_contexts()
+                .cloned()
+                .map(HistoryEntry::TextThread),
+        );
+
+        history_entries.sort_unstable_by_key(|entry| std::cmp::Reverse(entry.updated_at()));
+        history_entries
+    }
+
+    pub fn is_empty(&self, cx: &App) -> bool {
+        self.threads.is_empty()
+            && self
+                .context_store
+                .read(cx)
+                .unordered_contexts()
+                .next()
+                .is_none()
+    }
+
+    pub fn recently_opened_entries(&self, cx: &App) -> Vec<HistoryEntry> {
+        #[cfg(debug_assertions)]
+        if std::env::var("ZED_SIMULATE_NO_THREAD_HISTORY").is_ok() {
+            return Vec::new();
+        }
+
+        let thread_entries = self.threads.iter().flat_map(|thread| {
+            self.recently_opened_entries
+                .iter()
+                .enumerate()
+                .flat_map(|(index, entry)| match entry {
+                    HistoryEntryId::AcpThread(id) if &thread.id == id => {
+                        Some((index, HistoryEntry::AcpThread(thread.clone())))
+                    }
+                    _ => None,
+                })
+        });
+
+        let context_entries =
+            self.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,
+                        })
+                });
+
+        thread_entries
+            .chain(context_entries)
+            // optimization to halt iteration early
+            .take(self.recently_opened_entries.len())
+            .sorted_unstable_by_key(|(index, _)| *index)
+            .map(|(_, entry)| entry)
+            .collect()
+    }
+
+    fn save_recently_opened_entries(&mut self, cx: &mut Context<Self>) {
+        let serialized_entries = self
+            .recently_opened_entries
+            .iter()
+            .filter_map(|entry| match entry {
+                HistoryEntryId::TextThread(path) => path.file_name().map(|file| {
+                    SerializedRecentOpen::TextThread(file.to_string_lossy().to_string())
+                }),
+                HistoryEntryId::AcpThread(id) => {
+                    Some(SerializedRecentOpen::AcpThread(id.to_string()))
+                }
+            })
+            .collect::<Vec<_>>();
+
+        self._save_recently_opened_entries_task = cx.spawn(async move |_, cx| {
+            let content = serde_json::to_string(&serialized_entries).unwrap();
+            cx.background_executor()
+                .timer(SAVE_RECENTLY_OPENED_ENTRIES_DEBOUNCE)
+                .await;
+
+            if cfg!(any(feature = "test-support", test)) {
+                return;
+            }
+            KEY_VALUE_STORE
+                .write_kvp(RECENTLY_OPENED_THREADS_KEY.to_owned(), content)
+                .await
+                .log_err();
+        });
+    }
+
+    fn load_recently_opened_entries(cx: &AsyncApp) -> Task<Result<VecDeque<HistoryEntryId>>> {
+        cx.background_spawn(async move {
+            if cfg!(any(feature = "test-support", test)) {
+                anyhow::bail!("history store does not persist in tests");
+            }
+            let json = KEY_VALUE_STORE
+                .read_kvp(RECENTLY_OPENED_THREADS_KEY)?
+                .unwrap_or("[]".to_string());
+            let entries = serde_json::from_str::<Vec<SerializedRecentOpen>>(&json)
+                .context("deserializing persisted agent panel navigation history")?
+                .into_iter()
+                .take(MAX_RECENTLY_OPENED_ENTRIES)
+                .flat_map(|entry| match entry {
+                    SerializedRecentOpen::AcpThread(id) => Some(HistoryEntryId::AcpThread(
+                        acp::SessionId(id.as_str().into()),
+                    )),
+                    SerializedRecentOpen::TextThread(file_name) => Some(
+                        HistoryEntryId::TextThread(contexts_dir().join(file_name).into()),
+                    ),
+                })
+                .collect();
+            Ok(entries)
+        })
+    }
+
+    pub fn push_recently_opened_entry(&mut self, entry: HistoryEntryId, cx: &mut Context<Self>) {
+        self.recently_opened_entries
+            .retain(|old_entry| old_entry != &entry);
+        self.recently_opened_entries.push_front(entry);
+        self.recently_opened_entries
+            .truncate(MAX_RECENTLY_OPENED_ENTRIES);
+        self.save_recently_opened_entries(cx);
+    }
+
+    pub fn remove_recently_opened_thread(&mut self, id: acp::SessionId, cx: &mut Context<Self>) {
+        self.recently_opened_entries.retain(
+            |entry| !matches!(entry, HistoryEntryId::AcpThread(thread_id) if thread_id == &id),
+        );
+        self.save_recently_opened_entries(cx);
+    }
+
+    pub fn replace_recently_opened_text_thread(
+        &mut self,
+        old_path: &Path,
+        new_path: &Arc<Path>,
+        cx: &mut Context<Self>,
+    ) {
+        for entry in &mut self.recently_opened_entries {
+            match entry {
+                HistoryEntryId::TextThread(path) if path.as_ref() == old_path => {
+                    *entry = HistoryEntryId::TextThread(new_path.clone());
+                    break;
+                }
+                _ => {}
+            }
+        }
+        self.save_recently_opened_entries(cx);
+    }
+
+    pub fn remove_recently_opened_entry(&mut self, entry: &HistoryEntryId, cx: &mut Context<Self>) {
+        self.recently_opened_entries
+            .retain(|old_entry| old_entry != entry);
+        self.save_recently_opened_entries(cx);
+    }
+
+    pub fn recent_entries(&self, limit: usize, cx: &mut Context<Self>) -> Vec<HistoryEntry> {
+        self.entries(cx).into_iter().take(limit).collect()
+    }
+}

crates/agent2/src/native_agent_server.rs 🔗

@@ -1,16 +1,25 @@
-use std::path::Path;
-use std::rc::Rc;
+use std::{any::Any, path::Path, rc::Rc, sync::Arc};
 
 use agent_servers::AgentServer;
 use anyhow::Result;
+use fs::Fs;
 use gpui::{App, Entity, Task};
 use project::Project;
 use prompt_store::PromptStore;
 
-use crate::{templates::Templates, NativeAgent, NativeAgentConnection};
+use crate::{HistoryStore, NativeAgent, NativeAgentConnection, templates::Templates};
 
 #[derive(Clone)]
-pub struct NativeAgentServer;
+pub struct NativeAgentServer {
+    fs: Arc<dyn Fs>,
+    history: Entity<HistoryStore>,
+}
+
+impl NativeAgentServer {
+    pub fn new(fs: Arc<dyn Fs>, history: Entity<HistoryStore>) -> Self {
+        Self { fs, history }
+    }
+}
 
 impl AgentServer for NativeAgentServer {
     fn name(&self) -> &'static str {
@@ -18,16 +27,15 @@ impl AgentServer for NativeAgentServer {
     }
 
     fn empty_state_headline(&self) -> &'static str {
-        "Native Agent"
+        "Welcome to the Agent Panel"
     }
 
     fn empty_state_message(&self) -> &'static str {
-        "How can I help you today?"
+        ""
     }
 
     fn logo(&self) -> ui::IconName {
-        // Using the ZedAssistant icon as it's the native built-in agent
-        ui::IconName::ZedAssistant
+        ui::IconName::ZedAgent
     }
 
     fn connect(
@@ -41,6 +49,8 @@ impl AgentServer for NativeAgentServer {
             _root_dir
         );
         let project = project.clone();
+        let fs = self.fs.clone();
+        let history = self.history.clone();
         let prompt_store = PromptStore::global(cx);
         cx.spawn(async move |cx| {
             log::debug!("Creating templates for native agent");
@@ -48,7 +58,8 @@ impl AgentServer for NativeAgentServer {
             let prompt_store = prompt_store.await?;
 
             log::debug!("Creating native agent entity");
-            let agent = NativeAgent::new(project, templates, Some(prompt_store), cx).await?;
+            let agent =
+                NativeAgent::new(project, history, templates, Some(prompt_store), fs, cx).await?;
 
             // Create the connection wrapper
             let connection = NativeAgentConnection(agent);
@@ -57,4 +68,57 @@ impl AgentServer for NativeAgentServer {
             Ok(Rc::new(connection) as Rc<dyn acp_thread::AgentConnection>)
         })
     }
+
+    fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
+        self
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    use assistant_context::ContextStore;
+    use gpui::AppContext;
+
+    agent_servers::e2e_tests::common_e2e_tests!(
+        async |fs, project, cx| {
+            let auth = cx.update(|cx| {
+                prompt_store::init(cx);
+                terminal::init(cx);
+
+                let registry = language_model::LanguageModelRegistry::read_global(cx);
+                let auth = registry
+                    .provider(&language_model::ANTHROPIC_PROVIDER_ID)
+                    .unwrap()
+                    .authenticate(cx);
+
+                cx.spawn(async move |_| auth.await)
+            });
+
+            auth.await.unwrap();
+
+            cx.update(|cx| {
+                let registry = language_model::LanguageModelRegistry::global(cx);
+
+                registry.update(cx, |registry, cx| {
+                    registry.select_default_model(
+                        Some(&language_model::SelectedModel {
+                            provider: language_model::ANTHROPIC_PROVIDER_ID,
+                            model: language_model::LanguageModelId("claude-sonnet-4-latest".into()),
+                        }),
+                        cx,
+                    );
+                });
+            });
+
+            let history = cx.update(|cx| {
+                let context_store = cx.new(move |cx| ContextStore::fake(project.clone(), cx));
+                cx.new(move |cx| HistoryStore::new(context_store, cx))
+            });
+
+            NativeAgentServer::new(fs.clone(), history)
+        },
+        allow_option_id = "allow"
+    );
 }

crates/agent2/src/templates.rs 🔗

@@ -62,7 +62,7 @@ fn contains(
         handlebars::RenderError::new("contains: missing or invalid query parameter")
     })?;
 
-    if list.contains(&query) {
+    if list.contains(query) {
         out.write("true")?;
     }
 

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

@@ -1,27 +1,31 @@
 use super::*;
-use crate::templates::Templates;
-use acp_thread::AgentConnection;
+use acp_thread::{AgentConnection, AgentModelGroupName, AgentModelList, UserMessageId};
+use action_log::ActionLog;
 use agent_client_protocol::{self as acp};
+use agent_settings::AgentProfileId;
 use anyhow::Result;
-use assistant_tool::ActionLog;
 use client::{Client, UserStore};
-use fs::FakeFs;
-use futures::channel::mpsc::UnboundedReceiver;
-use gpui::{http_client::FakeHttpClient, AppContext, Entity, Task, TestAppContext};
+use fs::{FakeFs, Fs};
+use futures::{StreamExt, channel::mpsc::UnboundedReceiver};
+use gpui::{
+    App, AppContext, Entity, Task, TestAppContext, UpdateGlobal, http_client::FakeHttpClient,
+};
 use indoc::indoc;
 use language_model::{
-    fake_provider::FakeLanguageModel, LanguageModel, LanguageModelCompletionError,
-    LanguageModelCompletionEvent, LanguageModelId, LanguageModelRegistry, LanguageModelToolResult,
-    LanguageModelToolUse, MessageContent, Role, StopReason,
+    LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelId,
+    LanguageModelProviderName, LanguageModelRegistry, LanguageModelRequestMessage,
+    LanguageModelToolResult, LanguageModelToolUse, MessageContent, Role, StopReason,
+    fake_provider::FakeLanguageModel,
 };
+use pretty_assertions::assert_eq;
 use project::Project;
 use prompt_store::ProjectContext;
 use reqwest_client::ReqwestClient;
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 use serde_json::json;
-use smol::stream::StreamExt;
-use std::{cell::RefCell, path::Path, rc::Rc, sync::Arc, time::Duration};
+use settings::SettingsStore;
+use std::{path::Path, rc::Rc, sync::Arc, time::Duration};
 use util::path;
 
 mod test_tools;
@@ -30,19 +34,24 @@ use test_tools::*;
 #[gpui::test]
 #[ignore = "can't run on CI yet"]
 async fn test_echo(cx: &mut TestAppContext) {
-    let ThreadTest { model, thread, .. } = setup(cx, TestModel::Sonnet4).await;
+    let ThreadTest { thread, .. } = setup(cx, TestModel::Sonnet4).await;
 
     let events = thread
         .update(cx, |thread, cx| {
-            thread.send(model.clone(), "Testing: Reply with 'Hello'", cx)
+            thread.send(UserMessageId::new(), ["Testing: Reply with 'Hello'"], cx)
         })
+        .unwrap()
         .collect()
         .await;
     thread.update(cx, |thread, _cx| {
         assert_eq!(
-            thread.messages().last().unwrap().content,
-            vec![MessageContent::Text("Hello".to_string())]
-        );
+            thread.last_message().unwrap().to_markdown(),
+            indoc! {"
+                ## Assistant
+
+                Hello
+            "}
+        )
     });
     assert_eq!(stop_events(events), vec![acp::StopReason::EndTurn]);
 }
@@ -50,28 +59,30 @@ async fn test_echo(cx: &mut TestAppContext) {
 #[gpui::test]
 #[ignore = "can't run on CI yet"]
 async fn test_thinking(cx: &mut TestAppContext) {
-    let ThreadTest { model, thread, .. } = setup(cx, TestModel::Sonnet4Thinking).await;
+    let ThreadTest { thread, .. } = setup(cx, TestModel::Sonnet4Thinking).await;
 
     let events = thread
         .update(cx, |thread, cx| {
             thread.send(
-                model.clone(),
-                indoc! {"
+                UserMessageId::new(),
+                [indoc! {"
                     Testing:
 
                     Generate a thinking step where you just think the word 'Think',
                     and have your final answer be 'Hello'
-                "},
+                "}],
                 cx,
             )
         })
+        .unwrap()
         .collect()
         .await;
     thread.update(cx, |thread, _cx| {
         assert_eq!(
-            thread.messages().last().unwrap().to_markdown(),
+            thread.last_message().unwrap().to_markdown(),
             indoc! {"
-                ## assistant
+                ## Assistant
+
                 <think>Think</think>
                 Hello
             "}
@@ -90,9 +101,15 @@ async fn test_system_prompt(cx: &mut TestAppContext) {
     } = setup(cx, TestModel::Fake).await;
     let fake_model = model.as_fake();
 
-    project_context.borrow_mut().shell = "test-shell".into();
+    project_context.update(cx, |project_context, _cx| {
+        project_context.shell = "test-shell".into()
+    });
     thread.update(cx, |thread, _| thread.add_tool(EchoTool));
-    thread.update(cx, |thread, cx| thread.send(model.clone(), "abc", cx));
+    thread
+        .update(cx, |thread, cx| {
+            thread.send(UserMessageId::new(), ["abc"], cx)
+        })
+        .unwrap();
     cx.run_until_parked();
     let mut pending_completions = fake_model.pending_completions();
     assert_eq!(
@@ -119,21 +136,156 @@ async fn test_system_prompt(cx: &mut TestAppContext) {
     );
 }
 
+#[gpui::test]
+async fn test_prompt_caching(cx: &mut TestAppContext) {
+    let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
+    let fake_model = model.as_fake();
+
+    // Send initial user message and verify it's cached
+    thread
+        .update(cx, |thread, cx| {
+            thread.send(UserMessageId::new(), ["Message 1"], cx)
+        })
+        .unwrap();
+    cx.run_until_parked();
+
+    let completion = fake_model.pending_completions().pop().unwrap();
+    assert_eq!(
+        completion.messages[1..],
+        vec![LanguageModelRequestMessage {
+            role: Role::User,
+            content: vec!["Message 1".into()],
+            cache: true
+        }]
+    );
+    fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::Text(
+        "Response to Message 1".into(),
+    ));
+    fake_model.end_last_completion_stream();
+    cx.run_until_parked();
+
+    // Send another user message and verify only the latest is cached
+    thread
+        .update(cx, |thread, cx| {
+            thread.send(UserMessageId::new(), ["Message 2"], cx)
+        })
+        .unwrap();
+    cx.run_until_parked();
+
+    let completion = fake_model.pending_completions().pop().unwrap();
+    assert_eq!(
+        completion.messages[1..],
+        vec![
+            LanguageModelRequestMessage {
+                role: Role::User,
+                content: vec!["Message 1".into()],
+                cache: false
+            },
+            LanguageModelRequestMessage {
+                role: Role::Assistant,
+                content: vec!["Response to Message 1".into()],
+                cache: false
+            },
+            LanguageModelRequestMessage {
+                role: Role::User,
+                content: vec!["Message 2".into()],
+                cache: true
+            }
+        ]
+    );
+    fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::Text(
+        "Response to Message 2".into(),
+    ));
+    fake_model.end_last_completion_stream();
+    cx.run_until_parked();
+
+    // Simulate a tool call and verify that the latest tool result is cached
+    thread.update(cx, |thread, _| thread.add_tool(EchoTool));
+    thread
+        .update(cx, |thread, cx| {
+            thread.send(UserMessageId::new(), ["Use the echo tool"], cx)
+        })
+        .unwrap();
+    cx.run_until_parked();
+
+    let tool_use = LanguageModelToolUse {
+        id: "tool_1".into(),
+        name: EchoTool.name().into(),
+        raw_input: json!({"text": "test"}).to_string(),
+        input: json!({"text": "test"}),
+        is_input_complete: true,
+    };
+    fake_model
+        .send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(tool_use.clone()));
+    fake_model.end_last_completion_stream();
+    cx.run_until_parked();
+
+    let completion = fake_model.pending_completions().pop().unwrap();
+    let tool_result = LanguageModelToolResult {
+        tool_use_id: "tool_1".into(),
+        tool_name: EchoTool.name().into(),
+        is_error: false,
+        content: "test".into(),
+        output: Some("test".into()),
+    };
+    assert_eq!(
+        completion.messages[1..],
+        vec![
+            LanguageModelRequestMessage {
+                role: Role::User,
+                content: vec!["Message 1".into()],
+                cache: false
+            },
+            LanguageModelRequestMessage {
+                role: Role::Assistant,
+                content: vec!["Response to Message 1".into()],
+                cache: false
+            },
+            LanguageModelRequestMessage {
+                role: Role::User,
+                content: vec!["Message 2".into()],
+                cache: false
+            },
+            LanguageModelRequestMessage {
+                role: Role::Assistant,
+                content: vec!["Response to Message 2".into()],
+                cache: false
+            },
+            LanguageModelRequestMessage {
+                role: Role::User,
+                content: vec!["Use the echo tool".into()],
+                cache: false
+            },
+            LanguageModelRequestMessage {
+                role: Role::Assistant,
+                content: vec![MessageContent::ToolUse(tool_use)],
+                cache: false
+            },
+            LanguageModelRequestMessage {
+                role: Role::User,
+                content: vec![MessageContent::ToolResult(tool_result)],
+                cache: true
+            }
+        ]
+    );
+}
+
 #[gpui::test]
 #[ignore = "can't run on CI yet"]
 async fn test_basic_tool_calls(cx: &mut TestAppContext) {
-    let ThreadTest { model, thread, .. } = setup(cx, TestModel::Sonnet4).await;
+    let ThreadTest { thread, .. } = setup(cx, TestModel::Sonnet4).await;
 
     // Test a tool call that's likely to complete *before* streaming stops.
     let events = thread
         .update(cx, |thread, cx| {
             thread.add_tool(EchoTool);
             thread.send(
-                model.clone(),
-                "Now test the echo tool with 'Hello'. Does it work? Say 'Yes' or 'No'.",
+                UserMessageId::new(),
+                ["Now test the echo tool with 'Hello'. Does it work? Say 'Yes' or 'No'."],
                 cx,
             )
         })
+        .unwrap()
         .collect()
         .await;
     assert_eq!(stop_events(events), vec![acp::StopReason::EndTurn]);
@@ -144,49 +296,62 @@ async fn test_basic_tool_calls(cx: &mut TestAppContext) {
             thread.remove_tool(&AgentTool::name(&EchoTool));
             thread.add_tool(DelayTool);
             thread.send(
-                model.clone(),
-                "Now call the delay tool with 200ms. When the timer goes off, then you echo the output of the tool.",
+                UserMessageId::new(),
+                [
+                    "Now call the delay tool with 200ms.",
+                    "When the timer goes off, then you echo the output of the tool.",
+                ],
                 cx,
             )
         })
+        .unwrap()
         .collect()
         .await;
     assert_eq!(stop_events(events), vec![acp::StopReason::EndTurn]);
     thread.update(cx, |thread, _cx| {
-        assert!(thread
-            .messages()
-            .last()
-            .unwrap()
-            .content
-            .iter()
-            .any(|content| {
-                if let MessageContent::Text(text) = content {
-                    text.contains("Ding")
-                } else {
-                    false
-                }
-            }));
+        assert!(
+            thread
+                .last_message()
+                .unwrap()
+                .as_agent_message()
+                .unwrap()
+                .content
+                .iter()
+                .any(|content| {
+                    if let AgentMessageContent::Text(text) = content {
+                        text.contains("Ding")
+                    } else {
+                        false
+                    }
+                }),
+            "{}",
+            thread.to_markdown()
+        );
     });
 }
 
 #[gpui::test]
 #[ignore = "can't run on CI yet"]
 async fn test_streaming_tool_calls(cx: &mut TestAppContext) {
-    let ThreadTest { model, thread, .. } = setup(cx, TestModel::Sonnet4).await;
+    let ThreadTest { thread, .. } = setup(cx, TestModel::Sonnet4).await;
 
     // Test a tool call that's likely to complete *before* streaming stops.
-    let mut events = thread.update(cx, |thread, cx| {
-        thread.add_tool(WordListTool);
-        thread.send(model.clone(), "Test the word_list tool.", cx)
-    });
+    let mut events = thread
+        .update(cx, |thread, cx| {
+            thread.add_tool(WordListTool);
+            thread.send(UserMessageId::new(), ["Test the word_list tool."], cx)
+        })
+        .unwrap();
 
     let mut saw_partial_tool_use = false;
     while let Some(event) = events.next().await {
-        if let Ok(AgentResponseEvent::ToolCall(tool_call)) = event {
+        if let Ok(ThreadEvent::ToolCall(tool_call)) = event {
             thread.update(cx, |thread, _cx| {
                 // Look for a tool use in the thread's last message
-                let last_content = thread.messages().last().unwrap().content.last().unwrap();
-                if let MessageContent::ToolUse(last_tool_use) = last_content {
+                let message = thread.last_message().unwrap();
+                let agent_message = message.as_agent_message().unwrap();
+                let last_content = agent_message.content.last().unwrap();
+                if let AgentMessageContent::ToolUse(last_tool_use) = last_content {
                     assert_eq!(last_tool_use.name.as_ref(), "word_list");
                     if tool_call.status == acp::ToolCallStatus::Pending {
                         if !last_tool_use.is_input_complete
@@ -222,10 +387,12 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
     let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
     let fake_model = model.as_fake();
 
-    let mut events = thread.update(cx, |thread, cx| {
-        thread.add_tool(ToolRequiringPermission);
-        thread.send(model.clone(), "abc", cx)
-    });
+    let mut events = thread
+        .update(cx, |thread, cx| {
+            thread.add_tool(ToolRequiringPermission);
+            thread.send(UserMessageId::new(), ["abc"], cx)
+        })
+        .unwrap();
     cx.run_until_parked();
     fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
         LanguageModelToolUse {
@@ -268,14 +435,14 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
     assert_eq!(
         message.content,
         vec![
-            MessageContent::ToolResult(LanguageModelToolResult {
+            language_model::MessageContent::ToolResult(LanguageModelToolResult {
                 tool_use_id: tool_call_auth_1.tool_call.id.0.to_string().into(),
                 tool_name: ToolRequiringPermission.name().into(),
                 is_error: false,
                 content: "Allowed".into(),
-                output: None
+                output: Some("Allowed".into())
             }),
-            MessageContent::ToolResult(LanguageModelToolResult {
+            language_model::MessageContent::ToolResult(LanguageModelToolResult {
                 tool_use_id: tool_call_auth_2.tool_call.id.0.to_string().into(),
                 tool_name: ToolRequiringPermission.name().into(),
                 is_error: true,
@@ -284,6 +451,67 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
             })
         ]
     );
+
+    // Simulate yet another tool call.
+    fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
+        LanguageModelToolUse {
+            id: "tool_id_3".into(),
+            name: ToolRequiringPermission.name().into(),
+            raw_input: "{}".into(),
+            input: json!({}),
+            is_input_complete: true,
+        },
+    ));
+    fake_model.end_last_completion_stream();
+
+    // Respond by always allowing tools.
+    let tool_call_auth_3 = next_tool_call_authorization(&mut events).await;
+    tool_call_auth_3
+        .response
+        .send(tool_call_auth_3.options[0].id.clone())
+        .unwrap();
+    cx.run_until_parked();
+    let completion = fake_model.pending_completions().pop().unwrap();
+    let message = completion.messages.last().unwrap();
+    assert_eq!(
+        message.content,
+        vec![language_model::MessageContent::ToolResult(
+            LanguageModelToolResult {
+                tool_use_id: tool_call_auth_3.tool_call.id.0.to_string().into(),
+                tool_name: ToolRequiringPermission.name().into(),
+                is_error: false,
+                content: "Allowed".into(),
+                output: Some("Allowed".into())
+            }
+        )]
+    );
+
+    // Simulate a final tool call, ensuring we don't trigger authorization.
+    fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
+        LanguageModelToolUse {
+            id: "tool_id_4".into(),
+            name: ToolRequiringPermission.name().into(),
+            raw_input: "{}".into(),
+            input: json!({}),
+            is_input_complete: true,
+        },
+    ));
+    fake_model.end_last_completion_stream();
+    cx.run_until_parked();
+    let completion = fake_model.pending_completions().pop().unwrap();
+    let message = completion.messages.last().unwrap();
+    assert_eq!(
+        message.content,
+        vec![language_model::MessageContent::ToolResult(
+            LanguageModelToolResult {
+                tool_use_id: "tool_id_4".into(),
+                tool_name: ToolRequiringPermission.name().into(),
+                is_error: false,
+                content: "Allowed".into(),
+                output: Some("Allowed".into())
+            }
+        )]
+    );
 }
 
 #[gpui::test]
@@ -291,7 +519,11 @@ async fn test_tool_hallucination(cx: &mut TestAppContext) {
     let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
     let fake_model = model.as_fake();
 
-    let mut events = thread.update(cx, |thread, cx| thread.send(model.clone(), "abc", cx));
+    let mut events = thread
+        .update(cx, |thread, cx| {
+            thread.send(UserMessageId::new(), ["abc"], cx)
+        })
+        .unwrap();
     cx.run_until_parked();
     fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
         LanguageModelToolUse {
@@ -307,28 +539,218 @@ async fn test_tool_hallucination(cx: &mut TestAppContext) {
     let tool_call = expect_tool_call(&mut events).await;
     assert_eq!(tool_call.title, "nonexistent_tool");
     assert_eq!(tool_call.status, acp::ToolCallStatus::Pending);
-    let update = expect_tool_call_update(&mut events).await;
+    let update = expect_tool_call_update_fields(&mut events).await;
     assert_eq!(update.fields.status, Some(acp::ToolCallStatus::Failed));
 }
 
-async fn expect_tool_call(
-    events: &mut UnboundedReceiver<Result<AgentResponseEvent, LanguageModelCompletionError>>,
-) -> acp::ToolCall {
+#[gpui::test]
+async fn test_resume_after_tool_use_limit(cx: &mut TestAppContext) {
+    let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
+    let fake_model = model.as_fake();
+
+    let events = thread
+        .update(cx, |thread, cx| {
+            thread.add_tool(EchoTool);
+            thread.send(UserMessageId::new(), ["abc"], cx)
+        })
+        .unwrap();
+    cx.run_until_parked();
+    let tool_use = LanguageModelToolUse {
+        id: "tool_id_1".into(),
+        name: EchoTool.name().into(),
+        raw_input: "{}".into(),
+        input: serde_json::to_value(&EchoToolInput { text: "def".into() }).unwrap(),
+        is_input_complete: true,
+    };
+    fake_model
+        .send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(tool_use.clone()));
+    fake_model.end_last_completion_stream();
+
+    cx.run_until_parked();
+    let completion = fake_model.pending_completions().pop().unwrap();
+    let tool_result = LanguageModelToolResult {
+        tool_use_id: "tool_id_1".into(),
+        tool_name: EchoTool.name().into(),
+        is_error: false,
+        content: "def".into(),
+        output: Some("def".into()),
+    };
+    assert_eq!(
+        completion.messages[1..],
+        vec![
+            LanguageModelRequestMessage {
+                role: Role::User,
+                content: vec!["abc".into()],
+                cache: false
+            },
+            LanguageModelRequestMessage {
+                role: Role::Assistant,
+                content: vec![MessageContent::ToolUse(tool_use.clone())],
+                cache: false
+            },
+            LanguageModelRequestMessage {
+                role: Role::User,
+                content: vec![MessageContent::ToolResult(tool_result.clone())],
+                cache: true
+            },
+        ]
+    );
+
+    // Simulate reaching tool use limit.
+    fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::StatusUpdate(
+        cloud_llm_client::CompletionRequestStatus::ToolUseLimitReached,
+    ));
+    fake_model.end_last_completion_stream();
+    let last_event = events.collect::<Vec<_>>().await.pop().unwrap();
+    assert!(
+        last_event
+            .unwrap_err()
+            .is::<language_model::ToolUseLimitReachedError>()
+    );
+
+    let events = thread.update(cx, |thread, cx| thread.resume(cx)).unwrap();
+    cx.run_until_parked();
+    let completion = fake_model.pending_completions().pop().unwrap();
+    assert_eq!(
+        completion.messages[1..],
+        vec![
+            LanguageModelRequestMessage {
+                role: Role::User,
+                content: vec!["abc".into()],
+                cache: false
+            },
+            LanguageModelRequestMessage {
+                role: Role::Assistant,
+                content: vec![MessageContent::ToolUse(tool_use)],
+                cache: false
+            },
+            LanguageModelRequestMessage {
+                role: Role::User,
+                content: vec![MessageContent::ToolResult(tool_result)],
+                cache: false
+            },
+            LanguageModelRequestMessage {
+                role: Role::User,
+                content: vec!["Continue where you left off".into()],
+                cache: true
+            }
+        ]
+    );
+
+    fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::Text("Done".into()));
+    fake_model.end_last_completion_stream();
+    events.collect::<Vec<_>>().await;
+    thread.read_with(cx, |thread, _cx| {
+        assert_eq!(
+            thread.last_message().unwrap().to_markdown(),
+            indoc! {"
+                ## Assistant
+
+                Done
+            "}
+        )
+    });
+
+    // Ensure we error if calling resume when tool use limit was *not* reached.
+    let error = thread
+        .update(cx, |thread, cx| thread.resume(cx))
+        .unwrap_err();
+    assert_eq!(
+        error.to_string(),
+        "can only resume after tool use limit is reached"
+    )
+}
+
+#[gpui::test]
+async fn test_send_after_tool_use_limit(cx: &mut TestAppContext) {
+    let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
+    let fake_model = model.as_fake();
+
+    let events = thread
+        .update(cx, |thread, cx| {
+            thread.add_tool(EchoTool);
+            thread.send(UserMessageId::new(), ["abc"], cx)
+        })
+        .unwrap();
+    cx.run_until_parked();
+
+    let tool_use = LanguageModelToolUse {
+        id: "tool_id_1".into(),
+        name: EchoTool.name().into(),
+        raw_input: "{}".into(),
+        input: serde_json::to_value(&EchoToolInput { text: "def".into() }).unwrap(),
+        is_input_complete: true,
+    };
+    let tool_result = LanguageModelToolResult {
+        tool_use_id: "tool_id_1".into(),
+        tool_name: EchoTool.name().into(),
+        is_error: false,
+        content: "def".into(),
+        output: Some("def".into()),
+    };
+    fake_model
+        .send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(tool_use.clone()));
+    fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::StatusUpdate(
+        cloud_llm_client::CompletionRequestStatus::ToolUseLimitReached,
+    ));
+    fake_model.end_last_completion_stream();
+    let last_event = events.collect::<Vec<_>>().await.pop().unwrap();
+    assert!(
+        last_event
+            .unwrap_err()
+            .is::<language_model::ToolUseLimitReachedError>()
+    );
+
+    thread
+        .update(cx, |thread, cx| {
+            thread.send(UserMessageId::new(), vec!["ghi"], cx)
+        })
+        .unwrap();
+    cx.run_until_parked();
+    let completion = fake_model.pending_completions().pop().unwrap();
+    assert_eq!(
+        completion.messages[1..],
+        vec![
+            LanguageModelRequestMessage {
+                role: Role::User,
+                content: vec!["abc".into()],
+                cache: false
+            },
+            LanguageModelRequestMessage {
+                role: Role::Assistant,
+                content: vec![MessageContent::ToolUse(tool_use)],
+                cache: false
+            },
+            LanguageModelRequestMessage {
+                role: Role::User,
+                content: vec![MessageContent::ToolResult(tool_result)],
+                cache: false
+            },
+            LanguageModelRequestMessage {
+                role: Role::User,
+                content: vec!["ghi".into()],
+                cache: true
+            }
+        ]
+    );
+}
+
+async fn expect_tool_call(events: &mut UnboundedReceiver<Result<ThreadEvent>>) -> acp::ToolCall {
     let event = events
         .next()
         .await
         .expect("no tool call authorization event received")
         .unwrap();
     match event {
-        AgentResponseEvent::ToolCall(tool_call) => return tool_call,
+        ThreadEvent::ToolCall(tool_call) => tool_call,
         event => {
             panic!("Unexpected event {event:?}");
         }
     }
 }
 
-async fn expect_tool_call_update(
-    events: &mut UnboundedReceiver<Result<AgentResponseEvent, LanguageModelCompletionError>>,
+async fn expect_tool_call_update_fields(
+    events: &mut UnboundedReceiver<Result<ThreadEvent>>,
 ) -> acp::ToolCallUpdate {
     let event = events
         .next()
@@ -336,7 +758,7 @@ async fn expect_tool_call_update(
         .expect("no tool call authorization event received")
         .unwrap();
     match event {
-        AgentResponseEvent::ToolCallUpdate(tool_call_update) => return tool_call_update,
+        ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateFields(update)) => update,
         event => {
             panic!("Unexpected event {event:?}");
         }
@@ -344,7 +766,7 @@ async fn expect_tool_call_update(
 }
 
 async fn next_tool_call_authorization(
-    events: &mut UnboundedReceiver<Result<AgentResponseEvent, LanguageModelCompletionError>>,
+    events: &mut UnboundedReceiver<Result<ThreadEvent>>,
 ) -> ToolCallAuthorization {
     loop {
         let event = events
@@ -352,7 +774,7 @@ async fn next_tool_call_authorization(
             .await
             .expect("no tool call authorization event received")
             .unwrap();
-        if let AgentResponseEvent::ToolCallAuthorization(tool_call_authorization) = event {
+        if let ThreadEvent::ToolCallAuthorization(tool_call_authorization) = event {
             let permission_kinds = tool_call_authorization
                 .options
                 .iter()
@@ -374,18 +796,23 @@ async fn next_tool_call_authorization(
 #[gpui::test]
 #[ignore = "can't run on CI yet"]
 async fn test_concurrent_tool_calls(cx: &mut TestAppContext) {
-    let ThreadTest { model, thread, .. } = setup(cx, TestModel::Sonnet4).await;
+    let ThreadTest { thread, .. } = setup(cx, TestModel::Sonnet4).await;
 
     // Test concurrent tool calls with different delay times
     let events = thread
         .update(cx, |thread, cx| {
             thread.add_tool(DelayTool);
             thread.send(
-                model.clone(),
-                "Call the delay tool twice in the same message. Once with 100ms. Once with 300ms. When both timers are complete, describe the outputs.",
+                UserMessageId::new(),
+                [
+                    "Call the delay tool twice in the same message.",
+                    "Once with 100ms. Once with 300ms.",
+                    "When both timers are complete, describe the outputs.",
+                ],
                 cx,
             )
         })
+        .unwrap()
         .collect()
         .await;
 
@@ -393,12 +820,13 @@ async fn test_concurrent_tool_calls(cx: &mut TestAppContext) {
     assert_eq!(stop_reasons, vec![acp::StopReason::EndTurn]);
 
     thread.update(cx, |thread, _cx| {
-        let last_message = thread.messages().last().unwrap();
-        let text = last_message
+        let last_message = thread.last_message().unwrap();
+        let agent_message = last_message.as_agent_message().unwrap();
+        let text = agent_message
             .content
             .iter()
             .filter_map(|content| {
-                if let MessageContent::Text(text) = content {
+                if let AgentMessageContent::Text(text) = content {
                     Some(text.as_str())
                 } else {
                     None
@@ -411,83 +839,251 @@ async fn test_concurrent_tool_calls(cx: &mut TestAppContext) {
 }
 
 #[gpui::test]
-#[ignore = "can't run on CI yet"]
-async fn test_cancellation(cx: &mut TestAppContext) {
-    let ThreadTest { model, thread, .. } = setup(cx, TestModel::Sonnet4).await;
+async fn test_profiles(cx: &mut TestAppContext) {
+    let ThreadTest {
+        model, thread, fs, ..
+    } = setup(cx, TestModel::Fake).await;
+    let fake_model = model.as_fake();
 
-    let mut events = thread.update(cx, |thread, cx| {
-        thread.add_tool(InfiniteTool);
+    thread.update(cx, |thread, _cx| {
+        thread.add_tool(DelayTool);
         thread.add_tool(EchoTool);
-        thread.send(
-            model.clone(),
-            "Call the echo tool and then call the infinite tool, then explain their output",
-            cx,
-        )
+        thread.add_tool(InfiniteTool);
     });
 
-    // Wait until both tools are called.
-    let mut expected_tool_calls = vec!["echo", "infinite"];
-    let mut echo_id = None;
-    let mut echo_completed = false;
-    while let Some(event) = events.next().await {
-        match event.unwrap() {
-            AgentResponseEvent::ToolCall(tool_call) => {
-                assert_eq!(tool_call.title, expected_tool_calls.remove(0));
-                if tool_call.title == "echo" {
-                    echo_id = Some(tool_call.id);
-                }
-            }
-            AgentResponseEvent::ToolCallUpdate(acp::ToolCallUpdate {
-                id,
-                fields:
-                    acp::ToolCallUpdateFields {
-                        status: Some(acp::ToolCallStatus::Completed),
-                        ..
+    // Override profiles and wait for settings to be loaded.
+    fs.insert_file(
+        paths::settings_file(),
+        json!({
+            "agent": {
+                "profiles": {
+                    "test-1": {
+                        "name": "Test Profile 1",
+                        "tools": {
+                            EchoTool.name(): true,
+                            DelayTool.name(): true,
+                        }
                     },
-            }) if Some(&id) == echo_id.as_ref() => {
-                echo_completed = true;
+                    "test-2": {
+                        "name": "Test Profile 2",
+                        "tools": {
+                            InfiniteTool.name(): true,
+                        }
+                    }
+                }
             }
-            _ => {}
-        }
-
-        if expected_tool_calls.is_empty() && echo_completed {
-            break;
-        }
-    }
-
-    // Cancel the current send and ensure that the event stream is closed, even
-    // if one of the tools is still running.
-    thread.update(cx, |thread, _cx| thread.cancel());
-    events.collect::<Vec<_>>().await;
+        })
+        .to_string()
+        .into_bytes(),
+    )
+    .await;
+    cx.run_until_parked();
 
-    // Ensure we can still send a new message after cancellation.
-    let events = thread
+    // Test that test-1 profile (default) has echo and delay tools
+    thread
         .update(cx, |thread, cx| {
-            thread.send(model.clone(), "Testing: reply with 'Hello' then stop.", cx)
+            thread.set_profile(AgentProfileId("test-1".into()));
+            thread.send(UserMessageId::new(), ["test"], cx)
         })
+        .unwrap();
+    cx.run_until_parked();
+
+    let mut pending_completions = fake_model.pending_completions();
+    assert_eq!(pending_completions.len(), 1);
+    let completion = pending_completions.pop().unwrap();
+    let tool_names: Vec<String> = completion
+        .tools
+        .iter()
+        .map(|tool| tool.name.clone())
+        .collect();
+    assert_eq!(tool_names, vec![DelayTool.name(), EchoTool.name()]);
+    fake_model.end_last_completion_stream();
+
+    // Switch to test-2 profile, and verify that it has only the infinite tool.
+    thread
+        .update(cx, |thread, cx| {
+            thread.set_profile(AgentProfileId("test-2".into()));
+            thread.send(UserMessageId::new(), ["test2"], cx)
+        })
+        .unwrap();
+    cx.run_until_parked();
+    let mut pending_completions = fake_model.pending_completions();
+    assert_eq!(pending_completions.len(), 1);
+    let completion = pending_completions.pop().unwrap();
+    let tool_names: Vec<String> = completion
+        .tools
+        .iter()
+        .map(|tool| tool.name.clone())
+        .collect();
+    assert_eq!(tool_names, vec![InfiniteTool.name()]);
+}
+
+#[gpui::test]
+#[ignore = "can't run on CI yet"]
+async fn test_cancellation(cx: &mut TestAppContext) {
+    let ThreadTest { thread, .. } = setup(cx, TestModel::Sonnet4).await;
+
+    let mut events = thread
+        .update(cx, |thread, cx| {
+            thread.add_tool(InfiniteTool);
+            thread.add_tool(EchoTool);
+            thread.send(
+                UserMessageId::new(),
+                ["Call the echo tool, then call the infinite tool, then explain their output"],
+                cx,
+            )
+        })
+        .unwrap();
+
+    // Wait until both tools are called.
+    let mut expected_tools = vec!["Echo", "Infinite Tool"];
+    let mut echo_id = None;
+    let mut echo_completed = false;
+    while let Some(event) = events.next().await {
+        match event.unwrap() {
+            ThreadEvent::ToolCall(tool_call) => {
+                assert_eq!(tool_call.title, expected_tools.remove(0));
+                if tool_call.title == "Echo" {
+                    echo_id = Some(tool_call.id);
+                }
+            }
+            ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateFields(
+                acp::ToolCallUpdate {
+                    id,
+                    fields:
+                        acp::ToolCallUpdateFields {
+                            status: Some(acp::ToolCallStatus::Completed),
+                            ..
+                        },
+                },
+            )) if Some(&id) == echo_id.as_ref() => {
+                echo_completed = true;
+            }
+            _ => {}
+        }
+
+        if expected_tools.is_empty() && echo_completed {
+            break;
+        }
+    }
+
+    // Cancel the current send and ensure that the event stream is closed, even
+    // if one of the tools is still running.
+    thread.update(cx, |thread, cx| thread.cancel(cx));
+    let events = events.collect::<Vec<_>>().await;
+    let last_event = events.last();
+    assert!(
+        matches!(
+            last_event,
+            Some(Ok(ThreadEvent::Stop(acp::StopReason::Cancelled)))
+        ),
+        "unexpected event {last_event:?}"
+    );
+
+    // Ensure we can still send a new message after cancellation.
+    let events = thread
+        .update(cx, |thread, cx| {
+            thread.send(
+                UserMessageId::new(),
+                ["Testing: reply with 'Hello' then stop."],
+                cx,
+            )
+        })
+        .unwrap()
         .collect::<Vec<_>>()
         .await;
     thread.update(cx, |thread, _cx| {
+        let message = thread.last_message().unwrap();
+        let agent_message = message.as_agent_message().unwrap();
         assert_eq!(
-            thread.messages().last().unwrap().content,
-            vec![MessageContent::Text("Hello".to_string())]
+            agent_message.content,
+            vec![AgentMessageContent::Text("Hello".to_string())]
         );
     });
     assert_eq!(stop_events(events), vec![acp::StopReason::EndTurn]);
 }
 
+#[gpui::test]
+async fn test_in_progress_send_canceled_by_next_send(cx: &mut TestAppContext) {
+    let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
+    let fake_model = model.as_fake();
+
+    let events_1 = thread
+        .update(cx, |thread, cx| {
+            thread.send(UserMessageId::new(), ["Hello 1"], cx)
+        })
+        .unwrap();
+    cx.run_until_parked();
+    fake_model.send_last_completion_stream_text_chunk("Hey 1!");
+    cx.run_until_parked();
+
+    let events_2 = thread
+        .update(cx, |thread, cx| {
+            thread.send(UserMessageId::new(), ["Hello 2"], cx)
+        })
+        .unwrap();
+    cx.run_until_parked();
+    fake_model.send_last_completion_stream_text_chunk("Hey 2!");
+    fake_model
+        .send_last_completion_stream_event(LanguageModelCompletionEvent::Stop(StopReason::EndTurn));
+    fake_model.end_last_completion_stream();
+
+    let events_1 = events_1.collect::<Vec<_>>().await;
+    assert_eq!(stop_events(events_1), vec![acp::StopReason::Cancelled]);
+    let events_2 = events_2.collect::<Vec<_>>().await;
+    assert_eq!(stop_events(events_2), vec![acp::StopReason::EndTurn]);
+}
+
+#[gpui::test]
+async fn test_subsequent_successful_sends_dont_cancel(cx: &mut TestAppContext) {
+    let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
+    let fake_model = model.as_fake();
+
+    let events_1 = thread
+        .update(cx, |thread, cx| {
+            thread.send(UserMessageId::new(), ["Hello 1"], cx)
+        })
+        .unwrap();
+    cx.run_until_parked();
+    fake_model.send_last_completion_stream_text_chunk("Hey 1!");
+    fake_model
+        .send_last_completion_stream_event(LanguageModelCompletionEvent::Stop(StopReason::EndTurn));
+    fake_model.end_last_completion_stream();
+    let events_1 = events_1.collect::<Vec<_>>().await;
+
+    let events_2 = thread
+        .update(cx, |thread, cx| {
+            thread.send(UserMessageId::new(), ["Hello 2"], cx)
+        })
+        .unwrap();
+    cx.run_until_parked();
+    fake_model.send_last_completion_stream_text_chunk("Hey 2!");
+    fake_model
+        .send_last_completion_stream_event(LanguageModelCompletionEvent::Stop(StopReason::EndTurn));
+    fake_model.end_last_completion_stream();
+    let events_2 = events_2.collect::<Vec<_>>().await;
+
+    assert_eq!(stop_events(events_1), vec![acp::StopReason::EndTurn]);
+    assert_eq!(stop_events(events_2), vec![acp::StopReason::EndTurn]);
+}
+
 #[gpui::test]
 async fn test_refusal(cx: &mut TestAppContext) {
     let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
     let fake_model = model.as_fake();
 
-    let events = thread.update(cx, |thread, cx| thread.send(model.clone(), "Hello", cx));
+    let events = thread
+        .update(cx, |thread, cx| {
+            thread.send(UserMessageId::new(), ["Hello"], cx)
+        })
+        .unwrap();
     cx.run_until_parked();
     thread.read_with(cx, |thread, _| {
         assert_eq!(
             thread.to_markdown(),
             indoc! {"
-                ## user
+                ## User
+
                 Hello
             "}
         );

crates/agent2/src/tests/test_tools.rs 🔗

@@ -7,13 +7,14 @@ use std::future;
 #[derive(JsonSchema, Serialize, Deserialize)]
 pub struct EchoToolInput {
     /// The text to echo.
-    text: String,
+    pub text: String,
 }
 
 pub struct EchoTool;
 
 impl AgentTool for EchoTool {
     type Input = EchoToolInput;
+    type Output = String;
 
     fn name(&self) -> SharedString {
         "echo".into()
@@ -23,7 +24,7 @@ impl AgentTool for EchoTool {
         acp::ToolKind::Other
     }
 
-    fn initial_title(&self, _: Self::Input) -> SharedString {
+    fn initial_title(&self, _input: Result<Self::Input, serde_json::Value>) -> SharedString {
         "Echo".into()
     }
 
@@ -48,13 +49,18 @@ pub struct DelayTool;
 
 impl AgentTool for DelayTool {
     type Input = DelayToolInput;
+    type Output = String;
 
     fn name(&self) -> SharedString {
         "delay".into()
     }
 
-    fn initial_title(&self, input: Self::Input) -> SharedString {
-        format!("Delay {}ms", input.ms).into()
+    fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
+        if let Ok(input) = input {
+            format!("Delay {}ms", input.ms).into()
+        } else {
+            "Delay".into()
+        }
     }
 
     fn kind(&self) -> acp::ToolKind {
@@ -84,6 +90,7 @@ pub struct ToolRequiringPermission;
 
 impl AgentTool for ToolRequiringPermission {
     type Input = ToolRequiringPermissionInput;
+    type Output = String;
 
     fn name(&self) -> SharedString {
         "tool_requiring_permission".into()
@@ -93,22 +100,19 @@ impl AgentTool for ToolRequiringPermission {
         acp::ToolKind::Other
     }
 
-    fn initial_title(&self, _input: Self::Input) -> SharedString {
+    fn initial_title(&self, _input: Result<Self::Input, serde_json::Value>) -> SharedString {
         "This tool requires permission".into()
     }
 
     fn run(
         self: Arc<Self>,
-        input: Self::Input,
+        _input: Self::Input,
         event_stream: ToolCallEventStream,
         cx: &mut App,
-    ) -> Task<Result<String>>
-    where
-        Self: Sized,
-    {
-        let auth_check = self.authorize(input, event_stream);
+    ) -> Task<Result<String>> {
+        let authorize = event_stream.authorize("Authorize?", cx);
         cx.foreground_executor().spawn(async move {
-            auth_check.await?;
+            authorize.await?;
             Ok("Allowed".to_string())
         })
     }
@@ -121,6 +125,7 @@ pub struct InfiniteTool;
 
 impl AgentTool for InfiniteTool {
     type Input = InfiniteToolInput;
+    type Output = String;
 
     fn name(&self) -> SharedString {
         "infinite".into()
@@ -130,8 +135,8 @@ impl AgentTool for InfiniteTool {
         acp::ToolKind::Other
     }
 
-    fn initial_title(&self, _input: Self::Input) -> SharedString {
-        "This is the tool that never ends... it just goes on and on my friends!".into()
+    fn initial_title(&self, _input: Result<Self::Input, serde_json::Value>) -> SharedString {
+        "Infinite Tool".into()
     }
 
     fn run(
@@ -171,19 +176,20 @@ pub struct WordListTool;
 
 impl AgentTool for WordListTool {
     type Input = WordListInput;
+    type Output = String;
 
     fn name(&self) -> SharedString {
         "word_list".into()
     }
 
-    fn initial_title(&self, _input: Self::Input) -> SharedString {
-        "List of random words".into()
-    }
-
     fn kind(&self) -> acp::ToolKind {
         acp::ToolKind::Other
     }
 
+    fn initial_title(&self, _input: Result<Self::Input, serde_json::Value>) -> SharedString {
+        "List of random words".into()
+    }
+
     fn run(
         self: Arc<Self>,
         _input: Self::Input,

crates/agent2/src/thread.rs 🔗

@@ -1,55 +1,369 @@
-use crate::templates::{SystemPromptTemplate, Template, Templates};
+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 anyhow::{anyhow, Context as _, Result};
-use assistant_tool::{adapt_schema_to_format, ActionLog};
-use cloud_llm_client::{CompletionIntent, CompletionMode};
-use collections::HashMap;
+use agent_settings::{
+    AgentProfileId, AgentSettings, CompletionMode, SUMMARIZE_THREAD_DETAILED_PROMPT,
+    SUMMARIZE_THREAD_PROMPT,
+};
+use anyhow::{Context as _, Result, anyhow};
+use assistant_tool::adapt_schema_to_format;
+use chrono::{DateTime, Utc};
+use client::{ModelRequestUsage, RequestUsage};
+use cloud_llm_client::{CompletionIntent, CompletionRequestStatus, UsageLimit};
+use collections::{HashMap, IndexMap};
+use fs::Fs;
 use futures::{
+    FutureExt,
     channel::{mpsc, oneshot},
+    future::Shared,
     stream::FuturesUnordered,
 };
-use gpui::{App, Context, Entity, SharedString, Task};
+use git::repository::DiffType;
+use gpui::{
+    App, AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task, WeakEntity,
+};
 use language_model::{
-    LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent,
-    LanguageModelRequest, LanguageModelRequestMessage, LanguageModelRequestTool,
-    LanguageModelToolResult, LanguageModelToolResultContent, LanguageModelToolSchemaFormat,
-    LanguageModelToolUse, LanguageModelToolUseId, MessageContent, Role, StopReason,
+    LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelExt,
+    LanguageModelImage, LanguageModelProviderId, LanguageModelRegistry, LanguageModelRequest,
+    LanguageModelRequestMessage, LanguageModelRequestTool, LanguageModelToolResult,
+    LanguageModelToolResultContent, LanguageModelToolSchemaFormat, LanguageModelToolUse,
+    LanguageModelToolUseId, Role, SelectedModel, StopReason, TokenUsage,
+};
+use project::{
+    Project,
+    git_store::{GitStore, RepositoryState},
 };
-use log;
-use project::Project;
 use prompt_store::ProjectContext;
 use schemars::{JsonSchema, Schema};
 use serde::{Deserialize, Serialize};
+use settings::{Settings, update_settings_file};
 use smol::stream::StreamExt;
-use std::{cell::RefCell, collections::BTreeMap, fmt::Write, future::Future, rc::Rc, sync::Arc};
-use util::{markdown::MarkdownCodeBlock, ResultExt};
+use std::{
+    collections::BTreeMap,
+    path::Path,
+    sync::Arc,
+    time::{Duration, Instant},
+};
+use std::{fmt::Write, ops::Range};
+use util::{ResultExt, markdown::MarkdownCodeBlock};
+use uuid::Uuid;
+
+const TOOL_CANCELED_MESSAGE: &str = "Tool canceled by user";
+
+/// 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)]
-pub struct AgentMessage {
-    pub role: Role,
-    pub content: Vec<MessageContent>,
+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 => "[resumed after tool use limit was reached]".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_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 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 symbol_context,
+                                "\n{}",
+                                MarkdownCodeBlock {
+                                    tag: &codeblock_tag(abs_path, None),
+                                    text: &content.to_string(),
+                                }
+                            )
+                            .ok();
+                        }
+                        MentionUri::Directory { .. } => {
+                            write!(&mut directory_context, "\n{}\n", content).ok();
+                        }
+                        MentionUri::Symbol {
+                            path, line_range, ..
+                        }
+                        | MentionUri::Selection {
+                            path, line_range, ..
+                        } => {
+                            write!(
+                                &mut rules_context,
+                                "\n{}",
+                                MarkdownCodeBlock {
+                                    tag: &codeblock_tag(path, 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 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<&Range<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 = format!("## {}\n", self.role);
+        let mut markdown = String::from("## Assistant\n\n");
 
         for content in &self.content {
             match content {
-                MessageContent::Text(text) => {
+                AgentMessageContent::Text(text) => {
                     markdown.push_str(text);
                     markdown.push('\n');
                 }
-                MessageContent::Thinking { text, .. } => {
+                AgentMessageContent::Thinking { text, .. } => {
                     markdown.push_str("<think>");
                     markdown.push_str(text);
                     markdown.push_str("</think>\n");
                 }
-                MessageContent::RedactedThinking(_) => markdown.push_str("<redacted_thinking />\n"),
-                MessageContent::Image(_) => {
-                    markdown.push_str("<image />\n");
+                AgentMessageContent::RedactedThinking(_) => {
+                    markdown.push_str("<redacted_thinking />\n")
                 }
-                MessageContent::ToolUse(tool_use) => {
+                AgentMessageContent::ToolUse(tool_use) => {
                     markdown.push_str(&format!(
                         "**Tool Use**: {} (ID: {})\n",
                         tool_use.name, tool_use.id
@@ -62,99 +376,578 @@ impl AgentMessage {
                         }
                     ));
                 }
-                MessageContent::ToolResult(tool_result) => {
-                    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();
-                        }
-                    }
+        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");
+            }
 
-                    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();
-                    }
+            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 {
+            let chunk = match chunk {
+                AgentMessageContent::Text(text) => {
+                    language_model::MessageContent::Text(text.clone())
+                }
+                AgentMessageContent::Thinking { text, signature } => {
+                    language_model::MessageContent::Thinking {
+                        text: text.clone(),
+                        signature: signature.clone(),
+                    }
+                }
+                AgentMessageContent::RedactedThinking(value) => {
+                    language_model::MessageContent::RedactedThinking(value.clone())
+                }
+                AgentMessageContent::ToolUse(value) => {
+                    language_model::MessageContent::ToolUse(value.clone())
+                }
+            };
+            assistant_message.content.push(chunk);
+        }
+
+        let mut user_message = LanguageModelRequestMessage {
+            role: Role::User,
+            content: Vec::new(),
+            cache: false,
+        };
+
+        for tool_result in self.tool_results.values() {
+            user_message
+                .content
+                .push(language_model::MessageContent::ToolResult(
+                    tool_result.clone(),
+                ));
+        }
+
+        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)]
-pub enum AgentResponseEvent {
+#[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(String),
+    Thinking {
+        text: String,
+        signature: Option<String>,
+    },
+    RedactedThinking(String),
+    ToolUse(LanguageModelToolUse),
+}
+
+#[derive(Debug)]
+pub enum ThreadEvent {
+    UserMessage(UserMessage),
+    AgentText(String),
+    AgentThinking(String),
     ToolCall(acp::ToolCall),
-    ToolCallUpdate(acp::ToolCallUpdate),
+    ToolCallUpdate(acp_thread::ToolCallUpdate),
     ToolCallAuthorization(ToolCallAuthorization),
+    TitleUpdate(SharedString),
+    Retry(acp_thread::RetryStatus),
     Stop(acp::StopReason),
 }
 
 #[derive(Debug)]
 pub struct ToolCallAuthorization {
-    pub tool_call: acp::ToolCall,
+    pub tool_call: acp::ToolCallUpdate,
     pub options: Vec<acp::PermissionOption>,
     pub response: oneshot::Sender<acp::PermissionOptionId>,
 }
 
 pub struct Thread {
-    messages: Vec<AgentMessage>,
+    id: acp::SessionId,
+    prompt_id: PromptId,
+    updated_at: DateTime<Utc>,
+    title: Option<SharedString>,
+    summary: Option<SharedString>,
+    messages: Vec<Message>,
     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<Task<()>>,
-    pending_tool_uses: HashMap<LanguageModelToolUseId, LanguageModelToolUse>,
+    running_turn: Option<RunningTurn>,
+    pending_message: Option<AgentMessage>,
     tools: BTreeMap<SharedString, Arc<dyn AnyAgentTool>>,
-    project_context: Rc<RefCell<ProjectContext>>,
+    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>,
-    pub selected_model: Arc<dyn LanguageModel>,
-    action_log: Entity<ActionLog>,
+    model: Option<Arc<dyn LanguageModel>>,
+    summarization_model: Option<Arc<dyn LanguageModel>>,
+    pub(crate) project: Entity<Project>,
+    pub(crate) action_log: Entity<ActionLog>,
 }
 
 impl Thread {
     pub fn new(
-        _project: Entity<Project>,
-        project_context: Rc<RefCell<ProjectContext>>,
+        project: Entity<Project>,
+        project_context: Entity<ProjectContext>,
+        context_server_registry: Entity<ContextServerRegistry>,
         action_log: Entity<ActionLog>,
         templates: Arc<Templates>,
-        default_model: Arc<dyn LanguageModel>,
+        model: Option<Arc<dyn LanguageModel>>,
+        cx: &mut Context<Self>,
     ) -> Self {
+        let profile_id = AgentSettings::get_global(cx).default_profile.clone();
         Self {
+            id: acp::SessionId(uuid::Uuid::new_v4().to_string().into()),
+            prompt_id: PromptId::new(),
+            updated_at: Utc::now(),
+            title: None,
+            summary: None,
             messages: Vec::new(),
-            completion_mode: CompletionMode::Normal,
+            completion_mode: AgentSettings::get_global(cx).preferred_completion_mode,
             running_turn: None,
-            pending_tool_uses: HashMap::default(),
+            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,
-            selected_model: default_model,
+            model,
+            summarization_model: None,
+            project,
             action_log,
         }
     }
 
-    pub fn set_mode(&mut self, mode: CompletionMode) {
+    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 Some(tool) = self.tools.get(tool_use.name.as_ref()) else {
+            stream
+                .0
+                .unbounded_send(Ok(ThreadEvent::ToolCall(acp::ToolCall {
+                    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());
+        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(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)
+        });
+
+        Self {
+            id,
+            prompt_id: PromptId::new(),
+            title: if db_thread.title.is_empty() {
+                None
+            } else {
+                Some(db_thread.title.clone())
+            },
+            summary: db_thread.detailed_summary,
+            messages: db_thread.messages,
+            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,
+        }
+    }
+
+    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 |_, cx| {
+            let worktree_snapshots = futures::future::join_all(worktree_snapshots).await;
+
+            let mut unsaved_buffers = Vec::new();
+            cx.update(|app_cx| {
+                let buffer_store = project.read(app_cx).buffer_store();
+                for buffer_handle in buffer_store.read(app_cx).buffers() {
+                    let buffer = buffer_handle.read(app_cx);
+                    if buffer.is_dirty()
+                        && let Some(file) = buffer.file()
+                    {
+                        let path = file.path().to_string_lossy().to_string();
+                        unsaved_buffers.push(path);
+                    }
+                }
+            })
+            .ok();
+
+            Arc::new(ProjectSnapshot {
+                worktree_snapshots,
+                unsaved_buffer_paths: unsaved_buffers,
+                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().to_string();
+                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_usage = self.latest_token_usage();
+        if old_usage != new_usage {
+            cx.emit(TokenUsageUpdated(new_usage));
+        }
+        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()
     }
 
-    pub fn messages(&self) -> &[AgentMessage] {
-        &self.messages
+    #[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, 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(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(), cx));
+        self.add_tool(ThinkingTool);
+        self.add_tool(WebSearchTool); // TODO: Enable this only if it's a zed model.
     }
 
     pub fn add_tool(&mut self, tool: impl AgentTool) {

crates/agent2/src/tool_schema.rs 🔗

@@ -0,0 +1,43 @@
+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,7 +1,35 @@
+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;
 
+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::*;

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

@@ -0,0 +1,239 @@
+use crate::{AgentToolOutput, AnyAgentTool, ToolCallEventStream};
+use agent_client_protocol::ToolKind;
+use anyhow::{Result, anyhow, bail};
+use collections::{BTreeMap, HashMap};
+use context_server::ContextServerId;
+use gpui::{App, Context, Entity, SharedString, Task};
+use project::context_server_store::{ContextServerStatus, ContextServerStore};
+use std::sync::Arc;
+use util::ResultExt;
+
+pub struct ContextServerRegistry {
+    server_store: Entity<ContextServerStore>,
+    registered_servers: HashMap<ContextServerId, RegisteredContextServer>,
+    _subscription: gpui::Subscription,
+}
+
+struct RegisteredContextServer {
+    tools: BTreeMap<SharedString, Arc<dyn AnyAgentTool>>,
+    load_tools: Task<Result<()>>,
+}
+
+impl ContextServerRegistry {
+    pub fn new(server_store: Entity<ContextServerStore>, cx: &mut Context<Self>) -> Self {
+        let mut this = Self {
+            server_store: server_store.clone(),
+            registered_servers: HashMap::default(),
+            _subscription: cx.subscribe(&server_store, Self::handle_context_server_store_event),
+        };
+        for server in server_store.read(cx).running_servers() {
+            this.reload_tools_for_server(server.id(), cx);
+        }
+        this
+    }
+
+    pub fn servers(
+        &self,
+    ) -> impl Iterator<
+        Item = (
+            &ContextServerId,
+            &BTreeMap<SharedString, Arc<dyn AnyAgentTool>>,
+        ),
+    > {
+        self.registered_servers
+            .iter()
+            .map(|(id, server)| (id, &server.tools))
+    }
+
+    fn reload_tools_for_server(&mut self, server_id: ContextServerId, cx: &mut Context<Self>) {
+        let Some(server) = self.server_store.read(cx).get_running_server(&server_id) else {
+            return;
+        };
+        let Some(client) = server.client() else {
+            return;
+        };
+        if !client.capable(context_server::protocol::ServerCapability::Tools) {
+            return;
+        }
+
+        let registered_server =
+            self.registered_servers
+                .entry(server_id.clone())
+                .or_insert(RegisteredContextServer {
+                    tools: BTreeMap::default(),
+                    load_tools: Task::ready(Ok(())),
+                });
+        registered_server.load_tools = cx.spawn(async move |this, cx| {
+            let response = client
+                .request::<context_server::types::requests::ListTools>(())
+                .await;
+
+            this.update(cx, |this, cx| {
+                let Some(registered_server) = this.registered_servers.get_mut(&server_id) else {
+                    return;
+                };
+
+                registered_server.tools.clear();
+                if let Some(response) = response.log_err() {
+                    for tool in response.tools {
+                        let tool = Arc::new(ContextServerTool::new(
+                            this.server_store.clone(),
+                            server.id(),
+                            tool,
+                        ));
+                        registered_server.tools.insert(tool.name(), tool);
+                    }
+                    cx.notify();
+                }
+            })
+        });
+    }
+
+    fn handle_context_server_store_event(
+        &mut self,
+        _: Entity<ContextServerStore>,
+        event: &project::context_server_store::Event,
+        cx: &mut Context<Self>,
+    ) {
+        match event {
+            project::context_server_store::Event::ServerStatusChanged { server_id, status } => {
+                match status {
+                    ContextServerStatus::Starting => {}
+                    ContextServerStatus::Running => {
+                        self.reload_tools_for_server(server_id.clone(), cx);
+                    }
+                    ContextServerStatus::Stopped | ContextServerStatus::Error(_) => {
+                        self.registered_servers.remove(server_id);
+                        cx.notify();
+                    }
+                }
+            }
+        }
+    }
+}
+
+struct ContextServerTool {
+    store: Entity<ContextServerStore>,
+    server_id: ContextServerId,
+    tool: context_server::types::Tool,
+}
+
+impl ContextServerTool {
+    fn new(
+        store: Entity<ContextServerStore>,
+        server_id: ContextServerId,
+        tool: context_server::types::Tool,
+    ) -> Self {
+        Self {
+            store,
+            server_id,
+            tool,
+        }
+    }
+}
+
+impl AnyAgentTool for ContextServerTool {
+    fn name(&self) -> SharedString {
+        self.tool.name.clone().into()
+    }
+
+    fn description(&self) -> SharedString {
+        self.tool.description.clone().unwrap_or_default().into()
+    }
+
+    fn kind(&self) -> ToolKind {
+        ToolKind::Other
+    }
+
+    fn initial_title(&self, _input: serde_json::Value) -> SharedString {
+        format!("Run MCP tool `{}`", self.tool.name).into()
+    }
+
+    fn input_schema(
+        &self,
+        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)?;
+        Ok(match schema {
+            serde_json::Value::Null => {
+                serde_json::json!({ "type": "object", "properties": [] })
+            }
+            serde_json::Value::Object(map) if map.is_empty() => {
+                serde_json::json!({ "type": "object", "properties": [] })
+            }
+            _ => schema,
+        })
+    }
+
+    fn run(
+        self: Arc<Self>,
+        input: serde_json::Value,
+        _event_stream: ToolCallEventStream,
+        cx: &mut App,
+    ) -> Task<Result<AgentToolOutput>> {
+        let Some(server) = self.store.read(cx).get_running_server(&self.server_id) else {
+            return Task::ready(Err(anyhow!("Context server not found")));
+        };
+        let tool_name = self.tool.name.clone();
+
+        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 {
+                    context_server::types::ToolResponseContent::Text { text } => {
+                        result.push_str(&text);
+                    }
+                    context_server::types::ToolResponseContent::Image { .. } => {
+                        log::warn!("Ignoring image content from tool response");
+                    }
+                    context_server::types::ToolResponseContent::Audio { .. } => {
+                        log::warn!("Ignoring audio content from tool response");
+                    }
+                    context_server::types::ToolResponseContent::Resource { .. } => {
+                        log::warn!("Ignoring resource content from tool response");
+                    }
+                }
+            }
+            Ok(AgentToolOutput {
+                raw_output: result.clone().into(),
+                llm_output: result.into(),
+            })
+        })
+    }
+
+    fn replay(
+        &self,
+        _input: serde_json::Value,
+        _output: serde_json::Value,
+        _event_stream: ToolCallEventStream,
+        _cx: &mut App,
+    ) -> Result<()> {
+        Ok(())
+    }
+}

crates/agent2/src/tools/copy_path_tool.rs 🔗

@@ -0,0 +1,111 @@
+use crate::{AgentTool, ToolCallEventStream};
+use agent_client_protocol::ToolKind;
+use anyhow::{Context as _, Result, anyhow};
+use gpui::{App, AppContext, Entity, SharedString, Task};
+use project::Project;
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+use std::sync::Arc;
+use util::markdown::MarkdownInlineCode;
+
+/// Copies a file or directory in the project, and returns confirmation that the copy succeeded.
+/// Directory contents will be copied recursively (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.
+#[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 {
+    project: Entity<Project>,
+}
+
+impl CopyPathTool {
+    pub fn new(project: Entity<Project>) -> Self {
+        Self { project }
+    }
+}
+
+impl AgentTool for CopyPathTool {
+    type Input = CopyPathToolInput;
+    type Output = String;
+
+    fn name(&self) -> SharedString {
+        "copy_path".into()
+    }
+
+    fn kind(&self) -> ToolKind {
+        ToolKind::Move
+    }
+
+    fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> ui::SharedString {
+        if let Ok(input) = input {
+            let src = MarkdownInlineCode(&input.source_path);
+            let dest = MarkdownInlineCode(&input.destination_path);
+            format!("Copy {src} to {dest}").into()
+        } else {
+            "Copy path".into()
+        }
+    }
+
+    fn run(
+        self: Arc<Self>,
+        input: Self::Input,
+        _event_stream: ToolCallEventStream,
+        cx: &mut App,
+    ) -> Task<Result<Self::Output>> {
+        let copy_task = self.project.update(cx, |project, cx| {
+            match project
+                .find_project_path(&input.source_path, cx)
+                .and_then(|project_path| project.entry_for_path(&project_path, cx))
+            {
+                Some(entity) => match project.find_project_path(&input.destination_path, cx) {
+                    Some(project_path) => {
+                        project.copy_entry(entity.id, None, project_path.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
+            ))
+        })
+    }
+}

crates/agent2/src/tools/create_directory_tool.rs 🔗

@@ -0,0 +1,86 @@
+use agent_client_protocol::ToolKind;
+use anyhow::{Context as _, Result, anyhow};
+use gpui::{App, Entity, SharedString, Task};
+use project::Project;
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+use std::sync::Arc;
+use util::markdown::MarkdownInlineCode;
+
+use crate::{AgentTool, ToolCallEventStream};
+
+/// Creates a new directory at the specified path within the project. Returns confirmation that the directory was created.
+///
+/// This tool creates a directory and all necessary parent directories (similar to `mkdir -p`). It should be used whenever you need to create new directories within the project.
+#[derive(Debug, Serialize, Deserialize, JsonSchema)]
+pub struct CreateDirectoryToolInput {
+    /// The path of the new directory.
+    ///
+    /// <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 {
+    project: Entity<Project>,
+}
+
+impl CreateDirectoryTool {
+    pub fn new(project: Entity<Project>) -> Self {
+        Self { project }
+    }
+}
+
+impl AgentTool for CreateDirectoryTool {
+    type Input = CreateDirectoryToolInput;
+    type Output = String;
+
+    fn name(&self) -> SharedString {
+        "create_directory".into()
+    }
+
+    fn kind(&self) -> ToolKind {
+        ToolKind::Read
+    }
+
+    fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
+        if let Ok(input) = input {
+            format!("Create directory {}", MarkdownInlineCode(&input.path)).into()
+        } else {
+            "Create directory".into()
+        }
+    }
+
+    fn run(
+        self: Arc<Self>,
+        input: Self::Input,
+        _event_stream: ToolCallEventStream,
+        cx: &mut App,
+    ) -> Task<Result<Self::Output>> {
+        let project_path = match self.project.read(cx).find_project_path(&input.path, cx) {
+            Some(project_path) => project_path,
+            None => {
+                return Task::ready(Err(anyhow!("Path to create was outside the project")));
+            }
+        };
+        let destination_path: Arc<str> = input.path.as_str().into();
+
+        let create_entry = self.project.update(cx, |project, cx| {
+            project.create_entry(project_path.clone(), true, cx)
+        });
+
+        cx.spawn(async move |_cx| {
+            create_entry
+                .await
+                .with_context(|| format!("Creating directory {destination_path}"))?;
+
+            Ok(format!("Created directory {destination_path}"))
+        })
+    }
+}

crates/agent2/src/tools/delete_path_tool.rs 🔗

@@ -0,0 +1,136 @@
+use crate::{AgentTool, ToolCallEventStream};
+use action_log::ActionLog;
+use agent_client_protocol::ToolKind;
+use anyhow::{Context as _, Result, anyhow};
+use futures::{SinkExt, StreamExt, channel::mpsc};
+use gpui::{App, AppContext, Entity, SharedString, Task};
+use project::{Project, ProjectPath};
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+use std::sync::Arc;
+
+/// Deletes the file or directory (and the directory's contents, recursively) at the specified path in the project, and returns confirmation of the deletion.
+#[derive(Debug, Serialize, Deserialize, JsonSchema)]
+pub struct DeletePathToolInput {
+    /// The path of the file or directory to delete.
+    ///
+    /// <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 {
+    project: Entity<Project>,
+    action_log: Entity<ActionLog>,
+}
+
+impl DeletePathTool {
+    pub fn new(project: Entity<Project>, action_log: Entity<ActionLog>) -> Self {
+        Self {
+            project,
+            action_log,
+        }
+    }
+}
+
+impl AgentTool for DeletePathTool {
+    type Input = DeletePathToolInput;
+    type Output = String;
+
+    fn name(&self) -> SharedString {
+        "delete_path".into()
+    }
+
+    fn kind(&self) -> ToolKind {
+        ToolKind::Delete
+    }
+
+    fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
+        if let Ok(input) = input {
+            format!("Delete “`{}`”", input.path).into()
+        } else {
+            "Delete path".into()
+        }
+    }
+
+    fn run(
+        self: Arc<Self>,
+        input: Self::Input,
+        _event_stream: ToolCallEventStream,
+        cx: &mut App,
+    ) -> Task<Result<Self::Output>> {
+        let path = input.path;
+        let Some(project_path) = self.project.read(cx).find_project_path(&path, cx) else {
+            return Task::ready(Err(anyhow!(
+                "Couldn't delete {path} because that path isn't in this project."
+            )));
+        };
+
+        let Some(worktree) = self
+            .project
+            .read(cx)
+            .worktree_for_id(project_path.worktree_id, cx)
+        else {
+            return Task::ready(Err(anyhow!(
+                "Couldn't delete {path} because that path isn't in this project."
+            )));
+        };
+
+        let worktree_snapshot = worktree.read(cx).snapshot();
+        let (mut paths_tx, mut paths_rx) = mpsc::channel(256);
+        cx.background_spawn({
+            let project_path = project_path.clone();
+            async move {
+                for entry in
+                    worktree_snapshot.traverse_from_path(true, false, false, &project_path.path)
+                {
+                    if !entry.path.starts_with(&project_path.path) {
+                        break;
+                    }
+                    paths_tx
+                        .send(ProjectPath {
+                            worktree_id: project_path.worktree_id,
+                            path: entry.path.clone(),
+                        })
+                        .await?;
+                }
+                anyhow::Ok(())
+            }
+        })
+        .detach();
+
+        let project = self.project.clone();
+        let action_log = self.action_log.clone();
+        cx.spawn(async move |cx| {
+            while let Some(path) = paths_rx.next().await {
+                if let Ok(buffer) = project
+                    .update(cx, |project, cx| project.open_buffer(path, cx))?
+                    .await
+                {
+                    action_log.update(cx, |action_log, cx| {
+                        action_log.will_delete_buffer(buffer.clone(), cx)
+                    })?;
+                }
+            }
+
+            let deletion_task = project
+                .update(cx, |project, cx| {
+                    project.delete_file(project_path, false, cx)
+                })?
+                .with_context(|| {
+                    format!("Couldn't delete {path} because that path isn't in this project.")
+                })?;
+            deletion_task
+                .await
+                .with_context(|| format!("Deleting {path}"))?;
+            Ok(format!("Deleted {path}"))
+        })
+    }
+}

crates/agent2/src/tools/diagnostics_tool.rs 🔗

@@ -0,0 +1,163 @@
+use crate::{AgentTool, ToolCallEventStream};
+use agent_client_protocol as acp;
+use anyhow::{Result, anyhow};
+use gpui::{App, Entity, Task};
+use language::{DiagnosticSeverity, OffsetRangeExt};
+use project::Project;
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+use std::{fmt::Write, path::Path, sync::Arc};
+use ui::SharedString;
+use util::markdown::MarkdownInlineCode;
+
+/// Get errors and warnings for the project or a specific file.
+///
+/// This tool can be invoked after a series of edits to determine if further edits are necessary, or if the user asks to fix errors or warnings in their codebase.
+///
+/// When a path is provided, shows all diagnostics for that specific file.
+/// When no path is provided, shows a summary of error and warning counts for all files in the project.
+///
+/// <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>
+#[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>
+    pub path: Option<String>,
+}
+
+pub struct DiagnosticsTool {
+    project: Entity<Project>,
+}
+
+impl DiagnosticsTool {
+    pub fn new(project: Entity<Project>) -> Self {
+        Self { project }
+    }
+}
+
+impl AgentTool for DiagnosticsTool {
+    type Input = DiagnosticsToolInput;
+    type Output = String;
+
+    fn name(&self) -> SharedString {
+        "diagnostics".into()
+    }
+
+    fn kind(&self) -> acp::ToolKind {
+        acp::ToolKind::Read
+    }
+
+    fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
+        if let Some(path) = input.ok().and_then(|input| match input.path {
+            Some(path) if !path.is_empty() => Some(path),
+            _ => None,
+        }) {
+            format!("Check diagnostics for {}", MarkdownInlineCode(&path)).into()
+        } else {
+            "Check project diagnostics".into()
+        }
+    }
+
+    fn run(
+        self: Arc<Self>,
+        input: Self::Input,
+        _event_stream: ToolCallEventStream,
+        cx: &mut App,
+    ) -> Task<Result<Self::Output>> {
+        match input.path {
+            Some(path) if !path.is_empty() => {
+                let Some(project_path) = self.project.read(cx).find_project_path(&path, cx) else {
+                    return Task::ready(Err(anyhow!("Could not find path {path} in project",)));
+                };
+
+                let buffer = self
+                    .project
+                    .update(cx, |project, cx| project.open_buffer(project_path, cx));
+
+                cx.spawn(async move |cx| {
+                    let mut output = String::new();
+                    let buffer = buffer.await?;
+                    let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
+
+                    for (_, group) in snapshot.diagnostic_groups(None) {
+                        let entry = &group.entries[group.primary_ix];
+                        let range = entry.range.to_point(&snapshot);
+                        let severity = match entry.diagnostic.severity {
+                            DiagnosticSeverity::ERROR => "error",
+                            DiagnosticSeverity::WARNING => "warning",
+                            _ => continue,
+                        };
+
+                        writeln!(
+                            output,
+                            "{} at line {}: {}",
+                            severity,
+                            range.start.row + 1,
+                            entry.diagnostic.message
+                        )?;
+                    }
+
+                    if output.is_empty() {
+                        Ok("File doesn't have errors or warnings!".to_string())
+                    } else {
+                        Ok(output)
+                    }
+                })
+            }
+            _ => {
+                let project = self.project.read(cx);
+                let mut output = String::new();
+                let mut has_diagnostics = false;
+
+                for (project_path, _, summary) in project.diagnostic_summaries(true, cx) {
+                    if summary.error_count > 0 || summary.warning_count > 0 {
+                        let Some(worktree) = project.worktree_for_id(project_path.worktree_id, cx)
+                        else {
+                            continue;
+                        };
+
+                        has_diagnostics = true;
+                        output.push_str(&format!(
+                            "{}: {} error(s), {} warning(s)\n",
+                            Path::new(worktree.read(cx).root_name())
+                                .join(project_path.path)
+                                .display(),
+                            summary.error_count,
+                            summary.warning_count
+                        ));
+                    }
+                }
+
+                if has_diagnostics {
+                    Task::ready(Ok(output))
+                } else {
+                    Task::ready(Ok("No errors or warnings found in the project.".into()))
+                }
+            }
+        }
+    }
+}

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

@@ -0,0 +1,1575 @@
+use crate::{AgentTool, Thread, ToolCallEventStream};
+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};
+use indoc::formatdoc;
+use language::language_settings::{self, FormatOnSave};
+use language::{LanguageRegistry, ToPoint};
+use language_model::LanguageModelToolResultContent;
+use paths;
+use project::lsp_store::{FormatTrigger, LspFormatTarget};
+use project::{Project, ProjectPath};
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+use settings::Settings;
+use smol::stream::StreamExt as _;
+use std::path::{Path, PathBuf};
+use std::sync::Arc;
+use ui::SharedString;
+use util::ResultExt;
+
+const DEFAULT_UI_TEXT: &str = "Editing file";
+
+/// This is a tool for creating a new file or editing an existing file. For moving or renaming files, you should generally use the `terminal` tool with the 'mv' command instead.
+///
+/// Before using this tool:
+///
+/// 1. Use the `read_file` tool to understand the file's contents and context
+///
+/// 2. Verify the directory path is correct (only applicable when creating new files):
+///    - Use the `list_directory` tool to verify the parent directory exists and is the correct location
+#[derive(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(Debug, Serialize, Deserialize, JsonSchema)]
+struct EditFileToolPartialInput {
+    #[serde(default)]
+    path: String,
+    #[serde(default)]
+    display_description: String,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "lowercase")]
+pub enum EditFileMode {
+    Edit,
+    Create,
+    Overwrite,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct EditFileToolOutput {
+    #[serde(alias = "original_path")]
+    input_path: PathBuf,
+    new_text: String,
+    old_text: Arc<String>,
+    #[serde(default)]
+    diff: String,
+    #[serde(alias = "raw_output")]
+    edit_agent_output: EditAgentOutput,
+}
+
+impl From<EditFileToolOutput> for LanguageModelToolResultContent {
+    fn from(output: EditFileToolOutput) -> Self {
+        if output.diff.is_empty() {
+            "No edits were made.".into()
+        } else {
+            format!(
+                "Edited {}:\n\n```diff\n{}\n```",
+                output.input_path.display(),
+                output.diff
+            )
+            .into()
+        }
+    }
+}
+
+pub struct EditFileTool {
+    thread: WeakEntity<Thread>,
+    language_registry: Arc<LanguageRegistry>,
+}
+
+impl EditFileTool {
+    pub fn new(thread: WeakEntity<Thread>, language_registry: Arc<LanguageRegistry>) -> Self {
+        Self {
+            thread,
+            language_registry,
+        }
+    }
+
+    fn authorize(
+        &self,
+        input: &EditFileToolInput,
+        event_stream: &ToolCallEventStream,
+        cx: &mut App,
+    ) -> Task<Result<()>> {
+        if agent_settings::AgentSettings::get_global(cx).always_allow_tool_actions {
+            return Task::ready(Ok(()));
+        }
+
+        // If any path component matches the local settings folder, then this could affect
+        // the editor in ways beyond the project source, so prompt.
+        let local_settings_folder = paths::local_settings_folder_relative_path();
+        let path = Path::new(&input.path);
+        if path
+            .components()
+            .any(|component| component.as_os_str() == local_settings_folder.as_os_str())
+        {
+            return event_stream.authorize(
+                format!("{} (local settings)", input.display_description),
+                cx,
+            );
+        }
+
+        // It's also possible that the global config dir is configured to be inside the project,
+        // so check for that edge case too.
+        if let Ok(canonical_path) = std::fs::canonicalize(&input.path)
+            && canonical_path.starts_with(paths::config_dir())
+        {
+            return event_stream.authorize(
+                format!("{} (global settings)", input.display_description),
+                cx,
+            );
+        }
+
+        // Check if path is inside the global config directory
+        // First check if it's already inside project - if not, try to canonicalize
+        let Ok(project_path) = self.thread.read_with(cx, |thread, cx| {
+            thread.project().read(cx).find_project_path(&input.path, cx)
+        }) else {
+            return Task::ready(Err(anyhow!("thread was dropped")));
+        };
+
+        // If the path is inside the project, and it's not one of the above edge cases,
+        // then no confirmation is necessary. Otherwise, confirmation is necessary.
+        if project_path.is_some() {
+            Task::ready(Ok(()))
+        } else {
+            event_stream.authorize(&input.display_description, cx)
+        }
+    }
+}
+
+impl AgentTool for EditFileTool {
+    type Input = EditFileToolInput;
+    type Output = EditFileToolOutput;
+
+    fn name(&self) -> SharedString {
+        "edit_file".into()
+    }
+
+    fn kind(&self) -> acp::ToolKind {
+        acp::ToolKind::Edit
+    }
+
+    fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
+        match input {
+            Ok(input) => input.display_description.into(),
+            Err(raw_input) => {
+                if let Some(input) =
+                    serde_json::from_value::<EditFileToolPartialInput>(raw_input).ok()
+                {
+                    let description = input.display_description.trim();
+                    if !description.is_empty() {
+                        return description.to_string().into();
+                    }
+
+                    let path = input.path.trim().to_string();
+                    if !path.is_empty() {
+                        return path.into();
+                    }
+                }
+
+                DEFAULT_UI_TEXT.into()
+            }
+        }
+    }
+
+    fn run(
+        self: Arc<Self>,
+        input: Self::Input,
+        event_stream: ToolCallEventStream,
+        cx: &mut App,
+    ) -> Task<Result<Self::Output>> {
+        let Ok(project) = self
+            .thread
+            .read_with(cx, |thread, _cx| thread.project().clone())
+        else {
+            return Task::ready(Err(anyhow!("thread was dropped")));
+        };
+        let project_path = match resolve_path(&input, project.clone(), cx) {
+            Ok(path) => path,
+            Err(err) => return Task::ready(Err(anyhow!(err))),
+        };
+        let abs_path = project.read(cx).absolute_path(&project_path, cx);
+        if let Some(abs_path) = abs_path.clone() {
+            event_stream.update_fields(ToolCallUpdateFields {
+                locations: Some(vec![acp::ToolCallLocation {
+                    path: abs_path,
+                    line: None,
+                }]),
+                ..Default::default()
+            });
+        }
+
+        let authorize = self.authorize(&input, &event_stream, cx);
+        cx.spawn(async move |cx: &mut AsyncApp| {
+            authorize.await?;
+
+            let (request, model, action_log) = self.thread.update(cx, |thread, cx| {
+                let request = thread.build_completion_request(CompletionIntent::ToolResults, cx);
+                (request, thread.model().cloned(), thread.action_log().clone())
+            })?;
+            let request = request?;
+            let model = model.context("No language model configured")?;
+
+            let edit_format = EditFormat::from_model(model.clone())?;
+            let edit_agent = EditAgent::new(
+                model,
+                project.clone(),
+                action_log.clone(),
+                // TODO: move edit agent to this crate so we can use our templates
+                assistant_tools::templates::Templates::new(),
+                edit_format,
+            );
+
+            let buffer = project
+                .update(cx, |project, cx| {
+                    project.open_buffer(project_path.clone(), cx)
+                })?
+                .await?;
+
+            let diff = cx.new(|cx| Diff::new(buffer.clone(), cx))?;
+            event_stream.update_diff(diff.clone());
+
+            let old_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
+            let old_text = cx
+                .background_spawn({
+                    let old_snapshot = old_snapshot.clone();
+                    async move { Arc::new(old_snapshot.text()) }
+                })
+                .await;
+
+
+            let (output, mut events) = if matches!(input.mode, EditFileMode::Edit) {
+                edit_agent.edit(
+                    buffer.clone(),
+                    input.display_description.clone(),
+                    &request,
+                    cx,
+                )
+            } else {
+                edit_agent.overwrite(
+                    buffer.clone(),
+                    input.display_description.clone(),
+                    &request,
+                    cx,
+                )
+            };
+
+            let mut hallucinated_old_text = false;
+            let mut ambiguous_ranges = Vec::new();
+            let mut emitted_location = false;
+            while let Some(event) = events.next().await {
+                match event {
+                    EditAgentOutputEvent::Edited(range) => {
+                        if !emitted_location {
+                            let line = buffer.update(cx, |buffer, _cx| {
+                                range.start.to_point(&buffer.snapshot()).row
+                            }).ok();
+                            if let Some(abs_path) = abs_path.clone() {
+                                event_stream.update_fields(ToolCallUpdateFields {
+                                    locations: Some(vec![ToolCallLocation { path: abs_path, line }]),
+                                    ..Default::default()
+                                });
+                            }
+                            emitted_location = true;
+                        }
+                    },
+                    EditAgentOutputEvent::UnresolvedEditRange => hallucinated_old_text = true,
+                    EditAgentOutputEvent::AmbiguousEditRange(ranges) => ambiguous_ranges = ranges,
+                    EditAgentOutputEvent::ResolvingEditRange(range) => {
+                        diff.update(cx, |card, cx| card.reveal_range(range.clone(), cx))?;
+                        // if !emitted_location {
+                        //     let line = buffer.update(cx, |buffer, _cx| {
+                        //         range.start.to_point(&buffer.snapshot()).row
+                        //     }).ok();
+                        //     if let Some(abs_path) = abs_path.clone() {
+                        //         event_stream.update_fields(ToolCallUpdateFields {
+                        //             locations: Some(vec![ToolCallLocation { path: abs_path, line }]),
+                        //             ..Default::default()
+                        //         });
+                        //     }
+                        // }
+                    }
+                }
+            }
+
+            // If format_on_save is enabled, format the buffer
+            let format_on_save_enabled = buffer
+                .read_with(cx, |buffer, cx| {
+                    let settings = language_settings::language_settings(
+                        buffer.language().map(|l| l.name()),
+                        buffer.file(),
+                        cx,
+                    );
+                    settings.format_on_save != FormatOnSave::Off
+                })
+                .unwrap_or(false);
+
+            let edit_agent_output = output.await?;
+
+            if format_on_save_enabled {
+                action_log.update(cx, |log, cx| {
+                    log.buffer_edited(buffer.clone(), cx);
+                })?;
+
+                let format_task = project.update(cx, |project, cx| {
+                    project.format(
+                        HashSet::from_iter([buffer.clone()]),
+                        LspFormatTarget::Buffers,
+                        false, // Don't push to history since the tool did it.
+                        FormatTrigger::Save,
+                        cx,
+                    )
+                })?;
+                format_task.await.log_err();
+            }
+
+            project
+                .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))?
+                .await?;
+
+            action_log.update(cx, |log, cx| {
+                log.buffer_edited(buffer.clone(), cx);
+            })?;
+
+            let new_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
+            let (new_text, unified_diff) = cx
+                .background_spawn({
+                    let new_snapshot = new_snapshot.clone();
+                    let old_text = old_text.clone();
+                    async move {
+                        let new_text = new_snapshot.text();
+                        let diff = language::unified_diff(&old_text, &new_text);
+                        (new_text, diff)
+                    }
+                })
+                .await;
+
+            diff.update(cx, |diff, cx| diff.finalize(cx)).ok();
+
+            let input_path = input.path.display();
+            if unified_diff.is_empty() {
+                anyhow::ensure!(
+                    !hallucinated_old_text,
+                    formatdoc! {"
+                        Some edits were produced but none of them could be applied.
+                        Read the relevant sections of {input_path} again so that
+                        I can perform the requested edits.
+                    "}
+                );
+                anyhow::ensure!(
+                    ambiguous_ranges.is_empty(),
+                    {
+                        let line_numbers = ambiguous_ranges
+                            .iter()
+                            .map(|range| range.start.to_string())
+                            .collect::<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(EditFileToolOutput {
+                input_path: input.path,
+                new_text,
+                old_text,
+                diff: unified_diff,
+                edit_agent_output,
+            })
+        })
+    }
+
+    fn replay(
+        &self,
+        _input: Self::Input,
+        output: Self::Output,
+        event_stream: ToolCallEventStream,
+        cx: &mut App,
+    ) -> Result<()> {
+        event_stream.update_diff(cx.new(|cx| {
+            Diff::finalized(
+                output.input_path,
+                Some(output.old_text.to_string()),
+                output.new_text,
+                self.language_registry.clone(),
+                cx,
+            )
+        }));
+        Ok(())
+    }
+}
+
+/// Validate that the file path is valid, meaning:
+///
+/// - For `edit` and `overwrite`, the path must point to an existing file.
+/// - For `create`, the file must not already exist, but it's parent dir must exist.
+fn resolve_path(
+    input: &EditFileToolInput,
+    project: Entity<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()
+                .context("Can't create file: invalid filename")?;
+
+            let new_file_path = parent_project_path.map(|parent| ProjectPath {
+                path: Arc::from(parent.path.join(file_name)),
+                ..parent
+            });
+
+            new_file_path.context("Can't create file")
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::{ContextServerRegistry, Templates};
+    use action_log::ActionLog;
+    use client::TelemetrySettings;
+    use fs::Fs;
+    use gpui::{TestAppContext, UpdateGlobal};
+    use language_model::fake_provider::FakeLanguageModel;
+    use prompt_store::ProjectContext;
+    use serde_json::json;
+    use settings::SettingsStore;
+    use util::path;
+
+    #[gpui::test]
+    async fn test_edit_nonexistent_file(cx: &mut TestAppContext) {
+        init_test(cx);
+
+        let fs = project::FakeFs::new(cx.executor());
+        fs.insert_tree("/root", json!({})).await;
+        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
+        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
+        let action_log = cx.new(|_| ActionLog::new(project.clone()));
+        let context_server_registry =
+            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
+        let model = Arc::new(FakeLanguageModel::default());
+        let thread = cx.new(|cx| {
+            Thread::new(
+                project,
+                cx.new(|_cx| ProjectContext::default()),
+                context_server_registry,
+                action_log,
+                Templates::new(),
+                Some(model),
+                cx,
+            )
+        });
+        let result = cx
+            .update(|cx| {
+                let input = EditFileToolInput {
+                    display_description: "Some edit".into(),
+                    path: "root/nonexistent_file.txt".into(),
+                    mode: EditFileMode::Edit,
+                };
+                Arc::new(EditFileTool::new(thread.downgrade(), language_registry)).run(
+                    input,
+                    ToolCallEventStream::test().0,
+                    cx,
+                )
+            })
+            .await;
+        assert_eq!(
+            result.unwrap_err().to_string(),
+            "Can't edit file: path not found"
+        );
+    }
+
+    #[gpui::test]
+    async fn test_resolve_path_for_creating_file(cx: &mut TestAppContext) {
+        let mode = &EditFileMode::Create;
+
+        let result = test_resolve_path(mode, "root/new.txt", cx);
+        assert_resolved_path_eq(result.await, "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))
+    }
+
+    fn assert_resolved_path_eq(path: anyhow::Result<ProjectPath>, expected: &str) {
+        let actual = path
+            .expect("Should return valid path")
+            .path
+            .to_str()
+            .unwrap()
+            .replace("\\", "/"); // Naive Windows paths normalization
+        assert_eq!(actual, expected);
+    }
+
+    #[gpui::test]
+    async fn test_format_on_save(cx: &mut TestAppContext) {
+        init_test(cx);
+
+        let fs = project::FakeFs::new(cx.executor());
+        fs.insert_tree("/root", json!({"src": {}})).await;
+
+        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
+
+        // Set up a Rust language with LSP formatting support
+        let rust_language = Arc::new(language::Language::new(
+            language::LanguageConfig {
+                name: "Rust".into(),
+                matcher: language::LanguageMatcher {
+                    path_suffixes: vec!["rs".to_string()],
+                    ..Default::default()
+                },
+                ..Default::default()
+            },
+            None,
+        ));
+
+        // Register the language and fake LSP
+        let language_registry = project.read_with(cx, |project, _| project.languages().clone());
+        language_registry.add(rust_language);
+
+        let mut fake_language_servers = language_registry.register_fake_lsp(
+            "Rust",
+            language::FakeLspAdapter {
+                capabilities: lsp::ServerCapabilities {
+                    document_formatting_provider: Some(lsp::OneOf::Left(true)),
+                    ..Default::default()
+                },
+                ..Default::default()
+            },
+        );
+
+        // Create the file
+        fs.save(
+            path!("/root/src/main.rs").as_ref(),
+            &"initial content".into(),
+            language::LineEnding::Unix,
+        )
+        .await
+        .unwrap();
+
+        // Open the buffer to trigger LSP initialization
+        let buffer = project
+            .update(cx, |project, cx| {
+                project.open_local_buffer(path!("/root/src/main.rs"), cx)
+            })
+            .await
+            .unwrap();
+
+        // Register the buffer with language servers
+        let _handle = project.update(cx, |project, cx| {
+            project.register_buffer_with_language_servers(&buffer, cx)
+        });
+
+        const UNFORMATTED_CONTENT: &str = "fn main() {println!(\"Hello!\");}\n";
+        const FORMATTED_CONTENT: &str =
+            "This file was formatted by the fake formatter in the test.\n";
+
+        // Get the fake language server and set up formatting handler
+        let fake_language_server = fake_language_servers.next().await.unwrap();
+        fake_language_server.set_request_handler::<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 context_server_registry =
+            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
+        let model = Arc::new(FakeLanguageModel::default());
+        let thread = cx.new(|cx| {
+            Thread::new(
+                project,
+                cx.new(|_cx| ProjectContext::default()),
+                context_server_registry,
+                action_log.clone(),
+                Templates::new(),
+                Some(model.clone()),
+                cx,
+            )
+        });
+
+        // First, test with format_on_save enabled
+        cx.update(|cx| {
+            SettingsStore::update_global(cx, |store, cx| {
+                store.update_user_settings::<language::language_settings::AllLanguageSettings>(
+                    cx,
+                    |settings| {
+                        settings.defaults.format_on_save = Some(FormatOnSave::On);
+                        settings.defaults.formatter =
+                            Some(language::language_settings::SelectedFormatter::Auto);
+                    },
+                );
+            });
+        });
+
+        // Have the model stream unformatted content
+        let edit_result = {
+            let edit_task = cx.update(|cx| {
+                let input = EditFileToolInput {
+                    display_description: "Create main function".into(),
+                    path: "root/src/main.rs".into(),
+                    mode: EditFileMode::Overwrite,
+                };
+                Arc::new(EditFileTool::new(
+                    thread.downgrade(),
+                    language_registry.clone(),
+                ))
+                .run(input, ToolCallEventStream::test().0, cx)
+            });
+
+            // Stream the unformatted content
+            cx.executor().run_until_parked();
+            model.send_last_completion_stream_text_chunk(UNFORMATTED_CONTENT.to_string());
+            model.end_last_completion_stream();
+
+            edit_task.await
+        };
+        assert!(edit_result.is_ok());
+
+        // Wait for any async operations (e.g. formatting) to complete
+        cx.executor().run_until_parked();
+
+        // Read the file to verify it was formatted automatically
+        let new_content = fs.load(path!("/root/src/main.rs").as_ref()).await.unwrap();
+        assert_eq!(
+            // Ignore carriage returns on Windows
+            new_content.replace("\r\n", "\n"),
+            FORMATTED_CONTENT,
+            "Code should be formatted when format_on_save is enabled"
+        );
+
+        let stale_buffer_count = 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::<language::language_settings::AllLanguageSettings>(
+                    cx,
+                    |settings| {
+                        settings.defaults.format_on_save = Some(FormatOnSave::Off);
+                    },
+                );
+            });
+        });
+
+        // Stream unformatted edits again
+        let edit_result = {
+            let edit_task = cx.update(|cx| {
+                let input = EditFileToolInput {
+                    display_description: "Update main function".into(),
+                    path: "root/src/main.rs".into(),
+                    mode: EditFileMode::Overwrite,
+                };
+                Arc::new(EditFileTool::new(thread.downgrade(), language_registry)).run(
+                    input,
+                    ToolCallEventStream::test().0,
+                    cx,
+                )
+            });
+
+            // Stream the unformatted content
+            cx.executor().run_until_parked();
+            model.send_last_completion_stream_text_chunk(UNFORMATTED_CONTENT.to_string());
+            model.end_last_completion_stream();
+
+            edit_task.await
+        };
+        assert!(edit_result.is_ok());
+
+        // Wait for any async operations (e.g. formatting) to complete
+        cx.executor().run_until_parked();
+
+        // Verify the file was not formatted
+        let new_content = fs.load(path!("/root/src/main.rs").as_ref()).await.unwrap();
+        assert_eq!(
+            // Ignore carriage returns on Windows
+            new_content.replace("\r\n", "\n"),
+            UNFORMATTED_CONTENT,
+            "Code should not be formatted when format_on_save is disabled"
+        );
+    }
+
+    #[gpui::test]
+    async fn test_remove_trailing_whitespace(cx: &mut TestAppContext) {
+        init_test(cx);
+
+        let fs = project::FakeFs::new(cx.executor());
+        fs.insert_tree("/root", json!({"src": {}})).await;
+
+        // Create a simple file with trailing whitespace
+        fs.save(
+            path!("/root/src/main.rs").as_ref(),
+            &"initial content".into(),
+            language::LineEnding::Unix,
+        )
+        .await
+        .unwrap();
+
+        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
+        let context_server_registry =
+            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
+        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
+        let action_log = cx.new(|_| ActionLog::new(project.clone()));
+        let model = Arc::new(FakeLanguageModel::default());
+        let thread = cx.new(|cx| {
+            Thread::new(
+                project,
+                cx.new(|_cx| ProjectContext::default()),
+                context_server_registry,
+                action_log.clone(),
+                Templates::new(),
+                Some(model.clone()),
+                cx,
+            )
+        });
+
+        // First, test with remove_trailing_whitespace_on_save enabled
+        cx.update(|cx| {
+            SettingsStore::update_global(cx, |store, cx| {
+                store.update_user_settings::<language::language_settings::AllLanguageSettings>(
+                    cx,
+                    |settings| {
+                        settings.defaults.remove_trailing_whitespace_on_save = Some(true);
+                    },
+                );
+            });
+        });
+
+        const CONTENT_WITH_TRAILING_WHITESPACE: &str =
+            "fn main() {  \n    println!(\"Hello!\");  \n}\n";
+
+        // Have the model stream content that contains trailing whitespace
+        let edit_result = {
+            let edit_task = cx.update(|cx| {
+                let input = EditFileToolInput {
+                    display_description: "Create main function".into(),
+                    path: "root/src/main.rs".into(),
+                    mode: EditFileMode::Overwrite,
+                };
+                Arc::new(EditFileTool::new(
+                    thread.downgrade(),
+                    language_registry.clone(),
+                ))
+                .run(input, ToolCallEventStream::test().0, cx)
+            });
+
+            // Stream the content with trailing whitespace
+            cx.executor().run_until_parked();
+            model.send_last_completion_stream_text_chunk(
+                CONTENT_WITH_TRAILING_WHITESPACE.to_string(),
+            );
+            model.end_last_completion_stream();
+
+            edit_task.await
+        };
+        assert!(edit_result.is_ok());
+
+        // Wait for any async operations (e.g. formatting) to complete
+        cx.executor().run_until_parked();
+
+        // Read the file to verify trailing whitespace was removed automatically
+        assert_eq!(
+            // Ignore carriage returns on Windows
+            fs.load(path!("/root/src/main.rs").as_ref())
+                .await
+                .unwrap()
+                .replace("\r\n", "\n"),
+            "fn main() {\n    println!(\"Hello!\");\n}\n",
+            "Trailing whitespace should be removed when remove_trailing_whitespace_on_save is enabled"
+        );
+
+        // Next, test with remove_trailing_whitespace_on_save disabled
+        cx.update(|cx| {
+            SettingsStore::update_global(cx, |store, cx| {
+                store.update_user_settings::<language::language_settings::AllLanguageSettings>(
+                    cx,
+                    |settings| {
+                        settings.defaults.remove_trailing_whitespace_on_save = Some(false);
+                    },
+                );
+            });
+        });
+
+        // Stream edits again with trailing whitespace
+        let edit_result = {
+            let edit_task = cx.update(|cx| {
+                let input = EditFileToolInput {
+                    display_description: "Update main function".into(),
+                    path: "root/src/main.rs".into(),
+                    mode: EditFileMode::Overwrite,
+                };
+                Arc::new(EditFileTool::new(thread.downgrade(), language_registry)).run(
+                    input,
+                    ToolCallEventStream::test().0,
+                    cx,
+                )
+            });
+
+            // Stream the content with trailing whitespace
+            cx.executor().run_until_parked();
+            model.send_last_completion_stream_text_chunk(
+                CONTENT_WITH_TRAILING_WHITESPACE.to_string(),
+            );
+            model.end_last_completion_stream();
+
+            edit_task.await
+        };
+        assert!(edit_result.is_ok());
+
+        // Wait for any async operations (e.g. formatting) to complete
+        cx.executor().run_until_parked();
+
+        // Verify the file still has trailing whitespace
+        // Read the file again - it should still have trailing whitespace
+        let final_content = fs.load(path!("/root/src/main.rs").as_ref()).await.unwrap();
+        assert_eq!(
+            // Ignore carriage returns on Windows
+            final_content.replace("\r\n", "\n"),
+            CONTENT_WITH_TRAILING_WHITESPACE,
+            "Trailing whitespace should remain when remove_trailing_whitespace_on_save is disabled"
+        );
+    }
+
+    #[gpui::test]
+    async fn test_authorize(cx: &mut TestAppContext) {
+        init_test(cx);
+        let fs = project::FakeFs::new(cx.executor());
+        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
+        let context_server_registry =
+            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
+        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
+        let action_log = cx.new(|_| ActionLog::new(project.clone()));
+        let model = Arc::new(FakeLanguageModel::default());
+        let thread = cx.new(|cx| {
+            Thread::new(
+                project,
+                cx.new(|_cx| ProjectContext::default()),
+                context_server_registry,
+                action_log.clone(),
+                Templates::new(),
+                Some(model.clone()),
+                cx,
+            )
+        });
+        let tool = Arc::new(EditFileTool::new(thread.downgrade(), language_registry));
+        fs.insert_tree("/root", json!({})).await;
+
+        // Test 1: Path with .zed component should require confirmation
+        let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
+        let _auth = cx.update(|cx| {
+            tool.authorize(
+                &EditFileToolInput {
+                    display_description: "test 1".into(),
+                    path: ".zed/settings.json".into(),
+                    mode: EditFileMode::Edit,
+                },
+                &stream_tx,
+                cx,
+            )
+        });
+
+        let event = stream_rx.expect_authorization().await;
+        assert_eq!(
+            event.tool_call.fields.title,
+            Some("test 1 (local settings)".into())
+        );
+
+        // Test 2: Path outside project should require confirmation
+        let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
+        let _auth = cx.update(|cx| {
+            tool.authorize(
+                &EditFileToolInput {
+                    display_description: "test 2".into(),
+                    path: "/etc/hosts".into(),
+                    mode: EditFileMode::Edit,
+                },
+                &stream_tx,
+                cx,
+            )
+        });
+
+        let event = stream_rx.expect_authorization().await;
+        assert_eq!(event.tool_call.fields.title, Some("test 2".into()));
+
+        // Test 3: Relative path without .zed should not require confirmation
+        let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
+        cx.update(|cx| {
+            tool.authorize(
+                &EditFileToolInput {
+                    display_description: "test 3".into(),
+                    path: "root/src/main.rs".into(),
+                    mode: EditFileMode::Edit,
+                },
+                &stream_tx,
+                cx,
+            )
+        })
+        .await
+        .unwrap();
+        assert!(stream_rx.try_next().is_err());
+
+        // Test 4: Path with .zed in the middle should require confirmation
+        let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
+        let _auth = cx.update(|cx| {
+            tool.authorize(
+                &EditFileToolInput {
+                    display_description: "test 4".into(),
+                    path: "root/.zed/tasks.json".into(),
+                    mode: EditFileMode::Edit,
+                },
+                &stream_tx,
+                cx,
+            )
+        });
+        let event = stream_rx.expect_authorization().await;
+        assert_eq!(
+            event.tool_call.fields.title,
+            Some("test 4 (local settings)".into())
+        );
+
+        // Test 5: When always_allow_tool_actions is enabled, no confirmation needed
+        cx.update(|cx| {
+            let mut settings = agent_settings::AgentSettings::get_global(cx).clone();
+            settings.always_allow_tool_actions = true;
+            agent_settings::AgentSettings::override_global(settings, cx);
+        });
+
+        let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
+        cx.update(|cx| {
+            tool.authorize(
+                &EditFileToolInput {
+                    display_description: "test 5.1".into(),
+                    path: ".zed/settings.json".into(),
+                    mode: EditFileMode::Edit,
+                },
+                &stream_tx,
+                cx,
+            )
+        })
+        .await
+        .unwrap();
+        assert!(stream_rx.try_next().is_err());
+
+        let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
+        cx.update(|cx| {
+            tool.authorize(
+                &EditFileToolInput {
+                    display_description: "test 5.2".into(),
+                    path: "/etc/hosts".into(),
+                    mode: EditFileMode::Edit,
+                },
+                &stream_tx,
+                cx,
+            )
+        })
+        .await
+        .unwrap();
+        assert!(stream_rx.try_next().is_err());
+    }
+
+    #[gpui::test]
+    async fn test_authorize_global_config(cx: &mut TestAppContext) {
+        init_test(cx);
+        let fs = project::FakeFs::new(cx.executor());
+        fs.insert_tree("/project", json!({})).await;
+        let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
+        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
+        let context_server_registry =
+            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
+        let action_log = cx.new(|_| ActionLog::new(project.clone()));
+        let model = Arc::new(FakeLanguageModel::default());
+        let thread = cx.new(|cx| {
+            Thread::new(
+                project,
+                cx.new(|_cx| ProjectContext::default()),
+                context_server_registry,
+                action_log.clone(),
+                Templates::new(),
+                Some(model.clone()),
+                cx,
+            )
+        });
+        let tool = Arc::new(EditFileTool::new(thread.downgrade(), language_registry));
+
+        // Test global config paths - these should require confirmation if they exist and are outside the project
+        let test_cases = vec![
+            (
+                "/etc/hosts",
+                true,
+                "System file should require confirmation",
+            ),
+            (
+                "/usr/local/bin/script",
+                true,
+                "System bin file should require confirmation",
+            ),
+            (
+                "project/normal_file.rs",
+                false,
+                "Normal project file should not require confirmation",
+            ),
+        ];
+
+        for (path, should_confirm, description) in test_cases {
+            let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
+            let auth = cx.update(|cx| {
+                tool.authorize(
+                    &EditFileToolInput {
+                        display_description: "Edit file".into(),
+                        path: path.into(),
+                        mode: EditFileMode::Edit,
+                    },
+                    &stream_tx,
+                    cx,
+                )
+            });
+
+            if should_confirm {
+                stream_rx.expect_authorization().await;
+            } else {
+                auth.await.unwrap();
+                assert!(
+                    stream_rx.try_next().is_err(),
+                    "Failed for case: {} - path: {} - expected no confirmation but got one",
+                    description,
+                    path
+                );
+            }
+        }
+    }
+
+    #[gpui::test]
+    async fn test_needs_confirmation_with_multiple_worktrees(cx: &mut TestAppContext) {
+        init_test(cx);
+        let fs = project::FakeFs::new(cx.executor());
+
+        // Create multiple worktree directories
+        fs.insert_tree(
+            "/workspace/frontend",
+            json!({
+                "src": {
+                    "main.js": "console.log('frontend');"
+                }
+            }),
+        )
+        .await;
+        fs.insert_tree(
+            "/workspace/backend",
+            json!({
+                "src": {
+                    "main.rs": "fn main() {}"
+                }
+            }),
+        )
+        .await;
+        fs.insert_tree(
+            "/workspace/shared",
+            json!({
+                ".zed": {
+                    "settings.json": "{}"
+                }
+            }),
+        )
+        .await;
+
+        // Create project with multiple worktrees
+        let project = Project::test(
+            fs.clone(),
+            [
+                path!("/workspace/frontend").as_ref(),
+                path!("/workspace/backend").as_ref(),
+                path!("/workspace/shared").as_ref(),
+            ],
+            cx,
+        )
+        .await;
+        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
+        let action_log = cx.new(|_| ActionLog::new(project.clone()));
+        let context_server_registry =
+            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
+        let model = Arc::new(FakeLanguageModel::default());
+        let thread = cx.new(|cx| {
+            Thread::new(
+                project.clone(),
+                cx.new(|_cx| ProjectContext::default()),
+                context_server_registry.clone(),
+                action_log.clone(),
+                Templates::new(),
+                Some(model.clone()),
+                cx,
+            )
+        });
+        let tool = Arc::new(EditFileTool::new(thread.downgrade(), language_registry));
+
+        // Test files in different worktrees
+        let test_cases = vec![
+            ("frontend/src/main.js", false, "File in first worktree"),
+            ("backend/src/main.rs", false, "File in second worktree"),
+            (
+                "shared/.zed/settings.json",
+                true,
+                ".zed file in third worktree",
+            ),
+            ("/etc/hosts", true, "Absolute path outside all worktrees"),
+            (
+                "../outside/file.txt",
+                true,
+                "Relative path outside worktrees",
+            ),
+        ];
+
+        for (path, should_confirm, description) in test_cases {
+            let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
+            let auth = cx.update(|cx| {
+                tool.authorize(
+                    &EditFileToolInput {
+                        display_description: "Edit file".into(),
+                        path: path.into(),
+                        mode: EditFileMode::Edit,
+                    },
+                    &stream_tx,
+                    cx,
+                )
+            });
+
+            if should_confirm {
+                stream_rx.expect_authorization().await;
+            } else {
+                auth.await.unwrap();
+                assert!(
+                    stream_rx.try_next().is_err(),
+                    "Failed for case: {} - path: {} - expected no confirmation but got one",
+                    description,
+                    path
+                );
+            }
+        }
+    }
+
+    #[gpui::test]
+    async fn test_needs_confirmation_edge_cases(cx: &mut TestAppContext) {
+        init_test(cx);
+        let fs = project::FakeFs::new(cx.executor());
+        fs.insert_tree(
+            "/project",
+            json!({
+                ".zed": {
+                    "settings.json": "{}"
+                },
+                "src": {
+                    ".zed": {
+                        "local.json": "{}"
+                    }
+                }
+            }),
+        )
+        .await;
+        let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
+        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
+        let action_log = cx.new(|_| ActionLog::new(project.clone()));
+        let context_server_registry =
+            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
+        let model = Arc::new(FakeLanguageModel::default());
+        let thread = cx.new(|cx| {
+            Thread::new(
+                project.clone(),
+                cx.new(|_cx| ProjectContext::default()),
+                context_server_registry.clone(),
+                action_log.clone(),
+                Templates::new(),
+                Some(model.clone()),
+                cx,
+            )
+        });
+        let tool = Arc::new(EditFileTool::new(thread.downgrade(), language_registry));
+
+        // Test edge cases
+        let test_cases = vec![
+            // Empty path - find_project_path returns Some for empty paths
+            ("", false, "Empty path is treated as project root"),
+            // Root directory
+            ("/", true, "Root directory should be outside project"),
+            // Parent directory references - find_project_path resolves these
+            (
+                "project/../other",
+                false,
+                "Path with .. is resolved by find_project_path",
+            ),
+            (
+                "project/./src/file.rs",
+                false,
+                "Path with . should work normally",
+            ),
+            // Windows-style paths (if on Windows)
+            #[cfg(target_os = "windows")]
+            ("C:\\Windows\\System32\\hosts", true, "Windows system path"),
+            #[cfg(target_os = "windows")]
+            ("project\\src\\main.rs", false, "Windows-style project path"),
+        ];
+
+        for (path, should_confirm, description) in test_cases {
+            let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
+            let auth = cx.update(|cx| {
+                tool.authorize(
+                    &EditFileToolInput {
+                        display_description: "Edit file".into(),
+                        path: path.into(),
+                        mode: EditFileMode::Edit,
+                    },
+                    &stream_tx,
+                    cx,
+                )
+            });
+
+            if should_confirm {
+                stream_rx.expect_authorization().await;
+            } else {
+                auth.await.unwrap();
+                assert!(
+                    stream_rx.try_next().is_err(),
+                    "Failed for case: {} - path: {} - expected no confirmation but got one",
+                    description,
+                    path
+                );
+            }
+        }
+    }
+
+    #[gpui::test]
+    async fn test_needs_confirmation_with_different_modes(cx: &mut TestAppContext) {
+        init_test(cx);
+        let fs = project::FakeFs::new(cx.executor());
+        fs.insert_tree(
+            "/project",
+            json!({
+                "existing.txt": "content",
+                ".zed": {
+                    "settings.json": "{}"
+                }
+            }),
+        )
+        .await;
+        let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
+        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
+        let action_log = cx.new(|_| ActionLog::new(project.clone()));
+        let context_server_registry =
+            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
+        let model = Arc::new(FakeLanguageModel::default());
+        let thread = cx.new(|cx| {
+            Thread::new(
+                project.clone(),
+                cx.new(|_cx| ProjectContext::default()),
+                context_server_registry.clone(),
+                action_log.clone(),
+                Templates::new(),
+                Some(model.clone()),
+                cx,
+            )
+        });
+        let tool = Arc::new(EditFileTool::new(thread.downgrade(), language_registry));
+
+        // Test different EditFileMode values
+        let modes = vec![
+            EditFileMode::Edit,
+            EditFileMode::Create,
+            EditFileMode::Overwrite,
+        ];
+
+        for mode in modes {
+            // Test .zed path with different modes
+            let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
+            let _auth = cx.update(|cx| {
+                tool.authorize(
+                    &EditFileToolInput {
+                        display_description: "Edit settings".into(),
+                        path: "project/.zed/settings.json".into(),
+                        mode: mode.clone(),
+                    },
+                    &stream_tx,
+                    cx,
+                )
+            });
+
+            stream_rx.expect_authorization().await;
+
+            // Test outside path with different modes
+            let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
+            let _auth = cx.update(|cx| {
+                tool.authorize(
+                    &EditFileToolInput {
+                        display_description: "Edit file".into(),
+                        path: "/outside/file.txt".into(),
+                        mode: mode.clone(),
+                    },
+                    &stream_tx,
+                    cx,
+                )
+            });
+
+            stream_rx.expect_authorization().await;
+
+            // Test normal path with different modes
+            let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
+            cx.update(|cx| {
+                tool.authorize(
+                    &EditFileToolInput {
+                        display_description: "Edit file".into(),
+                        path: "project/normal.txt".into(),
+                        mode: mode.clone(),
+                    },
+                    &stream_tx,
+                    cx,
+                )
+            })
+            .await
+            .unwrap();
+            assert!(stream_rx.try_next().is_err());
+        }
+    }
+
+    #[gpui::test]
+    async fn test_initial_title_with_partial_input(cx: &mut TestAppContext) {
+        init_test(cx);
+        let fs = project::FakeFs::new(cx.executor());
+        let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
+        let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
+        let action_log = cx.new(|_| ActionLog::new(project.clone()));
+        let context_server_registry =
+            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
+        let model = Arc::new(FakeLanguageModel::default());
+        let thread = cx.new(|cx| {
+            Thread::new(
+                project.clone(),
+                cx.new(|_cx| ProjectContext::default()),
+                context_server_registry,
+                action_log.clone(),
+                Templates::new(),
+                Some(model.clone()),
+                cx,
+            )
+        });
+        let tool = Arc::new(EditFileTool::new(thread.downgrade(), language_registry));
+
+        assert_eq!(
+            tool.initial_title(Err(json!({
+                "path": "src/main.rs",
+                "display_description": "",
+                "old_string": "old code",
+                "new_string": "new code"
+            }))),
+            "src/main.rs"
+        );
+        assert_eq!(
+            tool.initial_title(Err(json!({
+                "path": "",
+                "display_description": "Fix error handling",
+                "old_string": "old code",
+                "new_string": "new code"
+            }))),
+            "Fix error handling"
+        );
+        assert_eq!(
+            tool.initial_title(Err(json!({
+                "path": "src/main.rs",
+                "display_description": "Fix error handling",
+                "old_string": "old code",
+                "new_string": "new code"
+            }))),
+            "Fix error handling"
+        );
+        assert_eq!(
+            tool.initial_title(Err(json!({
+                "path": "",
+                "display_description": "",
+                "old_string": "old code",
+                "new_string": "new code"
+            }))),
+            DEFAULT_UI_TEXT
+        );
+        assert_eq!(
+            tool.initial_title(Err(serde_json::Value::Null)),
+            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);
+        });
+    }
+}

crates/agent2/src/tools/fetch_tool.rs 🔗

@@ -0,0 +1,155 @@
+use std::rc::Rc;
+use std::sync::Arc;
+use std::{borrow::Cow, cell::RefCell};
+
+use agent_client_protocol as acp;
+use anyhow::{Context as _, Result, bail};
+use futures::AsyncReadExt as _;
+use gpui::{App, AppContext as _, Task};
+use html_to_markdown::{TagHandler, convert_html_to_markdown, markdown};
+use http_client::{AsyncBody, HttpClientWithUrl};
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+use ui::SharedString;
+use util::markdown::MarkdownEscaped;
+
+use crate::{AgentTool, ToolCallEventStream};
+
+#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
+enum ContentType {
+    Html,
+    Plaintext,
+    Json,
+}
+
+/// Fetches a URL and returns the content as Markdown.
+#[derive(Debug, Serialize, Deserialize, JsonSchema)]
+pub struct FetchToolInput {
+    /// The URL to fetch.
+    url: String,
+}
+
+pub struct FetchTool {
+    http_client: Arc<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 = if content_type.starts_with("text/plain") {
+            ContentType::Plaintext
+        } else if content_type.starts_with("application/json") {
+            ContentType::Json
+        } else {
+            ContentType::Html
+        };
+
+        match content_type {
+            ContentType::Html => {
+                let mut handlers: Vec<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 AgentTool for FetchTool {
+    type Input = FetchToolInput;
+    type Output = String;
+
+    fn name(&self) -> SharedString {
+        "fetch".into()
+    }
+
+    fn kind(&self) -> acp::ToolKind {
+        acp::ToolKind::Fetch
+    }
+
+    fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
+        match input {
+            Ok(input) => format!("Fetch {}", MarkdownEscaped(&input.url)).into(),
+            Err(_) => "Fetch URL".into(),
+        }
+    }
+
+    fn run(
+        self: Arc<Self>,
+        input: Self::Input,
+        _event_stream: ToolCallEventStream,
+        cx: &mut App,
+    ) -> Task<Result<Self::Output>> {
+        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)
+        })
+    }
+}

crates/agent2/src/tools/find_path_tool.rs 🔗

@@ -1,6 +1,8 @@
+use crate::{AgentTool, ToolCallEventStream};
 use agent_client_protocol as acp;
-use anyhow::{anyhow, Result};
+use anyhow::{Result, anyhow};
 use gpui::{App, AppContext, Entity, SharedString, Task};
+use language_model::LanguageModelToolResultContent;
 use project::Project;
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
@@ -8,8 +10,6 @@ use std::fmt::Write;
 use std::{cmp, path::PathBuf, sync::Arc};
 use util::paths::PathMatcher;
 
-use crate::{AgentTool, ToolCallEventStream};
-
 /// Fast file path pattern matching tool that works with any codebase size
 ///
 /// - Supports glob patterns like "**/*.js" or "src/**/*.ts"
@@ -31,7 +31,6 @@ pub struct FindPathToolInput {
     /// 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)]
@@ -39,8 +38,35 @@ pub struct FindPathToolInput {
 }
 
 #[derive(Debug, Serialize, Deserialize)]
-struct FindPathToolOutput {
-    paths: Vec<PathBuf>,
+pub struct FindPathToolOutput {
+    offset: usize,
+    current_matches_page: Vec<PathBuf>,
+    all_matches_len: usize,
+}
+
+impl From<FindPathToolOutput> for LanguageModelToolResultContent {
+    fn from(output: FindPathToolOutput) -> Self {
+        if output.current_matches_page.is_empty() {
+            "No matches found".into()
+        } else {
+            let mut llm_output = format!("Found {} total matches.", output.all_matches_len);
+            if output.all_matches_len > RESULTS_PER_PAGE {
+                write!(
+                    &mut llm_output,
+                    "\nShowing results {}-{} (provide 'offset' parameter for more results):",
+                    output.offset + 1,
+                    output.offset + output.current_matches_page.len()
+                )
+                .unwrap();
+            }
+
+            for mat in output.current_matches_page {
+                write!(&mut llm_output, "\n{}", mat.display()).unwrap();
+            }
+
+            llm_output.into()
+        }
+    }
 }
 
 const RESULTS_PER_PAGE: usize = 50;
@@ -57,6 +83,7 @@ impl FindPathTool {
 
 impl AgentTool for FindPathTool {
     type Input = FindPathToolInput;
+    type Output = FindPathToolOutput;
 
     fn name(&self) -> SharedString {
         "find_path".into()
@@ -66,8 +93,12 @@ impl AgentTool for FindPathTool {
         acp::ToolKind::Search
     }
 
-    fn initial_title(&self, input: Self::Input) -> SharedString {
-        format!("Find paths matching “`{}`”", input.glob).into()
+    fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
+        let mut title = "Find paths".to_string();
+        if let Ok(input) = input {
+            title.push_str(&format!(" matching “`{}`”", input.glob));
+        }
+        title.into()
     }
 
     fn run(
@@ -75,7 +106,7 @@ impl AgentTool for FindPathTool {
         input: Self::Input,
         event_stream: ToolCallEventStream,
         cx: &mut App,
-    ) -> Task<Result<String>> {
+    ) -> Task<Result<FindPathToolOutput>> {
         let search_paths_task = search_paths(&input.glob, self.project.clone(), cx);
 
         cx.background_spawn(async move {
@@ -83,8 +114,8 @@ impl AgentTool for FindPathTool {
             let paginated_matches: &[PathBuf] = &matches[cmp::min(input.offset, matches.len())
                 ..cmp::min(input.offset + RESULTS_PER_PAGE, matches.len())];
 
-            event_stream.send_update(acp::ToolCallUpdateFields {
-                title: Some(if paginated_matches.len() == 0 {
+            event_stream.update_fields(acp::ToolCallUpdateFields {
+                title: Some(if paginated_matches.is_empty() {
                     "No matches".into()
                 } else if paginated_matches.len() == 1 {
                     "1 match".into()
@@ -107,32 +138,14 @@ impl AgentTool for FindPathTool {
                         })
                         .collect(),
                 ),
-                raw_output: Some(serde_json::json!({
-                    "paths": &matches,
-                })),
                 ..Default::default()
             });
 
-            if matches.is_empty() {
-                Ok("No matches found".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):",
-                        input.offset + 1,
-                        input.offset + paginated_matches.len()
-                    )
-                    .unwrap();
-                }
-
-                for mat in matches.iter().skip(input.offset).take(RESULTS_PER_PAGE) {
-                    write!(&mut message, "\n{}", mat.display()).unwrap();
-                }
-
-                Ok(message)
-            }
+            Ok(FindPathToolOutput {
+                offset: input.offset,
+                current_matches_page: paginated_matches.to_vec(),
+                all_matches_len: matches.len(),
+            })
         })
     }
 }

crates/agent2/src/tools/grep_tool.rs 🔗

@@ -0,0 +1,1182 @@
+use crate::{AgentTool, ToolCallEventStream};
+use agent_client_protocol as acp;
+use anyhow::{Result, anyhow};
+use futures::StreamExt;
+use gpui::{App, Entity, SharedString, Task};
+use language::{OffsetRangeExt, ParseStatus, Point};
+use project::{
+    Project, WorktreeSettings,
+    search::{SearchQuery, SearchResult},
+};
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+use settings::Settings;
+use std::{cmp, fmt::Write, sync::Arc};
+use util::RangeExt;
+use util::markdown::MarkdownInlineCode;
+use util::paths::PathMatcher;
+
+/// Searches the contents of files in the project with a regular expression
+///
+/// - Prefer this tool to path search when searching for symbols in the project, because you won't need to guess what path it's in.
+/// - Supports full regex syntax (eg. "log.*Error", "function\\s+\\w+", etc.)
+/// - Pass an `include_pattern` if you know how to narrow your search on the files system
+/// - Never use this tool to search for paths. Only search file contents with this tool.
+/// - Use this tool when you need to find files containing specific patterns
+/// - Results are paginated with 20 matches per page. Use the optional 'offset' parameter to request subsequent pages.
+/// - DO NOT use HTML entities solely to escape characters in the tool parameters.
+#[derive(Debug, Serialize, Deserialize, JsonSchema)]
+pub struct GrepToolInput {
+    /// A regex pattern to search for in the entire project. Note that the regex will be parsed by the Rust `regex` crate.
+    ///
+    /// Do NOT specify a path here! This will only be matched against the code **content**.
+    pub regex: String,
+    /// A glob pattern for the paths of files to include in the search.
+    /// Supports standard glob patterns like "**/*.rs" or "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 {
+    project: Entity<Project>,
+}
+
+impl GrepTool {
+    pub fn new(project: Entity<Project>) -> Self {
+        Self { project }
+    }
+}
+
+impl AgentTool for GrepTool {
+    type Input = GrepToolInput;
+    type Output = String;
+
+    fn name(&self) -> SharedString {
+        "grep".into()
+    }
+
+    fn kind(&self) -> acp::ToolKind {
+        acp::ToolKind::Search
+    }
+
+    fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
+        match input {
+            Ok(input) => {
+                let page = input.page();
+                let regex_str = MarkdownInlineCode(&input.regex);
+                let case_info = if input.case_sensitive {
+                    " (case-sensitive)"
+                } else {
+                    ""
+                };
+
+                if page > 1 {
+                    format!("Get page {page} of search results for regex {regex_str}{case_info}")
+                } else {
+                    format!("Search files for regex {regex_str}{case_info}")
+                }
+            }
+            Err(_) => "Search with regex".into(),
+        }
+        .into()
+    }
+
+    fn run(
+        self: Arc<Self>,
+        input: Self::Input,
+        _event_stream: ToolCallEventStream,
+        cx: &mut App,
+    ) -> Task<Result<Self::Output>> {
+        const CONTEXT_LINES: u32 = 2;
+        const MAX_ANCESTOR_LINES: u32 = 10;
+
+        let include_matcher = match PathMatcher::new(
+            input
+                .include_pattern
+                .as_ref()
+                .into_iter()
+                .collect::<Vec<_>>(),
+        ) {
+            Ok(matcher) => matcher,
+            Err(error) => {
+                return Task::ready(Err(anyhow!("invalid include glob pattern: {error}")));
+            }
+        };
+
+        // Exclude global file_scan_exclusions and private_files settings
+        let exclude_matcher = {
+            let global_settings = WorktreeSettings::get_global(cx);
+            let exclude_patterns = global_settings
+                .file_scan_exclusions
+                .sources()
+                .iter()
+                .chain(global_settings.private_files.sources().iter());
+
+            match PathMatcher::new(exclude_patterns) {
+                Ok(matcher) => matcher,
+                Err(error) => {
+                    return Task::ready(Err(anyhow!("invalid exclude pattern: {error}")));
+                }
+            }
+        };
+
+        let query = match SearchQuery::regex(
+            &input.regex,
+            false,
+            input.case_sensitive,
+            false,
+            false,
+            include_matcher,
+            exclude_matcher,
+            true, // Always match file include pattern against *full project paths* that start with a project root.
+            None,
+        ) {
+            Ok(query) => query,
+            Err(error) => return Task::ready(Err(error)),
+        };
+
+        let results = self
+            .project
+            .update(cx, |project, cx| project.search(query, cx));
+
+        let project = self.project.downgrade();
+        cx.spawn(async move |cx|  {
+            futures::pin_mut!(results);
+
+            let mut output = String::new();
+            let mut skips_remaining = input.offset;
+            let mut matches_found = 0;
+            let mut has_more_matches = false;
+
+            'outer: while let Some(SearchResult::Buffer { buffer, ranges }) = results.next().await {
+                if ranges.is_empty() {
+                    continue;
+                }
+
+                let Ok((Some(path), mut parse_status)) = buffer.read_with(cx, |buffer, cx| {
+                    (buffer.file().map(|file| file.full_path(cx)), buffer.parse_status())
+                }) else {
+                    continue;
+                };
+
+                // Check if this file should be excluded based on its worktree settings
+                if let Ok(Some(project_path)) = project.read_with(cx, |project, cx| {
+                    project.find_project_path(&path, cx)
+                })
+                    && cx.update(|cx| {
+                        let worktree_settings = WorktreeSettings::get(Some((&project_path).into()), cx);
+                        worktree_settings.is_path_excluded(&project_path.path)
+                            || worktree_settings.is_path_private(&project_path.path)
+                    }).unwrap_or(false) {
+                        continue;
+                    }
+
+                while *parse_status.borrow() != ParseStatus::Idle {
+                    parse_status.changed().await?;
+                }
+
+                let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
+
+                let mut ranges = ranges
+                    .into_iter()
+                    .map(|range| {
+                        let matched = range.to_point(&snapshot);
+                        let matched_end_line_len = snapshot.line_len(matched.end.row);
+                        let full_lines = Point::new(matched.start.row, 0)..Point::new(matched.end.row, matched_end_line_len);
+                        let symbols = snapshot.symbols_containing(matched.start, None);
+
+                        if let Some(ancestor_node) = snapshot.syntax_ancestor(full_lines.clone()) {
+                            let full_ancestor_range = ancestor_node.byte_range().to_point(&snapshot);
+                            let end_row = full_ancestor_range.end.row.min(full_ancestor_range.start.row + MAX_ANCESTOR_LINES);
+                            let end_col = snapshot.line_len(end_row);
+                            let capped_ancestor_range = Point::new(full_ancestor_range.start.row, 0)..Point::new(end_row, end_col);
+
+                            if capped_ancestor_range.contains_inclusive(&full_lines) {
+                                return (capped_ancestor_range, Some(full_ancestor_range), symbols)
+                            }
+                        }
+
+                        let mut matched = matched;
+                        matched.start.column = 0;
+                        matched.start.row =
+                            matched.start.row.saturating_sub(CONTEXT_LINES);
+                        matched.end.row = cmp::min(
+                            snapshot.max_point().row,
+                            matched.end.row + CONTEXT_LINES,
+                        );
+                        matched.end.column = snapshot.line_len(matched.end.row);
+
+                        (matched, None, symbols)
+                    })
+                    .peekable();
+
+                let mut file_header_written = false;
+
+                while let Some((mut range, ancestor_range, parent_symbols)) = ranges.next(){
+                    if skips_remaining > 0 {
+                        skips_remaining -= 1;
+                        continue;
+                    }
+
+                    // We'd already found a full page of matches, and we just found one more.
+                    if matches_found >= RESULTS_PER_PAGE {
+                        has_more_matches = true;
+                        break 'outer;
+                    }
+
+                    while let Some((next_range, _, _)) = ranges.peek() {
+                        if range.end.row >= next_range.start.row {
+                            range.end = next_range.end;
+                            ranges.next();
+                        } else {
+                            break;
+                        }
+                    }
+
+                    if !file_header_written {
+                        writeln!(output, "\n## Matches in {}", path.display())?;
+                        file_header_written = true;
+                    }
+
+                    let end_row = range.end.row;
+                    output.push_str("\n### ");
+
+                    if let Some(parent_symbols) = &parent_symbols {
+                        for symbol in parent_symbols {
+                            write!(output, "{} › ", symbol.text)?;
+                        }
+                    }
+
+                    if range.start.row == end_row {
+                        writeln!(output, "L{}", range.start.row + 1)?;
+                    } else {
+                        writeln!(output, "L{}-{}", range.start.row + 1, end_row + 1)?;
+                    }
+
+                    output.push_str("```\n");
+                    output.extend(snapshot.text_for_range(range));
+                    output.push_str("\n```\n");
+
+                    if let Some(ancestor_range) = ancestor_range
+                        && end_row < ancestor_range.end.row {
+                            let remaining_lines = ancestor_range.end.row - end_row;
+                            writeln!(output, "\n{} lines remaining in ancestor node. Read the file to see all.", remaining_lines)?;
+                        }
+
+                    matches_found += 1;
+                }
+            }
+
+            if matches_found == 0 {
+                Ok("No matches found".into())
+            } else if has_more_matches {
+                Ok(format!(
+                    "Showing matches {}-{} (there were more matches found; use offset: {} to see next page):\n{output}",
+                    input.offset + 1,
+                    input.offset + matches_found,
+                    input.offset + RESULTS_PER_PAGE,
+                ))
+            } else {
+                Ok(format!("Found {matches_found} matches:\n{output}"))
+            }
+        })
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use crate::ToolCallEventStream;
+
+    use super::*;
+    use gpui::{TestAppContext, UpdateGlobal};
+    use language::{Language, LanguageConfig, LanguageMatcher};
+    use project::{FakeFs, Project, WorktreeSettings};
+    use serde_json::json;
+    use settings::SettingsStore;
+    use unindent::Unindent;
+    use util::path;
+
+    #[gpui::test]
+    async fn test_grep_tool_with_include_pattern(cx: &mut TestAppContext) {
+        init_test(cx);
+        cx.executor().allow_parking();
+
+        let fs = FakeFs::new(cx.executor());
+        fs.insert_tree(
+            path!("/root"),
+            serde_json::json!({
+                "src": {
+                    "main.rs": "fn main() {\n    println!(\"Hello, world!\");\n}",
+                    "utils": {
+                        "helper.rs": "fn helper() {\n    println!(\"I'm a helper!\");\n}",
+                    },
+                },
+                "tests": {
+                    "test_main.rs": "fn test_main() {\n    assert!(true);\n}",
+                }
+            }),
+        )
+        .await;
+
+        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
+
+        // Test with include pattern for Rust files inside the root of the project
+        let input = GrepToolInput {
+            regex: "println".to_string(),
+            include_pattern: Some("root/**/*.rs".to_string()),
+            offset: 0,
+            case_sensitive: false,
+        };
+
+        let result = run_grep_tool(input, project.clone(), cx).await;
+        assert!(result.contains("main.rs"), "Should find matches in main.rs");
+        assert!(
+            result.contains("helper.rs"),
+            "Should find matches in helper.rs"
+        );
+        assert!(
+            !result.contains("test_main.rs"),
+            "Should not include test_main.rs even though it's a .rs file (because it doesn't have the pattern)"
+        );
+
+        // Test with include pattern for src directory only
+        let input = GrepToolInput {
+            regex: "fn".to_string(),
+            include_pattern: Some("root/**/src/**".to_string()),
+            offset: 0,
+            case_sensitive: false,
+        };
+
+        let result = run_grep_tool(input, project.clone(), cx).await;
+        assert!(
+            result.contains("main.rs"),
+            "Should find matches in src/main.rs"
+        );
+        assert!(
+            result.contains("helper.rs"),
+            "Should find matches in src/utils/helper.rs"
+        );
+        assert!(
+            !result.contains("test_main.rs"),
+            "Should not include test_main.rs as it's not in src directory"
+        );
+
+        // Test with empty include pattern (should default to all files)
+        let input = GrepToolInput {
+            regex: "fn".to_string(),
+            include_pattern: None,
+            offset: 0,
+            case_sensitive: false,
+        };
+
+        let result = run_grep_tool(input, project.clone(), cx).await;
+        assert!(result.contains("main.rs"), "Should find matches in main.rs");
+        assert!(
+            result.contains("helper.rs"),
+            "Should find matches in helper.rs"
+        );
+        assert!(
+            result.contains("test_main.rs"),
+            "Should include test_main.rs"
+        );
+    }
+
+    #[gpui::test]
+    async fn test_grep_tool_with_case_sensitivity(cx: &mut TestAppContext) {
+        init_test(cx);
+        cx.executor().allow_parking();
+
+        let fs = FakeFs::new(cx.executor());
+        fs.insert_tree(
+            path!("/root"),
+            serde_json::json!({
+                "case_test.txt": "This file has UPPERCASE and lowercase text.\nUPPERCASE patterns should match only with case_sensitive: true",
+            }),
+        )
+        .await;
+
+        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
+
+        // Test case-insensitive search (default)
+        let input = GrepToolInput {
+            regex: "uppercase".to_string(),
+            include_pattern: Some("**/*.txt".to_string()),
+            offset: 0,
+            case_sensitive: false,
+        };
+
+        let result = run_grep_tool(input, project.clone(), cx).await;
+        assert!(
+            result.contains("UPPERCASE"),
+            "Case-insensitive search should match uppercase"
+        );
+
+        // Test case-sensitive search
+        let input = GrepToolInput {
+            regex: "uppercase".to_string(),
+            include_pattern: Some("**/*.txt".to_string()),
+            offset: 0,
+            case_sensitive: true,
+        };
+
+        let result = run_grep_tool(input, project.clone(), cx).await;
+        assert!(
+            !result.contains("UPPERCASE"),
+            "Case-sensitive search should not match uppercase"
+        );
+
+        // Test case-sensitive search
+        let input = GrepToolInput {
+            regex: "LOWERCASE".to_string(),
+            include_pattern: Some("**/*.txt".to_string()),
+            offset: 0,
+            case_sensitive: true,
+        };
+
+        let result = run_grep_tool(input, project.clone(), cx).await;
+
+        assert!(
+            !result.contains("lowercase"),
+            "Case-sensitive search should match lowercase"
+        );
+
+        // Test case-sensitive search for lowercase pattern
+        let input = GrepToolInput {
+            regex: "lowercase".to_string(),
+            include_pattern: Some("**/*.txt".to_string()),
+            offset: 0,
+            case_sensitive: true,
+        };
+
+        let result = run_grep_tool(input, project.clone(), cx).await;
+        assert!(
+            result.contains("lowercase"),
+            "Case-sensitive search should match lowercase text"
+        );
+    }
+
+    /// Helper function to set up a syntax test environment
+    async fn setup_syntax_test(cx: &mut TestAppContext) -> Entity<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 = GrepToolInput {
+            regex: "This is at the top level".to_string(),
+            include_pattern: Some("**/*.rs".to_string()),
+            offset: 0,
+            case_sensitive: false,
+        };
+
+        let result = run_grep_tool(input, project.clone(), cx).await;
+        let expected = r#"
+            Found 1 matches:
+
+            ## Matches in root/test_syntax.rs
+
+            ### fn top_level_function › L1-3
+            ```
+            fn top_level_function() {
+                println!("This is at the top level");
+            }
+            ```
+            "#
+        .unindent();
+        assert_eq!(result, expected);
+    }
+
+    #[gpui::test]
+    async fn test_grep_function_body(cx: &mut TestAppContext) {
+        let project = setup_syntax_test(cx).await;
+
+        // Test: Line inside a function body
+        let input = GrepToolInput {
+            regex: "Function in nested module".to_string(),
+            include_pattern: Some("**/*.rs".to_string()),
+            offset: 0,
+            case_sensitive: false,
+        };
+
+        let result = run_grep_tool(input, project.clone(), cx).await;
+        let expected = r#"
+            Found 1 matches:
+
+            ## Matches in root/test_syntax.rs
+
+            ### mod feature_module › pub mod nested_module › pub fn nested_function › L10-14
+            ```
+                    ) {
+                        println!("Function in nested module");
+                        println!("{first_arg}");
+                        println!("{second_arg}");
+                    }
+            ```
+            "#
+        .unindent();
+        assert_eq!(result, expected);
+    }
+
+    #[gpui::test]
+    async fn test_grep_function_args_and_body(cx: &mut TestAppContext) {
+        let project = setup_syntax_test(cx).await;
+
+        // Test: Line with a function argument
+        let input = GrepToolInput {
+            regex: "second_arg".to_string(),
+            include_pattern: Some("**/*.rs".to_string()),
+            offset: 0,
+            case_sensitive: false,
+        };
+
+        let result = run_grep_tool(input, project.clone(), cx).await;
+        let expected = r#"
+            Found 1 matches:
+
+            ## Matches in root/test_syntax.rs
+
+            ### mod feature_module › pub mod nested_module › pub fn nested_function › L7-14
+            ```
+                    pub fn nested_function(
+                        first_arg: String,
+                        second_arg: i32,
+                    ) {
+                        println!("Function in nested module");
+                        println!("{first_arg}");
+                        println!("{second_arg}");
+                    }
+            ```
+            "#
+        .unindent();
+        assert_eq!(result, expected);
+    }
+
+    #[gpui::test]
+    async fn test_grep_if_block(cx: &mut TestAppContext) {
+        use unindent::Unindent;
+        let project = setup_syntax_test(cx).await;
+
+        // Test: Line inside an if block
+        let input = GrepToolInput {
+            regex: "Inside if block".to_string(),
+            include_pattern: Some("**/*.rs".to_string()),
+            offset: 0,
+            case_sensitive: false,
+        };
+
+        let result = run_grep_tool(input, project.clone(), cx).await;
+        let expected = r#"
+            Found 1 matches:
+
+            ## Matches in root/test_syntax.rs
+
+            ### impl MyStruct › fn method_with_block › L26-28
+            ```
+                    if condition {
+                        println!("Inside if block");
+                    }
+            ```
+            "#
+        .unindent();
+        assert_eq!(result, expected);
+    }
+
+    #[gpui::test]
+    async fn test_grep_long_function_top(cx: &mut TestAppContext) {
+        use unindent::Unindent;
+        let project = setup_syntax_test(cx).await;
+
+        // Test: Line in the middle of a long function - should show message about remaining lines
+        let input = GrepToolInput {
+            regex: "Line 5".to_string(),
+            include_pattern: Some("**/*.rs".to_string()),
+            offset: 0,
+            case_sensitive: false,
+        };
+
+        let result = run_grep_tool(input, project.clone(), cx).await;
+        let expected = r#"
+            Found 1 matches:
+
+            ## Matches in root/test_syntax.rs
+
+            ### impl MyStruct › fn long_function › L31-41
+            ```
+                fn long_function() {
+                    println!("Line 1");
+                    println!("Line 2");
+                    println!("Line 3");
+                    println!("Line 4");
+                    println!("Line 5");
+                    println!("Line 6");
+                    println!("Line 7");
+                    println!("Line 8");
+                    println!("Line 9");
+                    println!("Line 10");
+            ```
+
+            3 lines remaining in ancestor node. Read the file to see all.
+            "#
+        .unindent();
+        assert_eq!(result, expected);
+    }
+
+    #[gpui::test]
+    async fn test_grep_long_function_bottom(cx: &mut TestAppContext) {
+        use unindent::Unindent;
+        let project = setup_syntax_test(cx).await;
+
+        // Test: Line in the long function
+        let input = GrepToolInput {
+            regex: "Line 12".to_string(),
+            include_pattern: Some("**/*.rs".to_string()),
+            offset: 0,
+            case_sensitive: false,
+        };
+
+        let result = run_grep_tool(input, project.clone(), cx).await;
+        let expected = r#"
+            Found 1 matches:
+
+            ## Matches in root/test_syntax.rs
+
+            ### impl MyStruct › fn long_function › L41-45
+            ```
+                    println!("Line 10");
+                    println!("Line 11");
+                    println!("Line 12");
+                }
+            }
+            ```
+            "#
+        .unindent();
+        assert_eq!(result, expected);
+    }
+
+    async fn run_grep_tool(
+        input: GrepToolInput,
+        project: Entity<Project>,
+        cx: &mut TestAppContext,
+    ) -> String {
+        let tool = Arc::new(GrepTool { project });
+        let task = cx.update(|cx| tool.run(input, ToolCallEventStream::test().0, cx));
+
+        match task.await {
+            Ok(result) => {
+                if cfg!(windows) {
+                    result.replace("root\\", "root/")
+                } else {
+                    result
+                }
+            }
+            Err(e) => panic!("Failed to run grep tool: {}", e),
+        }
+    }
+
+    fn init_test(cx: &mut TestAppContext) {
+        cx.update(|cx| {
+            let settings_store = SettingsStore::test(cx);
+            cx.set_global(settings_store);
+            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 project::WorktreeSettings;
+            use settings::SettingsStore;
+            SettingsStore::update_global(cx, |store, cx| {
+                store.update_user_settings::<WorktreeSettings>(cx, |settings| {
+                    settings.file_scan_exclusions = Some(vec![
+                        "**/.secretdir".to_string(),
+                        "**/.mymetadata".to_string(),
+                    ]);
+                    settings.private_files = Some(vec![
+                        "**/.mysecrets".to_string(),
+                        "**/*.privatekey".to_string(),
+                        "**/*.mysensitive".to_string(),
+                    ]);
+                });
+            });
+        });
+
+        let project = Project::test(fs.clone(), [path!("/project_root").as_ref()], cx).await;
+
+        // Searching for files outside the project worktree should return no results
+        let result = run_grep_tool(
+            GrepToolInput {
+                regex: "outside_function".to_string(),
+                include_pattern: None,
+                offset: 0,
+                case_sensitive: false,
+            },
+            project.clone(),
+            cx,
+        )
+        .await;
+        let paths = extract_paths_from_results(&result);
+        assert!(
+            paths.is_empty(),
+            "grep_tool should not find files outside the project worktree"
+        );
+
+        // Searching within the project should succeed
+        let result = run_grep_tool(
+            GrepToolInput {
+                regex: "main".to_string(),
+                include_pattern: None,
+                offset: 0,
+                case_sensitive: false,
+            },
+            project.clone(),
+            cx,
+        )
+        .await;
+        let paths = extract_paths_from_results(&result);
+        assert!(
+            paths.iter().any(|p| p.contains("allowed_file.rs")),
+            "grep_tool should be able to search files inside worktrees"
+        );
+
+        // Searching files that match file_scan_exclusions should return no results
+        let result = run_grep_tool(
+            GrepToolInput {
+                regex: "special_configuration".to_string(),
+                include_pattern: None,
+                offset: 0,
+                case_sensitive: false,
+            },
+            project.clone(),
+            cx,
+        )
+        .await;
+        let paths = extract_paths_from_results(&result);
+        assert!(
+            paths.is_empty(),
+            "grep_tool should not search files in .secretdir (file_scan_exclusions)"
+        );
+
+        let result = run_grep_tool(
+            GrepToolInput {
+                regex: "custom_metadata".to_string(),
+                include_pattern: None,
+                offset: 0,
+                case_sensitive: false,
+            },
+            project.clone(),
+            cx,
+        )
+        .await;
+        let paths = extract_paths_from_results(&result);
+        assert!(
+            paths.is_empty(),
+            "grep_tool should not search .mymetadata files (file_scan_exclusions)"
+        );
+
+        // Searching private files should return no results
+        let result = run_grep_tool(
+            GrepToolInput {
+                regex: "SECRET_KEY".to_string(),
+                include_pattern: None,
+                offset: 0,
+                case_sensitive: false,
+            },
+            project.clone(),
+            cx,
+        )
+        .await;
+        let paths = extract_paths_from_results(&result);
+        assert!(
+            paths.is_empty(),
+            "grep_tool should not search .mysecrets (private_files)"
+        );
+
+        let result = run_grep_tool(
+            GrepToolInput {
+                regex: "private_key_content".to_string(),
+                include_pattern: None,
+                offset: 0,
+                case_sensitive: false,
+            },
+            project.clone(),
+            cx,
+        )
+        .await;
+        let paths = extract_paths_from_results(&result);
+
+        assert!(
+            paths.is_empty(),
+            "grep_tool should not search .privatekey files (private_files)"
+        );
+
+        let result = run_grep_tool(
+            GrepToolInput {
+                regex: "sensitive_data".to_string(),
+                include_pattern: None,
+                offset: 0,
+                case_sensitive: false,
+            },
+            project.clone(),
+            cx,
+        )
+        .await;
+        let paths = extract_paths_from_results(&result);
+        assert!(
+            paths.is_empty(),
+            "grep_tool should not search .mysensitive files (private_files)"
+        );
+
+        // Searching a normal file should still work, even with private_files configured
+        let result = run_grep_tool(
+            GrepToolInput {
+                regex: "normal_file_content".to_string(),
+                include_pattern: None,
+                offset: 0,
+                case_sensitive: false,
+            },
+            project.clone(),
+            cx,
+        )
+        .await;
+        let paths = extract_paths_from_results(&result);
+        assert!(
+            paths.iter().any(|p| p.contains("normal_file.rs")),
+            "Should be able to search normal files"
+        );
+
+        // Path traversal attempts with .. in include_pattern should not escape project
+        let result = run_grep_tool(
+            GrepToolInput {
+                regex: "outside_function".to_string(),
+                include_pattern: Some("../outside_project/**/*.rs".to_string()),
+                offset: 0,
+                case_sensitive: false,
+            },
+            project.clone(),
+            cx,
+        )
+        .await;
+        let paths = extract_paths_from_results(&result);
+        assert!(
+            paths.is_empty(),
+            "grep_tool should not allow escaping project boundaries with relative paths"
+        );
+    }
+
+    #[gpui::test]
+    async fn test_grep_with_multiple_worktree_settings(cx: &mut TestAppContext) {
+        init_test(cx);
+
+        let fs = FakeFs::new(cx.executor());
+
+        // Create first worktree with its own private files
+        fs.insert_tree(
+            path!("/worktree1"),
+            json!({
+                ".zed": {
+                    "settings.json": r#"{
+                        "file_scan_exclusions": ["**/fixture.*"],
+                        "private_files": ["**/secret.rs"]
+                    }"#
+                },
+                "src": {
+                    "main.rs": "fn main() { let secret_key = \"hidden\"; }",
+                    "secret.rs": "const API_KEY: &str = \"secret_value\";",
+                    "utils.rs": "pub fn get_config() -> String { \"config\".to_string() }"
+                },
+                "tests": {
+                    "test.rs": "fn test_secret() { assert!(true); }",
+                    "fixture.sql": "SELECT * FROM secret_table;"
+                }
+            }),
+        )
+        .await;
+
+        // Create second worktree with different private files
+        fs.insert_tree(
+            path!("/worktree2"),
+            json!({
+                ".zed": {
+                    "settings.json": r#"{
+                        "file_scan_exclusions": ["**/internal.*"],
+                        "private_files": ["**/private.js", "**/data.json"]
+                    }"#
+                },
+                "lib": {
+                    "public.js": "export function getSecret() { return 'public'; }",
+                    "private.js": "const SECRET_KEY = \"private_value\";",
+                    "data.json": "{\"secret_data\": \"hidden\"}"
+                },
+                "docs": {
+                    "README.md": "# Documentation with secret info",
+                    "internal.md": "Internal secret documentation"
+                }
+            }),
+        )
+        .await;
+
+        // Set global settings
+        cx.update(|cx| {
+            SettingsStore::update_global(cx, |store, cx| {
+                store.update_user_settings::<WorktreeSettings>(cx, |settings| {
+                    settings.file_scan_exclusions =
+                        Some(vec!["**/.git".to_string(), "**/node_modules".to_string()]);
+                    settings.private_files = Some(vec!["**/.env".to_string()]);
+                });
+            });
+        });
+
+        let project = Project::test(
+            fs.clone(),
+            [path!("/worktree1").as_ref(), path!("/worktree2").as_ref()],
+            cx,
+        )
+        .await;
+
+        // Wait for worktrees to be fully scanned
+        cx.executor().run_until_parked();
+
+        // Search for "secret" - should exclude files based on worktree-specific settings
+        let result = run_grep_tool(
+            GrepToolInput {
+                regex: "secret".to_string(),
+                include_pattern: None,
+                offset: 0,
+                case_sensitive: false,
+            },
+            project.clone(),
+            cx,
+        )
+        .await;
+        let paths = extract_paths_from_results(&result);
+
+        // Should find matches in non-private files
+        assert!(
+            paths.iter().any(|p| p.contains("main.rs")),
+            "Should find 'secret' in worktree1/src/main.rs"
+        );
+        assert!(
+            paths.iter().any(|p| p.contains("test.rs")),
+            "Should find 'secret' in worktree1/tests/test.rs"
+        );
+        assert!(
+            paths.iter().any(|p| p.contains("public.js")),
+            "Should find 'secret' in worktree2/lib/public.js"
+        );
+        assert!(
+            paths.iter().any(|p| p.contains("README.md")),
+            "Should find 'secret' in worktree2/docs/README.md"
+        );
+
+        // Should NOT find matches in private/excluded files based on worktree settings
+        assert!(
+            !paths.iter().any(|p| p.contains("secret.rs")),
+            "Should not search in worktree1/src/secret.rs (local private_files)"
+        );
+        assert!(
+            !paths.iter().any(|p| p.contains("fixture.sql")),
+            "Should not search in worktree1/tests/fixture.sql (local file_scan_exclusions)"
+        );
+        assert!(
+            !paths.iter().any(|p| p.contains("private.js")),
+            "Should not search in worktree2/lib/private.js (local private_files)"
+        );
+        assert!(
+            !paths.iter().any(|p| p.contains("data.json")),
+            "Should not search in worktree2/lib/data.json (local private_files)"
+        );
+        assert!(
+            !paths.iter().any(|p| p.contains("internal.md")),
+            "Should not search in worktree2/docs/internal.md (local file_scan_exclusions)"
+        );
+
+        // Test with `include_pattern` specific to one worktree
+        let result = run_grep_tool(
+            GrepToolInput {
+                regex: "secret".to_string(),
+                include_pattern: Some("worktree1/**/*.rs".to_string()),
+                offset: 0,
+                case_sensitive: false,
+            },
+            project.clone(),
+            cx,
+        )
+        .await;
+
+        let paths = extract_paths_from_results(&result);
+
+        // Should only find matches in worktree1 *.rs files (excluding private ones)
+        assert!(
+            paths.iter().any(|p| p.contains("main.rs")),
+            "Should find match in worktree1/src/main.rs"
+        );
+        assert!(
+            paths.iter().any(|p| p.contains("test.rs")),
+            "Should find match in worktree1/tests/test.rs"
+        );
+        assert!(
+            !paths.iter().any(|p| p.contains("secret.rs")),
+            "Should not find match in excluded worktree1/src/secret.rs"
+        );
+        assert!(
+            paths.iter().all(|p| !p.contains("worktree2")),
+            "Should not find any matches in worktree2"
+        );
+    }
+
+    // Helper function to extract file paths from grep results
+    fn extract_paths_from_results(results: &str) -> Vec<String> {
+        results
+            .lines()
+            .filter(|line| line.starts_with("## Matches in "))
+            .map(|line| {
+                line.strip_prefix("## Matches in ")
+                    .unwrap()
+                    .trim()
+                    .to_string()
+            })
+            .collect()
+    }
+}

crates/agent2/src/tools/list_directory_tool.rs 🔗

@@ -0,0 +1,662 @@
+use crate::{AgentTool, ToolCallEventStream};
+use agent_client_protocol::ToolKind;
+use anyhow::{Result, anyhow};
+use gpui::{App, Entity, SharedString, Task};
+use project::{Project, WorktreeSettings};
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+use settings::Settings;
+use std::fmt::Write;
+use std::{path::Path, sync::Arc};
+use util::markdown::MarkdownInlineCode;
+
+/// Lists files and directories in a given path. Prefer the `grep` or `find_path` tools when searching the codebase.
+#[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 {
+    project: Entity<Project>,
+}
+
+impl ListDirectoryTool {
+    pub fn new(project: Entity<Project>) -> Self {
+        Self { project }
+    }
+}
+
+impl AgentTool for ListDirectoryTool {
+    type Input = ListDirectoryToolInput;
+    type Output = String;
+
+    fn name(&self) -> SharedString {
+        "list_directory".into()
+    }
+
+    fn kind(&self) -> ToolKind {
+        ToolKind::Read
+    }
+
+    fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
+        if let Ok(input) = input {
+            let path = MarkdownInlineCode(&input.path);
+            format!("List the {path} directory's contents").into()
+        } else {
+            "List directory".into()
+        }
+    }
+
+    fn run(
+        self: Arc<Self>,
+        input: Self::Input,
+        _event_stream: ToolCallEventStream,
+        cx: &mut App,
+    ) -> Task<Result<Self::Output>> {
+        // Sometimes models will return these even though we tell it to give a path and not a glob.
+        // When this happens, just list the root worktree directories.
+        if matches!(input.path.as_str(), "." | "" | "./" | "*") {
+            let output = self
+                .project
+                .read(cx)
+                .worktrees(cx)
+                .filter_map(|worktree| {
+                    worktree.read(cx).root_entry().and_then(|entry| {
+                        if entry.is_dir() {
+                            entry.path.to_str()
+                        } else {
+                            None
+                        }
+                    })
+                })
+                .collect::<Vec<_>>()
+                .join("\n");
+
+            return Task::ready(Ok(output));
+        }
+
+        let Some(project_path) = self.project.read(cx).find_project_path(&input.path, cx) else {
+            return Task::ready(Err(anyhow!("Path {} not found in project", input.path)));
+        };
+        let Some(worktree) = self
+            .project
+            .read(cx)
+            .worktree_for_id(project_path.worktree_id, cx)
+        else {
+            return Task::ready(Err(anyhow!("Worktree not found")));
+        };
+
+        // Check if the directory whose contents we're listing is itself excluded or private
+        let global_settings = WorktreeSettings::get_global(cx);
+        if global_settings.is_path_excluded(&project_path.path) {
+            return Task::ready(Err(anyhow!(
+                "Cannot list directory because its path matches the user's global `file_scan_exclusions` setting: {}",
+                &input.path
+            )));
+        }
+
+        if global_settings.is_path_private(&project_path.path) {
+            return Task::ready(Err(anyhow!(
+                "Cannot list directory because its path matches the user's global `private_files` setting: {}",
+                &input.path
+            )));
+        }
+
+        let worktree_settings = WorktreeSettings::get(Some((&project_path).into()), cx);
+        if worktree_settings.is_path_excluded(&project_path.path) {
+            return Task::ready(Err(anyhow!(
+                "Cannot list directory because its path matches the user's worktree`file_scan_exclusions` setting: {}",
+                &input.path
+            )));
+        }
+
+        if worktree_settings.is_path_private(&project_path.path) {
+            return Task::ready(Err(anyhow!(
+                "Cannot list directory because its path matches the user's worktree `private_paths` setting: {}",
+                &input.path
+            )));
+        }
+
+        let worktree_snapshot = worktree.read(cx).snapshot();
+        let worktree_root_name = worktree.read(cx).root_name().to_string();
+
+        let Some(entry) = worktree_snapshot.entry_for_path(&project_path.path) else {
+            return Task::ready(Err(anyhow!("Path not found: {}", input.path)));
+        };
+
+        if !entry.is_dir() {
+            return Task::ready(Err(anyhow!("{} is not a directory.", input.path)));
+        }
+        let worktree_snapshot = worktree.read(cx).snapshot();
+
+        let mut folders = Vec::new();
+        let mut files = Vec::new();
+
+        for entry in worktree_snapshot.child_entries(&project_path.path) {
+            // Skip private and excluded files and directories
+            if global_settings.is_path_private(&entry.path)
+                || global_settings.is_path_excluded(&entry.path)
+            {
+                continue;
+            }
+
+            if self
+                .project
+                .read(cx)
+                .find_project_path(&entry.path, cx)
+                .map(|project_path| {
+                    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;
+            }
+
+            let full_path = Path::new(&worktree_root_name)
+                .join(&entry.path)
+                .display()
+                .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))
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use gpui::{TestAppContext, UpdateGlobal};
+    use indoc::indoc;
+    use project::{FakeFs, Project, WorktreeSettings};
+    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 tool = Arc::new(ListDirectoryTool::new(project));
+
+        // Test listing root directory
+        let input = ListDirectoryToolInput {
+            path: "project".into(),
+        };
+        let output = cx
+            .update(|cx| tool.clone().run(input, ToolCallEventStream::test().0, cx))
+            .await
+            .unwrap();
+        assert_eq!(
+            output,
+            platform_paths(indoc! {"
+                # Folders:
+                project/src
+                project/tests
+
+                # Files:
+                project/Cargo.toml
+                project/README.md
+            "})
+        );
+
+        // Test listing src directory
+        let input = ListDirectoryToolInput {
+            path: "project/src".into(),
+        };
+        let output = cx
+            .update(|cx| tool.clone().run(input, ToolCallEventStream::test().0, cx))
+            .await
+            .unwrap();
+        assert_eq!(
+            output,
+            platform_paths(indoc! {"
+                # Folders:
+                project/src/models
+                project/src/utils
+
+                # Files:
+                project/src/lib.rs
+                project/src/main.rs
+            "})
+        );
+
+        // Test listing directory with only files
+        let input = ListDirectoryToolInput {
+            path: "project/tests".into(),
+        };
+        let output = cx
+            .update(|cx| tool.clone().run(input, ToolCallEventStream::test().0, cx))
+            .await
+            .unwrap();
+        assert!(!output.contains("# Folders:"));
+        assert!(output.contains("# Files:"));
+        assert!(output.contains(&platform_paths("project/tests/integration_test.rs")));
+    }
+
+    #[gpui::test]
+    async fn test_list_directory_empty_directory(cx: &mut TestAppContext) {
+        init_test(cx);
+
+        let fs = FakeFs::new(cx.executor());
+        fs.insert_tree(
+            path!("/project"),
+            json!({
+                "empty_dir": {}
+            }),
+        )
+        .await;
+
+        let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
+        let tool = Arc::new(ListDirectoryTool::new(project));
+
+        let input = ListDirectoryToolInput {
+            path: "project/empty_dir".into(),
+        };
+        let output = cx
+            .update(|cx| tool.clone().run(input, ToolCallEventStream::test().0, cx))
+            .await
+            .unwrap();
+        assert_eq!(output, "project/empty_dir is empty.\n");
+    }
+
+    #[gpui::test]
+    async fn test_list_directory_error_cases(cx: &mut TestAppContext) {
+        init_test(cx);
+
+        let fs = FakeFs::new(cx.executor());
+        fs.insert_tree(
+            path!("/project"),
+            json!({
+                "file.txt": "content"
+            }),
+        )
+        .await;
+
+        let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
+        let tool = Arc::new(ListDirectoryTool::new(project));
+
+        // Test non-existent path
+        let input = ListDirectoryToolInput {
+            path: "project/nonexistent".into(),
+        };
+        let output = cx
+            .update(|cx| tool.clone().run(input, ToolCallEventStream::test().0, cx))
+            .await;
+        assert!(output.unwrap_err().to_string().contains("Path not found"));
+
+        // Test trying to list a file instead of directory
+        let input = ListDirectoryToolInput {
+            path: "project/file.txt".into(),
+        };
+        let output = cx
+            .update(|cx| tool.run(input, ToolCallEventStream::test().0, cx))
+            .await;
+        assert!(
+            output
+                .unwrap_err()
+                .to_string()
+                .contains("is not a directory")
+        );
+    }
+
+    #[gpui::test]
+    async fn test_list_directory_security(cx: &mut TestAppContext) {
+        init_test(cx);
+
+        let fs = FakeFs::new(cx.executor());
+        fs.insert_tree(
+            path!("/project"),
+            json!({
+                "normal_dir": {
+                    "file1.txt": "content",
+                    "file2.txt": "content"
+                },
+                ".mysecrets": "SECRET_KEY=abc123",
+                ".secretdir": {
+                    "config": "special configuration",
+                    "secret.txt": "secret content"
+                },
+                ".mymetadata": "custom metadata",
+                "visible_dir": {
+                    "normal.txt": "normal content",
+                    "special.privatekey": "private key content",
+                    "data.mysensitive": "sensitive data",
+                    ".hidden_subdir": {
+                        "hidden_file.txt": "hidden content"
+                    }
+                }
+            }),
+        )
+        .await;
+
+        // Configure settings explicitly
+        cx.update(|cx| {
+            SettingsStore::update_global(cx, |store, cx| {
+                store.update_user_settings::<WorktreeSettings>(cx, |settings| {
+                    settings.file_scan_exclusions = Some(vec![
+                        "**/.secretdir".to_string(),
+                        "**/.mymetadata".to_string(),
+                        "**/.hidden_subdir".to_string(),
+                    ]);
+                    settings.private_files = Some(vec![
+                        "**/.mysecrets".to_string(),
+                        "**/*.privatekey".to_string(),
+                        "**/*.mysensitive".to_string(),
+                    ]);
+                });
+            });
+        });
+
+        let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
+        let tool = Arc::new(ListDirectoryTool::new(project));
+
+        // Listing root directory should exclude private and excluded files
+        let input = ListDirectoryToolInput {
+            path: "project".into(),
+        };
+        let output = cx
+            .update(|cx| tool.clone().run(input, ToolCallEventStream::test().0, cx))
+            .await
+            .unwrap();
+
+        // Should include normal directories
+        assert!(output.contains("normal_dir"), "Should list normal_dir");
+        assert!(output.contains("visible_dir"), "Should list visible_dir");
+
+        // Should NOT include excluded or private files
+        assert!(
+            !output.contains(".secretdir"),
+            "Should not list .secretdir (file_scan_exclusions)"
+        );
+        assert!(
+            !output.contains(".mymetadata"),
+            "Should not list .mymetadata (file_scan_exclusions)"
+        );
+        assert!(
+            !output.contains(".mysecrets"),
+            "Should not list .mysecrets (private_files)"
+        );
+
+        // Trying to list an excluded directory should fail
+        let input = ListDirectoryToolInput {
+            path: "project/.secretdir".into(),
+        };
+        let output = cx
+            .update(|cx| tool.clone().run(input, ToolCallEventStream::test().0, cx))
+            .await;
+        assert!(
+            output
+                .unwrap_err()
+                .to_string()
+                .contains("file_scan_exclusions"),
+            "Error should mention file_scan_exclusions"
+        );
+
+        // Listing a directory should exclude private files within it
+        let input = ListDirectoryToolInput {
+            path: "project/visible_dir".into(),
+        };
+        let output = cx
+            .update(|cx| tool.clone().run(input, ToolCallEventStream::test().0, cx))
+            .await
+            .unwrap();
+
+        // Should include normal files
+        assert!(output.contains("normal.txt"), "Should list normal.txt");
+
+        // Should NOT include private files
+        assert!(
+            !output.contains("privatekey"),
+            "Should not list .privatekey files (private_files)"
+        );
+        assert!(
+            !output.contains("mysensitive"),
+            "Should not list .mysensitive files (private_files)"
+        );
+
+        // Should NOT include subdirectories that match exclusions
+        assert!(
+            !output.contains(".hidden_subdir"),
+            "Should not list .hidden_subdir (file_scan_exclusions)"
+        );
+    }
+
+    #[gpui::test]
+    async fn test_list_directory_with_multiple_worktree_settings(cx: &mut TestAppContext) {
+        init_test(cx);
+
+        let fs = FakeFs::new(cx.executor());
+
+        // Create first worktree with its own private files
+        fs.insert_tree(
+            path!("/worktree1"),
+            json!({
+                ".zed": {
+                    "settings.json": r#"{
+                        "file_scan_exclusions": ["**/fixture.*"],
+                        "private_files": ["**/secret.rs", "**/config.toml"]
+                    }"#
+                },
+                "src": {
+                    "main.rs": "fn main() { println!(\"Hello from worktree1\"); }",
+                    "secret.rs": "const API_KEY: &str = \"secret_key_1\";",
+                    "config.toml": "[database]\nurl = \"postgres://localhost/db1\""
+                },
+                "tests": {
+                    "test.rs": "mod tests { fn test_it() {} }",
+                    "fixture.sql": "CREATE TABLE users (id INT, name VARCHAR(255));"
+                }
+            }),
+        )
+        .await;
+
+        // Create second worktree with different private files
+        fs.insert_tree(
+            path!("/worktree2"),
+            json!({
+                ".zed": {
+                    "settings.json": r#"{
+                        "file_scan_exclusions": ["**/internal.*"],
+                        "private_files": ["**/private.js", "**/data.json"]
+                    }"#
+                },
+                "lib": {
+                    "public.js": "export function greet() { return 'Hello from worktree2'; }",
+                    "private.js": "const SECRET_TOKEN = \"private_token_2\";",
+                    "data.json": "{\"api_key\": \"json_secret_key\"}"
+                },
+                "docs": {
+                    "README.md": "# Public Documentation",
+                    "internal.md": "# Internal Secrets and Configuration"
+                }
+            }),
+        )
+        .await;
+
+        // Set global settings
+        cx.update(|cx| {
+            SettingsStore::update_global(cx, |store, cx| {
+                store.update_user_settings::<WorktreeSettings>(cx, |settings| {
+                    settings.file_scan_exclusions =
+                        Some(vec!["**/.git".to_string(), "**/node_modules".to_string()]);
+                    settings.private_files = Some(vec!["**/.env".to_string()]);
+                });
+            });
+        });
+
+        let project = Project::test(
+            fs.clone(),
+            [path!("/worktree1").as_ref(), path!("/worktree2").as_ref()],
+            cx,
+        )
+        .await;
+
+        // Wait for worktrees to be fully scanned
+        cx.executor().run_until_parked();
+
+        let tool = Arc::new(ListDirectoryTool::new(project));
+
+        // Test listing worktree1/src - should exclude secret.rs and config.toml based on local settings
+        let input = ListDirectoryToolInput {
+            path: "worktree1/src".into(),
+        };
+        let output = cx
+            .update(|cx| tool.clone().run(input, ToolCallEventStream::test().0, cx))
+            .await
+            .unwrap();
+        assert!(output.contains("main.rs"), "Should list main.rs");
+        assert!(
+            !output.contains("secret.rs"),
+            "Should not list secret.rs (local private_files)"
+        );
+        assert!(
+            !output.contains("config.toml"),
+            "Should not list config.toml (local private_files)"
+        );
+
+        // Test listing worktree1/tests - should exclude fixture.sql based on local settings
+        let input = ListDirectoryToolInput {
+            path: "worktree1/tests".into(),
+        };
+        let output = cx
+            .update(|cx| tool.clone().run(input, ToolCallEventStream::test().0, cx))
+            .await
+            .unwrap();
+        assert!(output.contains("test.rs"), "Should list test.rs");
+        assert!(
+            !output.contains("fixture.sql"),
+            "Should not list fixture.sql (local file_scan_exclusions)"
+        );
+
+        // Test listing worktree2/lib - should exclude private.js and data.json based on local settings
+        let input = ListDirectoryToolInput {
+            path: "worktree2/lib".into(),
+        };
+        let output = cx
+            .update(|cx| tool.clone().run(input, ToolCallEventStream::test().0, cx))
+            .await
+            .unwrap();
+        assert!(output.contains("public.js"), "Should list public.js");
+        assert!(
+            !output.contains("private.js"),
+            "Should not list private.js (local private_files)"
+        );
+        assert!(
+            !output.contains("data.json"),
+            "Should not list data.json (local private_files)"
+        );
+
+        // Test listing worktree2/docs - should exclude internal.md based on local settings
+        let input = ListDirectoryToolInput {
+            path: "worktree2/docs".into(),
+        };
+        let output = cx
+            .update(|cx| tool.clone().run(input, ToolCallEventStream::test().0, cx))
+            .await
+            .unwrap();
+        assert!(output.contains("README.md"), "Should list README.md");
+        assert!(
+            !output.contains("internal.md"),
+            "Should not list internal.md (local file_scan_exclusions)"
+        );
+
+        // Test trying to list an excluded directory directly
+        let input = ListDirectoryToolInput {
+            path: "worktree1/src/secret.rs".into(),
+        };
+        let output = cx
+            .update(|cx| tool.clone().run(input, ToolCallEventStream::test().0, cx))
+            .await;
+        assert!(
+            output
+                .unwrap_err()
+                .to_string()
+                .contains("Cannot list directory"),
+        );
+    }
+}

crates/agent2/src/tools/move_path_tool.rs 🔗

@@ -0,0 +1,120 @@
+use crate::{AgentTool, ToolCallEventStream};
+use agent_client_protocol::ToolKind;
+use anyhow::{Context as _, Result, anyhow};
+use gpui::{App, AppContext, Entity, SharedString, Task};
+use project::Project;
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+use std::{path::Path, sync::Arc};
+use util::markdown::MarkdownInlineCode;
+
+/// Moves or rename a file or directory in the project, and returns confirmation that the move succeeded.
+///
+/// If the source and destination directories are the same, but the filename is different, this performs a rename. Otherwise, it performs a move.
+///
+/// This tool should be used when it's desirable to move or rename a file or directory without changing its contents at all.
+#[derive(Debug, Serialize, Deserialize, JsonSchema)]
+pub struct MovePathToolInput {
+    /// The source path of the file or directory to move/rename.
+    ///
+    /// <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 {
+    project: Entity<Project>,
+}
+
+impl MovePathTool {
+    pub fn new(project: Entity<Project>) -> Self {
+        Self { project }
+    }
+}
+
+impl AgentTool for MovePathTool {
+    type Input = MovePathToolInput;
+    type Output = String;
+
+    fn name(&self) -> SharedString {
+        "move_path".into()
+    }
+
+    fn kind(&self) -> ToolKind {
+        ToolKind::Move
+    }
+
+    fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
+        if let Ok(input) = input {
+            let src = MarkdownInlineCode(&input.source_path);
+            let dest = MarkdownInlineCode(&input.destination_path);
+            let src_path = Path::new(&input.source_path);
+            let dest_path = Path::new(&input.destination_path);
+
+            match dest_path
+                .file_name()
+                .and_then(|os_str| os_str.to_os_string().into_string().ok())
+            {
+                Some(filename) if src_path.parent() == dest_path.parent() => {
+                    let filename = MarkdownInlineCode(&filename);
+                    format!("Rename {src} to {filename}").into()
+                }
+                _ => format!("Move {src} to {dest}").into(),
+            }
+        } else {
+            "Move path".into()
+        }
+    }
+
+    fn run(
+        self: Arc<Self>,
+        input: Self::Input,
+        _event_stream: ToolCallEventStream,
+        cx: &mut App,
+    ) -> Task<Result<Self::Output>> {
+        let rename_task = self.project.update(cx, |project, cx| {
+            match project
+                .find_project_path(&input.source_path, cx)
+                .and_then(|project_path| project.entry_for_path(&project_path, cx))
+            {
+                Some(entity) => match project.find_project_path(&input.destination_path, cx) {
+                    Some(project_path) => project.rename_entry(entity.id, project_path.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
+            ))
+        })
+    }
+}

crates/agent2/src/tools/now_tool.rs 🔗

@@ -0,0 +1,59 @@
+use std::sync::Arc;
+
+use agent_client_protocol as acp;
+use anyhow::Result;
+use chrono::{Local, Utc};
+use gpui::{App, SharedString, Task};
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+
+use crate::{AgentTool, ToolCallEventStream};
+
+#[derive(Debug, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum Timezone {
+    /// Use UTC for the datetime.
+    Utc,
+    /// Use local time for the datetime.
+    Local,
+}
+
+/// Returns the current datetime in RFC 3339 format.
+/// Only use this tool when the user specifically asks for it or the current task would benefit from knowing the current datetime.
+#[derive(Debug, Serialize, Deserialize, JsonSchema)]
+pub struct NowToolInput {
+    /// The timezone to use for the datetime.
+    timezone: Timezone,
+}
+
+pub struct NowTool;
+
+impl AgentTool for NowTool {
+    type Input = NowToolInput;
+    type Output = String;
+
+    fn name(&self) -> SharedString {
+        "now".into()
+    }
+
+    fn kind(&self) -> acp::ToolKind {
+        acp::ToolKind::Other
+    }
+
+    fn initial_title(&self, _input: Result<Self::Input, serde_json::Value>) -> SharedString {
+        "Get current time".into()
+    }
+
+    fn run(
+        self: Arc<Self>,
+        input: Self::Input,
+        _event_stream: ToolCallEventStream,
+        _cx: &mut App,
+    ) -> Task<Result<String>> {
+        let now = match input.timezone {
+            Timezone::Utc => Utc::now().to_rfc3339(),
+            Timezone::Local => Local::now().to_rfc3339(),
+        };
+        Task::ready(Ok(format!("The current datetime is {now}.")))
+    }
+}

crates/agent2/src/tools/open_tool.rs 🔗

@@ -0,0 +1,166 @@
+use crate::AgentTool;
+use agent_client_protocol::ToolKind;
+use anyhow::{Context as _, Result};
+use gpui::{App, AppContext, Entity, SharedString, Task};
+use project::Project;
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+use std::{path::PathBuf, sync::Arc};
+use util::markdown::MarkdownEscaped;
+
+/// This tool opens a file or URL with the default application associated with it on the user's operating system:
+///
+/// - On macOS, it's equivalent to the `open` command
+/// - On Windows, it's equivalent to `start`
+/// - On Linux, it uses something like `xdg-open`, `gio open`, `gnome-open`, `kde-open`, `wslview` as appropriate
+///
+/// For example, it can open a web browser with a URL, open a PDF file with the default PDF viewer, etc.
+///
+/// You MUST ONLY use this tool when the user has explicitly requested opening something. You MUST NEVER assume that the user would like for you to use this tool.
+#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
+pub struct OpenToolInput {
+    /// The path or URL to open with the default application.
+    path_or_url: String,
+}
+
+pub struct OpenTool {
+    project: Entity<Project>,
+}
+
+impl OpenTool {
+    pub fn new(project: Entity<Project>) -> Self {
+        Self { project }
+    }
+}
+
+impl AgentTool for OpenTool {
+    type Input = OpenToolInput;
+    type Output = String;
+
+    fn name(&self) -> SharedString {
+        "open".into()
+    }
+
+    fn kind(&self) -> ToolKind {
+        ToolKind::Execute
+    }
+
+    fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
+        if let Ok(input) = input {
+            format!("Open `{}`", MarkdownEscaped(&input.path_or_url)).into()
+        } else {
+            "Open file or URL".into()
+        }
+    }
+
+    fn run(
+        self: Arc<Self>,
+        input: Self::Input,
+        event_stream: crate::ToolCallEventStream,
+        cx: &mut App,
+    ) -> Task<Result<Self::Output>> {
+        // If path_or_url turns out to be a path in the project, make it absolute.
+        let abs_path = to_absolute_path(&input.path_or_url, self.project.clone(), cx);
+        let authorize = event_stream.authorize(self.initial_title(Ok(input.clone())), cx);
+        cx.background_spawn(async move {
+            authorize.await?;
+
+            match abs_path {
+                Some(path) => open::that(path),
+                None => open::that(&input.path_or_url),
+            }
+            .context("Failed to open URL or file path")?;
+
+            Ok(format!("Successfully opened {}", input.path_or_url))
+        })
+    }
+}
+
+fn to_absolute_path(
+    potential_path: &str,
+    project: Entity<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().to_string();
+
+        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/agent2/src/tools/read_file_tool.rs 🔗

@@ -1,15 +1,16 @@
-use agent_client_protocol::{self as acp};
-use anyhow::{anyhow, Result};
-use assistant_tool::{outline, ActionLog};
-use gpui::{Entity, Task};
+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::{Anchor, Point};
-use project::{AgentLocation, Project, WorktreeSettings};
+use language::Point;
+use language_model::{LanguageModelImage, LanguageModelToolResultContent};
+use project::{AgentLocation, ImageItem, Project, WorktreeSettings, image_store};
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 use settings::Settings;
 use std::sync::Arc;
-use ui::{App, SharedString};
 
 use crate::{AgentTool, ToolCallEventStream};
 
@@ -20,8 +21,7 @@ use crate::{AgentTool, ToolCallEventStream};
 pub struct ReadFileToolInput {
     /// The relative path of the file to read.
     ///
-    /// This path should never be absolute, and the first component
-    /// of the path should always be a root directory in a project.
+    /// This path should never be absolute, and the first component of the path should always be a root directory in a project.
     ///
     /// <example>
     /// If the project has the following root directories:
@@ -33,11 +33,9 @@ pub struct ReadFileToolInput {
     /// 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>,
@@ -59,6 +57,7 @@ impl ReadFileTool {
 
 impl AgentTool for ReadFileTool {
     type Input = ReadFileToolInput;
+    type Output = LanguageModelToolResultContent;
 
     fn name(&self) -> SharedString {
         "read_file".into()
@@ -68,24 +67,28 @@ impl AgentTool for ReadFileTool {
         acp::ToolKind::Read
     }
 
-    fn initial_title(&self, input: Self::Input) -> SharedString {
-        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
-                )
+    fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
+        if let Ok(input) = input {
+            let path = &input.path;
+            match (input.start_line, input.end_line) {
+                (Some(start), Some(end)) => {
+                    format!(
+                        "[Read file `{}` (lines {}-{})](@selection:{}:({}-{}))",
+                        path, start, end, path, start, end
+                    )
+                }
+                (Some(start), None) => {
+                    format!(
+                        "[Read file `{}` (from line {})](@selection:{}:({}-{}))",
+                        path, start, path, start, start
+                    )
+                }
+                _ => format!("[Read file `{}`](@file:{})", path, path),
             }
-            _ => format!("[Read file `{}`](@file:{})", path, path),
+            .into()
+        } else {
+            "Read file".into()
         }
-        .into()
     }
 
     fn run(
@@ -93,7 +96,7 @@ impl AgentTool for ReadFileTool {
         input: Self::Input,
         event_stream: ToolCallEventStream,
         cx: &mut App,
-    ) -> Task<Result<String>> {
+    ) -> Task<Result<LanguageModelToolResultContent>> {
         let Some(project_path) = self.project.read(cx).find_project_path(&input.path, cx) else {
             return Task::ready(Err(anyhow!("Path {} not found in project", &input.path)));
         };
@@ -132,51 +135,27 @@ impl AgentTool for ReadFileTool {
 
         let file_path = input.path.clone();
 
-        event_stream.send_update(acp::ToolCallUpdateFields {
-            locations: Some(vec![acp::ToolCallLocation {
-                path: project_path.path.to_path_buf(),
-                line: input.start_line,
-                // TODO (tracked): use full range
-            }]),
-            ..Default::default()
-        });
-
-        // TODO (tracked): images
-        // if image_store::is_image_file(&self.project, &project_path, cx) {
-        //     let model = &self.thread.read(cx).selected_model;
-
-        //     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();
-        //     }
-
-        //     return cx.spawn(async move |cx| -> Result<ToolResultOutput> {
-        //         let image_entity: Entity<ImageItem> = cx
-        //             .update(|cx| {
-        //                 self.project.update(cx, |project, cx| {
-        //                     project.open_image(project_path.clone(), cx)
-        //                 })
-        //             })?
-        //             .await?;
-
-        //         let image =
-        //             image_entity.read_with(cx, |image_item, _| Arc::clone(&image_item.image))?;
-
-        //         let language_model_image = cx
-        //             .update(|cx| LanguageModelImage::from_image(image, cx))?
-        //             .await
-        //             .context("processing image")?;
-
-        //         Ok(ToolResultOutput {
-        //             content: ToolResultContent::Image(language_model_image),
-        //             output: None,
-        //         })
-        //     });
-        // }
-        //
+        if image_store::is_image_file(&self.project, &project_path, cx) {
+            return cx.spawn(async move |cx| {
+                let image_entity: Entity<ImageItem> = cx
+                    .update(|cx| {
+                        self.project.update(cx, |project, cx| {
+                            project.open_image(project_path.clone(), cx)
+                        })
+                    })?
+                    .await?;
+
+                let image =
+                    image_entity.read_with(cx, |image_item, _| Arc::clone(&image_item.image))?;
+
+                let language_model_image = cx
+                    .update(|cx| LanguageModelImage::from_image(image, cx))?
+                    .await
+                    .context("processing image")?;
+
+                Ok(language_model_image.into())
+            });
+        }
 
         let project = self.project.clone();
         let action_log = self.action_log.clone();
@@ -184,31 +163,24 @@ impl AgentTool for ReadFileTool {
         cx.spawn(async move |cx| {
             let buffer = cx
                 .update(|cx| {
-                    project.update(cx, |project, cx| project.open_buffer(project_path, cx))
+                    project.update(cx, |project, cx| {
+                        project.open_buffer(project_path.clone(), cx)
+                    })
                 })?
                 .await?;
             if buffer.read_with(cx, |buffer, _| {
                 buffer
                     .file()
                     .as_ref()
-                    .map_or(true, |file| !file.disk_state().exists())
+                    .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,
-                );
-            })?;
+            let mut anchor = None;
 
             // Check if specific line ranges are provided
-            if input.start_line.is_some() || input.end_line.is_some() {
-                let mut anchor = None;
+            let result = if input.start_line.is_some() || input.end_line.is_some() {
                 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.
@@ -232,19 +204,7 @@ impl AgentTool for ReadFileTool {
                     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)
+                Ok(result.into())
             } else {
                 // No line ranges specified, so check file size to see if it's too big.
                 let file_size = buffer.read_with(cx, |buffer, _cx| buffer.text().len())?;
@@ -254,15 +214,16 @@ impl AgentTool for ReadFileTool {
                     let result = buffer.read_with(cx, |buffer, _cx| buffer.text())?;
 
                     action_log.update(cx, |log, cx| {
-                        log.buffer_read(buffer, cx);
+                        log.buffer_read(buffer.clone(), cx);
                     })?;
 
-                    Ok(result)
+                    Ok(result.into())
                 } else {
                     // File is too big, so return the outline
                     // and a suggestion to read again with line numbers.
                     let outline =
-                        outline::file_outline(project, file_path, action_log, None, cx).await?;
+                        outline::file_outline(project.clone(), file_path, action_log, None, cx)
+                            .await?;
                     Ok(formatdoc! {"
                         This file was too big to read all at once.
 
@@ -276,20 +237,40 @@ impl AgentTool for ReadFileTool {
 
                         Alternatively, you can fall back to the `grep` tool (if available)
                         to search the file for specific content."
-                    })
+                    }
+                    .into())
                 }
-            }
+            };
+
+            project.update(cx, |project, cx| {
+                if let Some(abs_path) = project.absolute_path(&project_path, cx) {
+                    project.set_agent_location(
+                        Some(AgentLocation {
+                            buffer: buffer.downgrade(),
+                            position: anchor.unwrap_or(text::Anchor::MIN),
+                        }),
+                        cx,
+                    );
+                    event_stream.update_fields(ToolCallUpdateFields {
+                        locations: Some(vec![acp::ToolCallLocation {
+                            path: abs_path,
+                            line: input.start_line.map(|line| line.saturating_sub(1)),
+                        }]),
+                        ..Default::default()
+                    });
+                }
+            })?;
+
+            result
         })
     }
 }
 
 #[cfg(test)]
 mod test {
-    use crate::TestToolCallEventStream;
-
     use super::*;
     use gpui::{AppContext, TestAppContext, UpdateGlobal as _};
-    use language::{tree_sitter_rust, Language, LanguageConfig, LanguageMatcher};
+    use language::{Language, LanguageConfig, LanguageMatcher, tree_sitter_rust};
     use project::{FakeFs, Project};
     use serde_json::json;
     use settings::SettingsStore;
@@ -304,7 +285,7 @@ mod test {
         let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
         let action_log = cx.new(|_| ActionLog::new(project.clone()));
         let tool = Arc::new(ReadFileTool::new(project, action_log));
-        let event_stream = TestToolCallEventStream::new();
+        let (event_stream, _) = ToolCallEventStream::test();
 
         let result = cx
             .update(|cx| {
@@ -313,7 +294,7 @@ mod test {
                     start_line: None,
                     end_line: None,
                 };
-                tool.run(input, event_stream.stream(), cx)
+                tool.run(input, event_stream, cx)
             })
             .await;
         assert_eq!(
@@ -321,6 +302,7 @@ mod test {
             "root/nonexistent_file.txt not found"
         );
     }
+
     #[gpui::test]
     async fn test_read_small_file(cx: &mut TestAppContext) {
         init_test(cx);
@@ -336,7 +318,6 @@ mod test {
         let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
         let action_log = cx.new(|_| ActionLog::new(project.clone()));
         let tool = Arc::new(ReadFileTool::new(project, action_log));
-        let event_stream = TestToolCallEventStream::new();
         let result = cx
             .update(|cx| {
                 let input = ReadFileToolInput {
@@ -344,10 +325,10 @@ mod test {
                     start_line: None,
                     end_line: None,
                 };
-                tool.run(input, event_stream.stream(), cx)
+                tool.run(input, ToolCallEventStream::test().0, cx)
             })
             .await;
-        assert_eq!(result.unwrap(), "This is a small file content");
+        assert_eq!(result.unwrap(), "This is a small file content".into());
     }
 
     #[gpui::test]
@@ -367,18 +348,18 @@ mod test {
         language_registry.add(Arc::new(rust_lang()));
         let action_log = cx.new(|_| ActionLog::new(project.clone()));
         let tool = Arc::new(ReadFileTool::new(project, action_log));
-        let event_stream = TestToolCallEventStream::new();
-        let content = cx
+        let result = cx
             .update(|cx| {
                 let input = ReadFileToolInput {
                     path: "root/large_file.rs".into(),
                     start_line: None,
                     end_line: None,
                 };
-                tool.clone().run(input, event_stream.stream(), cx)
+                tool.clone().run(input, ToolCallEventStream::test().0, cx)
             })
             .await
             .unwrap();
+        let content = result.to_str().unwrap();
 
         assert_eq!(
             content.lines().skip(4).take(6).collect::<Vec<_>>(),
@@ -399,10 +380,11 @@ mod test {
                     start_line: None,
                     end_line: None,
                 };
-                tool.run(input, event_stream.stream(), cx)
+                tool.run(input, ToolCallEventStream::test().0, cx)
             })
-            .await;
-        let content = result.unwrap();
+            .await
+            .unwrap();
+        let content = result.to_str().unwrap();
         let expected_content = (0..1000)
             .flat_map(|i| {
                 vec![
@@ -438,7 +420,6 @@ mod test {
 
         let action_log = cx.new(|_| ActionLog::new(project.clone()));
         let tool = Arc::new(ReadFileTool::new(project, action_log));
-        let event_stream = TestToolCallEventStream::new();
         let result = cx
             .update(|cx| {
                 let input = ReadFileToolInput {
@@ -446,10 +427,10 @@ mod test {
                     start_line: Some(2),
                     end_line: Some(4),
                 };
-                tool.run(input, event_stream.stream(), cx)
+                tool.run(input, ToolCallEventStream::test().0, cx)
             })
             .await;
-        assert_eq!(result.unwrap(), "Line 2\nLine 3\nLine 4");
+        assert_eq!(result.unwrap(), "Line 2\nLine 3\nLine 4".into());
     }
 
     #[gpui::test]
@@ -467,7 +448,6 @@ mod test {
         let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
         let action_log = cx.new(|_| ActionLog::new(project.clone()));
         let tool = Arc::new(ReadFileTool::new(project, action_log));
-        let event_stream = TestToolCallEventStream::new();
 
         // start_line of 0 should be treated as 1
         let result = cx
@@ -477,10 +457,10 @@ mod test {
                     start_line: Some(0),
                     end_line: Some(2),
                 };
-                tool.clone().run(input, event_stream.stream(), cx)
+                tool.clone().run(input, ToolCallEventStream::test().0, cx)
             })
             .await;
-        assert_eq!(result.unwrap(), "Line 1\nLine 2");
+        assert_eq!(result.unwrap(), "Line 1\nLine 2".into());
 
         // end_line of 0 should result in at least 1 line
         let result = cx
@@ -490,10 +470,10 @@ mod test {
                     start_line: Some(1),
                     end_line: Some(0),
                 };
-                tool.clone().run(input, event_stream.stream(), cx)
+                tool.clone().run(input, ToolCallEventStream::test().0, cx)
             })
             .await;
-        assert_eq!(result.unwrap(), "Line 1");
+        assert_eq!(result.unwrap(), "Line 1".into());
 
         // when start_line > end_line, should still return at least 1 line
         let result = cx
@@ -503,10 +483,10 @@ mod test {
                     start_line: Some(3),
                     end_line: Some(2),
                 };
-                tool.clone().run(input, event_stream.stream(), cx)
+                tool.clone().run(input, ToolCallEventStream::test().0, cx)
             })
             .await;
-        assert_eq!(result.unwrap(), "Line 3");
+        assert_eq!(result.unwrap(), "Line 3".into());
     }
 
     fn init_test(cx: &mut TestAppContext) {
@@ -612,7 +592,6 @@ mod test {
         let project = Project::test(fs.clone(), [path!("/project_root").as_ref()], cx).await;
         let action_log = cx.new(|_| ActionLog::new(project.clone()));
         let tool = Arc::new(ReadFileTool::new(project, action_log));
-        let event_stream = TestToolCallEventStream::new();
 
         // Reading a file outside the project worktree should fail
         let result = cx
@@ -622,7 +601,7 @@ mod test {
                     start_line: None,
                     end_line: None,
                 };
-                tool.clone().run(input, event_stream.stream(), cx)
+                tool.clone().run(input, ToolCallEventStream::test().0, cx)
             })
             .await;
         assert!(
@@ -638,7 +617,7 @@ mod test {
                     start_line: None,
                     end_line: None,
                 };
-                tool.clone().run(input, event_stream.stream(), cx)
+                tool.clone().run(input, ToolCallEventStream::test().0, cx)
             })
             .await;
         assert!(
@@ -654,7 +633,7 @@ mod test {
                     start_line: None,
                     end_line: None,
                 };
-                tool.clone().run(input, event_stream.stream(), cx)
+                tool.clone().run(input, ToolCallEventStream::test().0, cx)
             })
             .await;
         assert!(
@@ -669,7 +648,7 @@ mod test {
                     start_line: None,
                     end_line: None,
                 };
-                tool.clone().run(input, event_stream.stream(), cx)
+                tool.clone().run(input, ToolCallEventStream::test().0, cx)
             })
             .await;
         assert!(
@@ -685,7 +664,7 @@ mod test {
                     start_line: None,
                     end_line: None,
                 };
-                tool.clone().run(input, event_stream.stream(), cx)
+                tool.clone().run(input, ToolCallEventStream::test().0, cx)
             })
             .await;
         assert!(
@@ -700,7 +679,7 @@ mod test {
                     start_line: None,
                     end_line: None,
                 };
-                tool.clone().run(input, event_stream.stream(), cx)
+                tool.clone().run(input, ToolCallEventStream::test().0, cx)
             })
             .await;
         assert!(
@@ -715,7 +694,7 @@ mod test {
                     start_line: None,
                     end_line: None,
                 };
-                tool.clone().run(input, event_stream.stream(), cx)
+                tool.clone().run(input, ToolCallEventStream::test().0, cx)
             })
             .await;
         assert!(
@@ -731,11 +710,11 @@ mod test {
                     start_line: None,
                     end_line: None,
                 };
-                tool.clone().run(input, event_stream.stream(), cx)
+                tool.clone().run(input, ToolCallEventStream::test().0, cx)
             })
             .await;
         assert!(result.is_ok(), "Should be able to read normal files");
-        assert_eq!(result.unwrap(), "Normal file content");
+        assert_eq!(result.unwrap(), "Normal file content".into());
 
         // Path traversal attempts with .. should fail
         let result = cx
@@ -745,7 +724,7 @@ mod test {
                     start_line: None,
                     end_line: None,
                 };
-                tool.run(input, event_stream.stream(), cx)
+                tool.run(input, ToolCallEventStream::test().0, cx)
             })
             .await;
         assert!(
@@ -826,7 +805,6 @@ mod test {
 
         let action_log = cx.new(|_| ActionLog::new(project.clone()));
         let tool = Arc::new(ReadFileTool::new(project.clone(), action_log.clone()));
-        let event_stream = TestToolCallEventStream::new();
 
         // Test reading allowed files in worktree1
         let result = cx
@@ -836,12 +814,15 @@ mod test {
                     start_line: None,
                     end_line: None,
                 };
-                tool.clone().run(input, event_stream.stream(), cx)
+                tool.clone().run(input, ToolCallEventStream::test().0, cx)
             })
             .await
             .unwrap();
 
-        assert_eq!(result, "fn main() { println!(\"Hello from worktree1\"); }");
+        assert_eq!(
+            result,
+            "fn main() { println!(\"Hello from worktree1\"); }".into()
+        );
 
         // Test reading private file in worktree1 should fail
         let result = cx
@@ -851,7 +832,7 @@ mod test {
                     start_line: None,
                     end_line: None,
                 };
-                tool.clone().run(input, event_stream.stream(), cx)
+                tool.clone().run(input, ToolCallEventStream::test().0, cx)
             })
             .await;
 
@@ -872,7 +853,7 @@ mod test {
                     start_line: None,
                     end_line: None,
                 };
-                tool.clone().run(input, event_stream.stream(), cx)
+                tool.clone().run(input, ToolCallEventStream::test().0, cx)
             })
             .await;
 
@@ -893,14 +874,14 @@ mod test {
                     start_line: None,
                     end_line: None,
                 };
-                tool.clone().run(input, event_stream.stream(), cx)
+                tool.clone().run(input, ToolCallEventStream::test().0, cx)
             })
             .await
             .unwrap();
 
         assert_eq!(
             result,
-            "export function greet() { return 'Hello from worktree2'; }"
+            "export function greet() { return 'Hello from worktree2'; }".into()
         );
 
         // Test reading private file in worktree2 should fail
@@ -911,7 +892,7 @@ mod test {
                     start_line: None,
                     end_line: None,
                 };
-                tool.clone().run(input, event_stream.stream(), cx)
+                tool.clone().run(input, ToolCallEventStream::test().0, cx)
             })
             .await;
 
@@ -932,7 +913,7 @@ mod test {
                     start_line: None,
                     end_line: None,
                 };
-                tool.clone().run(input, event_stream.stream(), cx)
+                tool.clone().run(input, ToolCallEventStream::test().0, cx)
             })
             .await;
 
@@ -954,7 +935,7 @@ mod test {
                     start_line: None,
                     end_line: None,
                 };
-                tool.clone().run(input, event_stream.stream(), cx)
+                tool.clone().run(input, ToolCallEventStream::test().0, cx)
             })
             .await;
 

crates/agent2/src/tools/terminal_tool.rs 🔗

@@ -0,0 +1,468 @@
+use agent_client_protocol as acp;
+use anyhow::Result;
+use futures::{FutureExt as _, future::Shared};
+use gpui::{App, AppContext, Entity, SharedString, Task};
+use project::{Project, terminals::TerminalKind};
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+use std::{
+    path::{Path, PathBuf},
+    sync::Arc,
+};
+use util::{ResultExt, get_system_shell, markdown::MarkdownInlineCode};
+
+use crate::{AgentTool, ToolCallEventStream};
+
+const COMMAND_OUTPUT_LIMIT: usize = 16 * 1024;
+
+/// Executes a shell one-liner and returns the combined output.
+///
+/// This tool spawns a process using the user's shell, reads from stdout and stderr (preserving the order of writes), and returns a string with the combined output result.
+///
+/// The output results will be shown to the user already, only list it again if necessary, avoid being redundant.
+///
+/// Make sure you use the `cd` parameter to navigate to one of the root directories of the project. NEVER do it as part of the `command` itself, otherwise it will error.
+///
+/// Do not use this tool for commands that run indefinitely, such as servers (like `npm run start`, `npm run dev`, `python -m http.server`, etc) or file watchers that don't terminate on their own.
+///
+/// Remember that each invocation of this tool will spawn a new shell process, so you can't rely on any state from previous invocations.
+#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
+pub struct TerminalToolInput {
+    /// The one-liner command to execute.
+    command: String,
+    /// Working directory for the command. This must be one of the root directories of the project.
+    cd: String,
+}
+
+pub struct TerminalTool {
+    project: Entity<Project>,
+    determine_shell: Shared<Task<String>>,
+}
+
+impl TerminalTool {
+    pub fn new(project: Entity<Project>, cx: &mut App) -> Self {
+        let determine_shell = cx.background_spawn(async move {
+            if cfg!(windows) {
+                return get_system_shell();
+            }
+
+            if which::which("bash").is_ok() {
+                "bash".into()
+            } else {
+                get_system_shell()
+            }
+        });
+        Self {
+            project,
+            determine_shell: determine_shell.shared(),
+        }
+    }
+}
+
+impl AgentTool for TerminalTool {
+    type Input = TerminalToolInput;
+    type Output = String;
+
+    fn name(&self) -> SharedString {
+        "terminal".into()
+    }
+
+    fn kind(&self) -> acp::ToolKind {
+        acp::ToolKind::Execute
+    }
+
+    fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
+        if let Ok(input) = input {
+            let mut lines = input.command.lines();
+            let first_line = lines.next().unwrap_or_default();
+            let remaining_line_count = lines.count();
+            match remaining_line_count {
+                0 => MarkdownInlineCode(first_line).to_string().into(),
+                1 => MarkdownInlineCode(&format!(
+                    "{} - {} more line",
+                    first_line, remaining_line_count
+                ))
+                .to_string()
+                .into(),
+                n => MarkdownInlineCode(&format!("{} - {} more lines", first_line, n))
+                    .to_string()
+                    .into(),
+            }
+        } else {
+            "Run terminal command".into()
+        }
+    }
+
+    fn run(
+        self: Arc<Self>,
+        input: Self::Input,
+        event_stream: ToolCallEventStream,
+        cx: &mut App,
+    ) -> Task<Result<Self::Output>> {
+        let language_registry = self.project.read(cx).languages().clone();
+        let working_dir = match working_dir(&input, &self.project, cx) {
+            Ok(dir) => dir,
+            Err(err) => return Task::ready(Err(err)),
+        };
+        let program = self.determine_shell.clone();
+        let command = if cfg!(windows) {
+            format!("$null | & {{{}}}", input.command.replace("\"", "'"))
+        } else if let Some(cwd) = working_dir
+            .as_ref()
+            .and_then(|cwd| cwd.as_os_str().to_str())
+        {
+            // Make sure once we're *inside* the shell, we cd into `cwd`
+            format!("(cd {cwd}; {}) </dev/null", input.command)
+        } else {
+            format!("({}) </dev/null", input.command)
+        };
+        let args = vec!["-c".into(), command];
+
+        let env = match &working_dir {
+            Some(dir) => self.project.update(cx, |project, cx| {
+                project.directory_environment(dir.as_path().into(), cx)
+            }),
+            None => Task::ready(None).shared(),
+        };
+
+        let env = cx.spawn(async move |_| {
+            let mut env = env.await.unwrap_or_default();
+            if cfg!(unix) {
+                env.insert("PAGER".into(), "cat".into());
+            }
+            env
+        });
+
+        let authorize = event_stream.authorize(self.initial_title(Ok(input.clone())), cx);
+
+        cx.spawn({
+            async move |cx| {
+                authorize.await?;
+
+                let program = program.await;
+                let env = env.await;
+                let terminal = self
+                    .project
+                    .update(cx, |project, cx| {
+                        project.create_terminal(
+                            TerminalKind::Task(task::SpawnInTerminal {
+                                command: Some(program),
+                                args,
+                                cwd: working_dir.clone(),
+                                env,
+                                ..Default::default()
+                            }),
+                            cx,
+                        )
+                    })?
+                    .await?;
+                let acp_terminal = cx.new(|cx| {
+                    acp_thread::Terminal::new(
+                        input.command.clone(),
+                        working_dir.clone(),
+                        terminal.clone(),
+                        language_registry,
+                        cx,
+                    )
+                })?;
+                event_stream.update_terminal(acp_terminal.clone());
+
+                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 (processed_content, finished_with_empty_output) = process_content(
+                    &content,
+                    &input.command,
+                    exit_status.map(portable_pty::ExitStatus::from),
+                );
+
+                acp_terminal
+                    .update(cx, |terminal, cx| {
+                        terminal.finish(
+                            exit_status,
+                            content.len(),
+                            processed_content.len(),
+                            content_line_count,
+                            finished_with_empty_output,
+                            cx,
+                        );
+                    })
+                    .log_err();
+
+                Ok(processed_content)
+            }
+        })
+    }
+}
+
+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.");
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use agent_settings::AgentSettings;
+    use editor::EditorSettings;
+    use fs::RealFs;
+    use gpui::{BackgroundExecutor, TestAppContext};
+    use pretty_assertions::assert_eq;
+    use serde_json::json;
+    use settings::{Settings, SettingsStore};
+    use terminal::terminal_settings::TerminalSettings;
+    use theme::ThemeSettings;
+    use util::test::TempTree;
+
+    use crate::ThreadEvent;
+
+    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);
+            ThemeSettings::register(cx);
+            TerminalSettings::register(cx);
+            EditorSettings::register(cx);
+            AgentSettings::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 input = TerminalToolInput {
+            command: "cat".to_owned(),
+            cd: tree
+                .path()
+                .join("project")
+                .as_path()
+                .to_string_lossy()
+                .to_string(),
+        };
+        let (event_stream_tx, mut event_stream_rx) = ToolCallEventStream::test();
+        let result = cx
+            .update(|cx| Arc::new(TerminalTool::new(project, cx)).run(input, event_stream_tx, cx));
+
+        let auth = event_stream_rx.expect_authorization().await;
+        auth.response.send(auth.options[0].id.clone()).unwrap();
+        event_stream_rx.expect_terminal().await;
+        assert_eq!(result.await.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 check = |input, expected, cx: &mut TestAppContext| {
+            let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
+            let result = cx.update(|cx| {
+                Arc::new(TerminalTool::new(project.clone(), cx)).run(input, stream_tx, cx)
+            });
+            cx.run_until_parked();
+            let event = stream_rx.try_next();
+            if let Ok(Some(Ok(ThreadEvent::ToolCallAuthorization(auth)))) = event {
+                auth.response.send(auth.options[0].id.clone()).unwrap();
+            }
+
+            cx.spawn(async move |_| {
+                let output = result.await;
+                assert_eq!(output.ok(), expected);
+            })
+        };
+
+        check(
+            TerminalToolInput {
+                command: "pwd".into(),
+                cd: ".".into(),
+            },
+            Some(format!(
+                "```\n{}\n```",
+                tree.path().join("project").display()
+            )),
+            cx,
+        )
+        .await;
+
+        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
+        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();
+
+        check(
+            TerminalToolInput {
+                command: "pwd".into(),
+                cd: "other-project".into(),
+            },
+            Some(format!(
+                "```\n{}\n```",
+                tree.path().join("other-project").display()
+            )),
+            cx,
+        )
+        .await;
+
+        check(
+            TerminalToolInput {
+                command: "pwd".into(),
+                cd: ".".into(),
+            },
+            None,
+            cx,
+        )
+        .await;
+    }
+}

crates/agent2/src/tools/thinking_tool.rs 🔗

@@ -11,8 +11,7 @@ use crate::{AgentTool, ToolCallEventStream};
 /// Use this tool when you need to work through complex problems, develop strategies, or outline approaches before taking action.
 #[derive(Debug, Serialize, Deserialize, JsonSchema)]
 pub struct ThinkingToolInput {
-    /// Content to think about. This should be a description of what to think about or
-    /// a problem to solve.
+    /// Content to think about. This should be a description of what to think about or a problem to solve.
     content: String,
 }
 
@@ -20,6 +19,7 @@ pub struct ThinkingTool;
 
 impl AgentTool for ThinkingTool {
     type Input = ThinkingToolInput;
+    type Output = String;
 
     fn name(&self) -> SharedString {
         "thinking".into()
@@ -29,7 +29,7 @@ impl AgentTool for ThinkingTool {
         acp::ToolKind::Think
     }
 
-    fn initial_title(&self, _input: Self::Input) -> SharedString {
+    fn initial_title(&self, _input: Result<Self::Input, serde_json::Value>) -> SharedString {
         "Thinking".into()
     }
 
@@ -39,7 +39,7 @@ impl AgentTool for ThinkingTool {
         event_stream: ToolCallEventStream,
         _cx: &mut App,
     ) -> Task<Result<String>> {
-        event_stream.send_update(acp::ToolCallUpdateFields {
+        event_stream.update_fields(acp::ToolCallUpdateFields {
             content: Some(vec![input.content.into()]),
             ..Default::default()
         });

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

@@ -0,0 +1,127 @@
+use std::sync::Arc;
+
+use crate::{AgentTool, ToolCallEventStream};
+use agent_client_protocol as acp;
+use anyhow::{Result, anyhow};
+use cloud_llm_client::WebSearchResponse;
+use gpui::{App, AppContext, Task};
+use language_model::{
+    LanguageModelProviderId, LanguageModelToolResultContent, ZED_CLOUD_PROVIDER_ID,
+};
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+use ui::prelude::*;
+use web_search::WebSearchRegistry;
+
+/// Search the web for information using your query.
+/// Use this when you need real-time information, facts, or data that might not be in your training.
+/// Results will include snippets and links from relevant web pages.
+#[derive(Debug, Serialize, Deserialize, JsonSchema)]
+pub struct WebSearchToolInput {
+    /// The search term or question to query on the web.
+    query: String,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(transparent)]
+pub struct WebSearchToolOutput(WebSearchResponse);
+
+impl From<WebSearchToolOutput> for LanguageModelToolResultContent {
+    fn from(value: WebSearchToolOutput) -> Self {
+        serde_json::to_string(&value.0)
+            .expect("Failed to serialize WebSearchResponse")
+            .into()
+    }
+}
+
+pub struct WebSearchTool;
+
+impl AgentTool for WebSearchTool {
+    type Input = WebSearchToolInput;
+    type Output = WebSearchToolOutput;
+
+    fn name(&self) -> SharedString {
+        "web_search".into()
+    }
+
+    fn kind(&self) -> acp::ToolKind {
+        acp::ToolKind::Fetch
+    }
+
+    fn initial_title(&self, _input: Result<Self::Input, serde_json::Value>) -> SharedString {
+        "Searching the Web".into()
+    }
+
+    /// We currently only support Zed Cloud as a provider.
+    fn supported_provider(&self, provider: &LanguageModelProviderId) -> bool {
+        provider == &ZED_CLOUD_PROVIDER_ID
+    }
+
+    fn run(
+        self: Arc<Self>,
+        input: Self::Input,
+        event_stream: ToolCallEventStream,
+        cx: &mut App,
+    ) -> Task<Result<Self::Output>> {
+        let Some(provider) = WebSearchRegistry::read_global(cx).active_provider() else {
+            return Task::ready(Err(anyhow!("Web search is not available.")));
+        };
+
+        let search_task = provider.search(input.query, cx);
+        cx.background_spawn(async move {
+            let response = match search_task.await {
+                Ok(response) => response,
+                Err(err) => {
+                    event_stream.update_fields(acp::ToolCallUpdateFields {
+                        title: Some("Web Search Failed".to_string()),
+                        ..Default::default()
+                    });
+                    return Err(err);
+                }
+            };
+
+            emit_update(&response, &event_stream);
+            Ok(WebSearchToolOutput(response))
+        })
+    }
+
+    fn replay(
+        &self,
+        _input: Self::Input,
+        output: Self::Output,
+        event_stream: ToolCallEventStream,
+        _cx: &mut App,
+    ) -> Result<()> {
+        emit_update(&output.0, &event_stream);
+        Ok(())
+    }
+}
+
+fn emit_update(response: &WebSearchResponse, event_stream: &ToolCallEventStream) {
+    let result_text = if response.results.len() == 1 {
+        "1 result".to_string()
+    } else {
+        format!("{} results", response.results.len())
+    };
+    event_stream.update_fields(acp::ToolCallUpdateFields {
+        title: Some(format!("Searched the web: {result_text}")),
+        content: Some(
+            response
+                .results
+                .iter()
+                .map(|result| acp::ToolCallContent::Content {
+                    content: acp::ContentBlock::ResourceLink(acp::ResourceLink {
+                        name: result.title.clone(),
+                        uri: result.url.clone(),
+                        title: Some(result.title.clone()),
+                        description: Some(result.text.clone()),
+                        mime_type: None,
+                        annotations: None,
+                        size: None,
+                    }),
+                })
+                .collect(),
+        ),
+        ..Default::default()
+    });
+}

crates/agent_servers/Cargo.toml 🔗

@@ -6,7 +6,7 @@ publish.workspace = true
 license = "GPL-3.0-or-later"
 
 [features]
-test-support = ["acp_thread/test-support", "gpui/test-support", "project/test-support"]
+test-support = ["acp_thread/test-support", "gpui/test-support", "project/test-support", "dep:env_logger", "fs", "client/test-support", "dep:gpui_tokio", "reqwest_client/test-support"]
 e2e = []
 
 [lints]
@@ -18,20 +18,31 @@ doctest = false
 
 [dependencies]
 acp_thread.workspace = true
+action_log.workspace = true
 agent-client-protocol.workspace = true
+agent_settings.workspace = true
 agentic-coding-protocol.workspace = true
 anyhow.workspace = true
+client = { workspace = true, optional = true }
 collections.workspace = true
 context_server.workspace = true
+env_logger = { workspace = true, optional = true }
+fs = { workspace = true, optional = true }
 futures.workspace = true
 gpui.workspace = true
+gpui_tokio = { workspace = true, optional = true }
 indoc.workspace = true
 itertools.workspace = true
+language.workspace = true
+language_model.workspace = true
+language_models.workspace = true
 log.workspace = true
 paths.workspace = true
 project.workspace = true
 rand.workspace = true
+reqwest_client = { workspace = true, optional = true }
 schemars.workspace = true
+semver.workspace = true
 serde.workspace = true
 serde_json.workspace = true
 settings.workspace = true
@@ -51,8 +62,12 @@ libc.workspace = true
 nix.workspace = true
 
 [dev-dependencies]
+client = { workspace = true, features = ["test-support"] }
 env_logger.workspace = true
+fs.workspace = true
 language.workspace = true
 indoc.workspace = true
 acp_thread = { workspace = true, features = ["test-support"] }
 gpui = { workspace = true, features = ["test-support"] }
+gpui_tokio.workspace = true
+reqwest_client = { workspace = true, features = ["test-support"] }

crates/agent_servers/src/acp.rs 🔗

@@ -19,14 +19,14 @@ pub async fn connect(
     root_dir: &Path,
     cx: &mut AsyncApp,
 ) -> Result<Rc<dyn AgentConnection>> {
-    let conn = v1::AcpConnection::stdio(server_name, command.clone(), &root_dir, cx).await;
+    let conn = v1::AcpConnection::stdio(server_name, command.clone(), root_dir, cx).await;
 
     match conn {
         Ok(conn) => Ok(Rc::new(conn) as _),
         Err(err) if err.is::<UnsupportedVersion>() => {
             // Consider re-using initialize response and subprocess when adding another version here
             let conn: Rc<dyn AgentConnection> =
-                Rc::new(v0::AcpConnection::stdio(server_name, command, &root_dir, cx).await?);
+                Rc::new(v0::AcpConnection::stdio(server_name, command, root_dir, cx).await?);
             Ok(conn)
         }
         Err(err) => Err(err),

crates/agent_servers/src/acp/v0.rs 🔗

@@ -1,11 +1,12 @@
 // Translates old acp agents into the new schema
+use action_log::ActionLog;
 use agent_client_protocol as acp;
 use agentic_coding_protocol::{self as acp_old, AgentRequest as _};
 use anyhow::{Context as _, Result, anyhow};
 use futures::channel::oneshot;
 use gpui::{AppContext as _, AsyncApp, Entity, Task, WeakEntity};
 use project::Project;
-use std::{cell::RefCell, path::Path, rc::Rc};
+use std::{any::Any, cell::RefCell, path::Path, rc::Rc};
 use ui::App;
 use util::ResultExt as _;
 
@@ -135,9 +136,9 @@ impl acp_old::Client for OldAcpClientDelegate {
         let response = cx
             .update(|cx| {
                 self.thread.borrow().update(cx, |thread, cx| {
-                    thread.request_tool_call_authorization(tool_call, acp_options, cx)
+                    thread.request_tool_call_authorization(tool_call.into(), acp_options, cx)
                 })
-            })?
+            })??
             .context("Failed to update thread")?
             .await;
 
@@ -148,7 +149,7 @@ impl acp_old::Client for OldAcpClientDelegate {
 
         Ok(acp_old::RequestToolCallConfirmationResponse {
             id: acp_old::ToolCallId(old_acp_id),
-            outcome: outcome,
+            outcome,
         })
     }
 
@@ -168,7 +169,7 @@ impl acp_old::Client for OldAcpClientDelegate {
                     cx,
                 )
             })
-        })?
+        })??
         .context("Failed to update thread")?;
 
         Ok(acp_old::PushToolCallResponse {
@@ -265,7 +266,7 @@ impl acp_old::Client for OldAcpClientDelegate {
 
 fn into_new_tool_call(id: acp::ToolCallId, request: acp_old::PushToolCallParams) -> acp::ToolCall {
     acp::ToolCall {
-        id: id,
+        id,
         title: request.label,
         kind: acp_kind_from_old_icon(request.icon),
         status: acp::ToolCallStatus::InProgress,
@@ -423,7 +424,7 @@ impl AgentConnection for AcpConnection {
         self: Rc<Self>,
         project: Entity<Project>,
         _cwd: &Path,
-        cx: &mut AsyncApp,
+        cx: &mut App,
     ) -> Task<Result<Entity<AcpThread>>> {
         let task = self.connection.request_any(
             acp_old::InitializeParams {
@@ -437,13 +438,14 @@ impl AgentConnection for AcpConnection {
             let result = acp_old::InitializeParams::response_from_any(result)?;
 
             if !result.is_authenticated {
-                anyhow::bail!(AuthRequired)
+                anyhow::bail!(AuthRequired::new())
             }
 
             cx.update(|cx| {
                 let thread = cx.new(|cx| {
                     let session_id = acp::SessionId("acp-old-no-id".into());
-                    AcpThread::new(self.name, self.clone(), project, session_id, cx)
+                    let action_log = cx.new(|_| ActionLog::new(project.clone()));
+                    AcpThread::new(self.name, self.clone(), project, action_log, session_id)
                 });
                 current_thread.replace(thread.downgrade());
                 thread
@@ -467,6 +469,7 @@ impl AgentConnection for AcpConnection {
 
     fn prompt(
         &self,
+        _id: Option<acp_thread::UserMessageId>,
         params: acp::PromptRequest,
         cx: &mut App,
     ) -> Task<Result<acp::PromptResponse>> {
@@ -495,6 +498,14 @@ impl AgentConnection for AcpConnection {
         })
     }
 
+    fn prompt_capabilities(&self) -> acp::PromptCapabilities {
+        acp::PromptCapabilities {
+            image: false,
+            audio: false,
+            embedded_context: false,
+        }
+    }
+
     fn cancel(&self, _session_id: &acp::SessionId, cx: &mut App) {
         let task = self
             .connection
@@ -506,4 +517,8 @@ impl AgentConnection for AcpConnection {
             })
             .detach_and_log_err(cx)
     }
+
+    fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
+        self
+    }
 }

crates/agent_servers/src/acp/v1.rs 🔗

@@ -1,28 +1,34 @@
-use agent_client_protocol::{self as acp, Agent as _};
+use action_log::ActionLog;
+use agent_client_protocol::{self as acp, Agent as _, ErrorCode};
 use anyhow::anyhow;
 use collections::HashMap;
+use futures::AsyncBufReadExt as _;
 use futures::channel::oneshot;
+use futures::io::BufReader;
 use project::Project;
-use std::cell::RefCell;
+use serde::Deserialize;
 use std::path::Path;
 use std::rc::Rc;
+use std::{any::Any, cell::RefCell};
 
 use anyhow::{Context as _, Result};
 use gpui::{App, AppContext as _, AsyncApp, Entity, Task, WeakEntity};
 
 use crate::{AgentServerCommand, acp::UnsupportedVersion};
-use acp_thread::{AcpThread, AgentConnection, AuthRequired};
+use acp_thread::{AcpThread, AgentConnection, AuthRequired, LoadError};
 
 pub struct AcpConnection {
     server_name: &'static str,
     connection: Rc<acp::ClientSideConnection>,
     sessions: Rc<RefCell<HashMap<acp::SessionId, AcpSession>>>,
     auth_methods: Vec<acp::AuthMethod>,
+    prompt_capabilities: acp::PromptCapabilities,
     _io_task: Task<Result<()>>,
 }
 
 pub struct AcpSession {
     thread: WeakEntity<AcpThread>,
+    suppress_abort_err: bool,
 }
 
 const MINIMUM_SUPPORTED_VERSION: acp::ProtocolVersion = acp::V1;
@@ -40,12 +46,13 @@ impl AcpConnection {
             .current_dir(root_dir)
             .stdin(std::process::Stdio::piped())
             .stdout(std::process::Stdio::piped())
-            .stderr(std::process::Stdio::inherit())
+            .stderr(std::process::Stdio::piped())
             .kill_on_drop(true)
             .spawn()?;
 
-        let stdout = child.stdout.take().expect("Failed to take stdout");
-        let stdin = child.stdin.take().expect("Failed to take stdin");
+        let stdout = child.stdout.take().context("Failed to take stdout")?;
+        let stdin = child.stdin.take().context("Failed to take stdin")?;
+        let stderr = child.stderr.take().context("Failed to take stderr")?;
         log::trace!("Spawned (pid: {})", child.id());
 
         let sessions = Rc::new(RefCell::new(HashMap::default()));
@@ -63,6 +70,18 @@ impl AcpConnection {
 
         let io_task = cx.background_spawn(io_task);
 
+        cx.background_spawn(async move {
+            let mut stderr = BufReader::new(stderr);
+            let mut line = String::new();
+            while let Ok(n) = stderr.read_line(&mut line).await
+                && n > 0
+            {
+                log::warn!("agent stderr: {}", &line);
+                line.clear();
+            }
+        })
+        .detach();
+
         cx.spawn({
             let sessions = sessions.clone();
             async move |cx| {
@@ -71,7 +90,9 @@ impl AcpConnection {
                 for session in sessions.borrow().values() {
                     session
                         .thread
-                        .update(cx, |thread, cx| thread.emit_server_exited(status, cx))
+                        .update(cx, |thread, cx| {
+                            thread.emit_load_error(LoadError::Exited { status }, cx)
+                        })
                         .ok();
                 }
 
@@ -101,6 +122,7 @@ impl AcpConnection {
             connection: connection.into(),
             server_name,
             sessions,
+            prompt_capabilities: response.agent_capabilities.prompt_capabilities,
             _io_task: io_task,
         })
     }
@@ -111,7 +133,7 @@ impl AgentConnection for AcpConnection {
         self: Rc<Self>,
         project: Entity<Project>,
         cwd: &Path,
-        cx: &mut AsyncApp,
+        cx: &mut App,
     ) -> Task<Result<Entity<AcpThread>>> {
         let conn = self.connection.clone();
         let sessions = self.sessions.clone();
@@ -125,26 +147,33 @@ impl AgentConnection for AcpConnection {
                 .await
                 .map_err(|err| {
                     if err.code == acp::ErrorCode::AUTH_REQUIRED.code {
-                        anyhow!(AuthRequired)
+                        let mut error = AuthRequired::new();
+
+                        if err.message != acp::ErrorCode::AUTH_REQUIRED.message {
+                            error = error.with_description(err.message);
+                        }
+
+                        anyhow!(error)
                     } else {
                         anyhow!(err)
                     }
                 })?;
 
             let session_id = response.session_id;
-
-            let thread = cx.new(|cx| {
+            let action_log = cx.new(|_| ActionLog::new(project.clone()))?;
+            let thread = cx.new(|_cx| {
                 AcpThread::new(
                     self.server_name,
                     self.clone(),
                     project,
+                    action_log,
                     session_id.clone(),
-                    cx,
                 )
             })?;
 
             let session = AcpSession {
                 thread: thread.downgrade(),
+                suppress_abort_err: false,
             };
             sessions.borrow_mut().insert(session_id, session);
 
@@ -171,17 +200,69 @@ impl AgentConnection for AcpConnection {
 
     fn prompt(
         &self,
+        _id: Option<acp_thread::UserMessageId>,
         params: acp::PromptRequest,
         cx: &mut App,
     ) -> Task<Result<acp::PromptResponse>> {
         let conn = self.connection.clone();
+        let sessions = self.sessions.clone();
+        let session_id = params.session_id.clone();
         cx.foreground_executor().spawn(async move {
-            let response = conn.prompt(params).await?;
-            Ok(response)
+            let result = conn.prompt(params).await;
+
+            let mut suppress_abort_err = false;
+
+            if let Some(session) = sessions.borrow_mut().get_mut(&session_id) {
+                suppress_abort_err = session.suppress_abort_err;
+                session.suppress_abort_err = false;
+            }
+
+            match result {
+                Ok(response) => Ok(response),
+                Err(err) => {
+                    if err.code != ErrorCode::INTERNAL_ERROR.code {
+                        anyhow::bail!(err)
+                    }
+
+                    let Some(data) = &err.data else {
+                        anyhow::bail!(err)
+                    };
+
+                    // Temporary workaround until the following PR is generally available:
+                    // https://github.com/google-gemini/gemini-cli/pull/6656
+
+                    #[derive(Deserialize)]
+                    #[serde(deny_unknown_fields)]
+                    struct ErrorDetails {
+                        details: Box<str>,
+                    }
+
+                    match serde_json::from_value(data.clone()) {
+                        Ok(ErrorDetails { details }) => {
+                            if suppress_abort_err && details.contains("This operation was aborted")
+                            {
+                                Ok(acp::PromptResponse {
+                                    stop_reason: acp::StopReason::Cancelled,
+                                })
+                            } else {
+                                Err(anyhow!(details))
+                            }
+                        }
+                        Err(_) => Err(anyhow!(err)),
+                    }
+                }
+            }
         })
     }
 
+    fn prompt_capabilities(&self) -> acp::PromptCapabilities {
+        self.prompt_capabilities
+    }
+
     fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) {
+        if let Some(session) = self.sessions.borrow_mut().get_mut(session_id) {
+            session.suppress_abort_err = true;
+        }
         let conn = self.connection.clone();
         let params = acp::CancelNotification {
             session_id: session_id.clone(),
@@ -190,6 +271,10 @@ impl AgentConnection for AcpConnection {
             .spawn(async move { conn.cancel(params).await })
             .detach();
     }
+
+    fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
+        self
+    }
 }
 
 struct ClientDelegate {
@@ -213,7 +298,7 @@ impl acp::Client for ClientDelegate {
                 thread.request_tool_call_authorization(arguments.tool_call, arguments.options, cx)
             })?;
 
-        let result = rx.await;
+        let result = rx?.await;
 
         let outcome = match result {
             Ok(option) => acp::RequestPermissionOutcome::Selected { option_id: option },

crates/agent_servers/src/agent_servers.rs 🔗

@@ -3,8 +3,8 @@ mod claude;
 mod gemini;
 mod settings;
 
-#[cfg(test)]
-mod e2e_tests;
+#[cfg(any(test, feature = "test-support"))]
+pub mod e2e_tests;
 
 pub use claude::*;
 pub use gemini::*;
@@ -18,6 +18,7 @@ use project::Project;
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 use std::{
+    any::Any,
     path::{Path, PathBuf},
     rc::Rc,
     sync::Arc,
@@ -40,6 +41,14 @@ pub trait AgentServer: Send {
         project: &Entity<Project>,
         cx: &mut App,
     ) -> Task<Result<Rc<dyn AgentConnection>>>;
+
+    fn into_any(self: Rc<Self>) -> Rc<dyn Any>;
+}
+
+impl dyn AgentServer {
+    pub fn downcast<T: 'static + AgentServer + Sized>(self: Rc<Self>) -> Option<Rc<T>> {
+        self.into_any().downcast().ok()
+    }
 }
 
 impl std::fmt::Debug for AgentServerCommand {
@@ -95,7 +104,7 @@ impl AgentServerCommand {
         cx: &mut AsyncApp,
     ) -> Option<Self> {
         if let Some(agent_settings) = settings {
-            return Some(Self {
+            Some(Self {
                 path: agent_settings.command.path,
                 args: agent_settings
                     .command
@@ -104,7 +113,7 @@ impl AgentServerCommand {
                     .chain(extra_args.iter().map(|arg| arg.to_string()))
                     .collect(),
                 env: agent_settings.command.env,
-            });
+            })
         } else {
             match find_bin_in_path(path_bin_name, project, cx).await {
                 Some(path) => Some(Self {

crates/agent_servers/src/claude.rs 🔗

@@ -1,19 +1,27 @@
+mod edit_tool;
 mod mcp_server;
+mod permission_tool;
+mod read_tool;
 pub mod tools;
+mod write_tool;
 
+use action_log::ActionLog;
 use collections::HashMap;
 use context_server::listener::McpServerTool;
+use language_models::provider::anthropic::AnthropicLanguageModelProvider;
 use project::Project;
 use settings::SettingsStore;
 use smol::process::Child;
+use std::any::Any;
 use std::cell::RefCell;
 use std::fmt::Display;
-use std::path::Path;
+use std::path::{Path, PathBuf};
 use std::rc::Rc;
+use util::command::new_smol_command;
 use uuid::Uuid;
 
 use agent_client_protocol as acp;
-use anyhow::{Result, anyhow};
+use anyhow::{Context as _, Result, anyhow};
 use futures::channel::oneshot;
 use futures::{AsyncBufReadExt, AsyncWriteExt};
 use futures::{
@@ -29,7 +37,7 @@ use util::{ResultExt, debug_panic};
 use crate::claude::mcp_server::{ClaudeZedMcpServer, McpConfig};
 use crate::claude::tools::ClaudeTool;
 use crate::{AgentServer, AgentServerCommand, AllAgentServersSettings};
-use acp_thread::{AcpThread, AgentConnection};
+use acp_thread::{AcpThread, AgentConnection, AuthRequired, LoadError, MentionUri};
 
 #[derive(Clone)]
 pub struct ClaudeCode;
@@ -63,6 +71,10 @@ impl AgentServer for ClaudeCode {
 
         Task::ready(Ok(Rc::new(connection) as _))
     }
+
+    fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
+        self
+    }
 }
 
 struct ClaudeAgentConnection {
@@ -74,12 +86,47 @@ impl AgentConnection for ClaudeAgentConnection {
         self: Rc<Self>,
         project: Entity<Project>,
         cwd: &Path,
-        cx: &mut AsyncApp,
+        cx: &mut App,
     ) -> Task<Result<Entity<AcpThread>>> {
         let cwd = cwd.to_owned();
         cx.spawn(async move |cx| {
+            let settings = cx.read_global(|settings: &SettingsStore, _| {
+                settings.get::<AllAgentServersSettings>(None).claude.clone()
+            })?;
+
+            let Some(command) = AgentServerCommand::resolve(
+                "claude",
+                &[],
+                Some(&util::paths::home_dir().join(".claude/local/claude")),
+                settings,
+                &project,
+                cx,
+            )
+            .await
+            else {
+                return Err(LoadError::NotInstalled {
+                    error_message: "Failed to find Claude Code binary".into(),
+                    install_message: "Install Claude Code".into(),
+                    install_command: "npm install -g @anthropic-ai/claude-code@latest".into(),
+                }.into());
+            };
+
+            let api_key =
+                cx.update(AnthropicLanguageModelProvider::api_key)?
+                    .await
+                    .map_err(|err| {
+                        if err.is::<language_model::AuthenticateError>() {
+                            anyhow!(AuthRequired::new().with_language_model_provider(
+                                language_model::ANTHROPIC_PROVIDER_ID
+                            ))
+                        } else {
+                            anyhow!(err)
+                        }
+                    })?;
+
             let (mut thread_tx, thread_rx) = watch::channel(WeakEntity::new_invalid());
-            let permission_mcp_server = ClaudeZedMcpServer::new(thread_rx.clone(), cx).await?;
+            let fs = project.read_with(cx, |project, _cx| project.fs().clone())?;
+            let permission_mcp_server = ClaudeZedMcpServer::new(thread_rx.clone(), fs, cx).await?;
 
             let mut mcp_servers = HashMap::default();
             mcp_servers.insert(
@@ -97,23 +144,6 @@ impl AgentConnection for ClaudeAgentConnection {
                 .await?;
             mcp_config_file.flush().await?;
 
-            let settings = cx.read_global(|settings: &SettingsStore, _| {
-                settings.get::<AllAgentServersSettings>(None).claude.clone()
-            })?;
-
-            let Some(command) = AgentServerCommand::resolve(
-                "claude",
-                &[],
-                Some(&util::paths::home_dir().join(".claude/local/claude")),
-                settings,
-                &project,
-                cx,
-            )
-            .await
-            else {
-                anyhow::bail!("Failed to find claude binary");
-            };
-
             let (incoming_message_tx, mut incoming_message_rx) = mpsc::unbounded();
             let (outgoing_tx, outgoing_rx) = mpsc::unbounded();
 
@@ -125,16 +155,30 @@ impl AgentConnection for ClaudeAgentConnection {
                 &command,
                 ClaudeSessionMode::Start,
                 session_id.clone(),
+                api_key,
                 &mcp_config_path,
                 &cwd,
             )?;
 
-            let stdin = child.stdin.take().unwrap();
-            let stdout = child.stdout.take().unwrap();
+            let stdout = child.stdout.take().context("Failed to take stdout")?;
+            let stdin = child.stdin.take().context("Failed to take stdin")?;
+            let stderr = child.stderr.take().context("Failed to take stderr")?;
 
             let pid = child.id();
             log::trace!("Spawned (pid: {})", pid);
 
+            cx.background_spawn(async move {
+                let mut stderr = BufReader::new(stderr);
+                let mut line = String::new();
+                while let Ok(n) = stderr.read_line(&mut line).await
+                    && n > 0
+                {
+                    log::warn!("agent stderr: {}", &line);
+                    line.clear();
+                }
+            })
+            .detach();
+
             cx.background_spawn(async move {
                 let mut outgoing_rx = Some(outgoing_rx);
 
@@ -169,20 +213,50 @@ impl AgentConnection for ClaudeAgentConnection {
                         .await
                     }
 
-                    if let Some(status) = child.status().await.log_err() {
-                        if let Some(thread) = thread_rx.recv().await.ok() {
-                            thread
-                                .update(cx, |thread, cx| {
-                                    thread.emit_server_exited(status, cx);
-                                })
-                                .ok();
-                        }
+                    if let Some(status) = child.status().await.log_err()
+                        && let Some(thread) = thread_rx.recv().await.ok()
+                    {
+                        let version = claude_version(command.path.clone(), cx).await.log_err();
+                        let help = claude_help(command.path.clone(), cx).await.log_err();
+                        thread
+                            .update(cx, |thread, cx| {
+                                let error = if let Some(version) = version
+                                    && let Some(help) = help
+                                    && (!help.contains("--input-format")
+                                        || !help.contains("--session-id"))
+                                {
+                                    LoadError::Unsupported {
+                                    error_message: format!(
+                                            "Your installed version of Claude Code ({}, version {}) does not have required features for use with Zed.",
+                                            command.path.to_string_lossy(),
+                                            version,
+                                        )
+                                        .into(),
+                                        upgrade_message: "Upgrade Claude Code to latest".into(),
+                                        upgrade_command: format!(
+                                            "{} update",
+                                            command.path.to_string_lossy()
+                                        ),
+                                    }
+                                } else {
+                                    LoadError::Exited { status }
+                                };
+                                thread.emit_load_error(error, cx);
+                            })
+                            .ok();
                     }
                 }
             });
 
-            let thread = cx.new(|cx| {
-                AcpThread::new("Claude Code", self.clone(), project, session_id.clone(), cx)
+            let action_log = cx.new(|_| ActionLog::new(project.clone()))?;
+            let thread = cx.new(|_cx| {
+                AcpThread::new(
+                    "Claude Code",
+                    self.clone(),
+                    project,
+                    action_log,
+                    session_id.clone(),
+                )
             })?;
 
             thread_tx.send(thread.downgrade())?;
@@ -210,6 +284,7 @@ impl AgentConnection for ClaudeAgentConnection {
 
     fn prompt(
         &self,
+        _id: Option<acp_thread::UserMessageId>,
         params: acp::PromptRequest,
         cx: &mut App,
     ) -> Task<Result<acp::PromptResponse>> {
@@ -224,27 +299,12 @@ impl AgentConnection for ClaudeAgentConnection {
         let (end_tx, end_rx) = oneshot::channel();
         session.turn_state.replace(TurnState::InProgress { end_tx });
 
-        let mut content = String::new();
-        for chunk in params.prompt {
-            match chunk {
-                acp::ContentBlock::Text(text_content) => {
-                    content.push_str(&text_content.text);
-                }
-                acp::ContentBlock::ResourceLink(resource_link) => {
-                    content.push_str(&format!("@{}", resource_link.uri));
-                }
-                acp::ContentBlock::Audio(_)
-                | acp::ContentBlock::Image(_)
-                | acp::ContentBlock::Resource(_) => {
-                    // TODO
-                }
-            }
-        }
+        let content = acp_content_to_claude(params.prompt);
 
         if let Err(err) = session.outgoing_tx.unbounded_send(SdkMessage::User {
             message: Message {
                 role: Role::User,
-                content: Content::UntaggedText(content),
+                content: Content::Chunks(content),
                 id: None,
                 model: None,
                 stop_reason: None,
@@ -259,9 +319,17 @@ impl AgentConnection for ClaudeAgentConnection {
         cx.foreground_executor().spawn(async move { end_rx.await? })
     }
 
+    fn prompt_capabilities(&self) -> acp::PromptCapabilities {
+        acp::PromptCapabilities {
+            image: true,
+            audio: false,
+            embedded_context: true,
+        }
+    }
+
     fn cancel(&self, session_id: &acp::SessionId, _cx: &mut App) {
         let sessions = self.sessions.borrow();
-        let Some(session) = sessions.get(&session_id) else {
+        let Some(session) = sessions.get(session_id) else {
             log::warn!("Attempted to cancel nonexistent session {}", session_id);
             return;
         };
@@ -270,7 +338,7 @@ impl AgentConnection for ClaudeAgentConnection {
 
         let turn_state = session.turn_state.take();
         let TurnState::InProgress { end_tx } = turn_state else {
-            // Already cancelled or idle, put it back
+            // Already canceled or idle, put it back
             session.turn_state.replace(turn_state);
             return;
         };
@@ -288,6 +356,10 @@ impl AgentConnection for ClaudeAgentConnection {
             })
             .log_err();
     }
+
+    fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
+        self
+    }
 }
 
 #[derive(Clone, Copy)]
@@ -301,6 +373,7 @@ fn spawn_claude(
     command: &AgentServerCommand,
     mode: ClaudeSessionMode,
     session_id: acp::SessionId,
+    api_key: language_models::provider::anthropic::ApiKey,
     mcp_config_path: &Path,
     root_dir: &Path,
 ) -> Result<Child> {
@@ -318,34 +391,55 @@ fn spawn_claude(
             &format!(
                 "mcp__{}__{}",
                 mcp_server::SERVER_NAME,
-                mcp_server::PermissionTool::NAME,
+                permission_tool::PermissionTool::NAME,
             ),
             "--allowedTools",
             &format!(
-                "mcp__{}__{},mcp__{}__{}",
-                mcp_server::SERVER_NAME,
-                mcp_server::EditTool::NAME,
+                "mcp__{}__{}",
                 mcp_server::SERVER_NAME,
-                mcp_server::ReadTool::NAME
+                read_tool::ReadTool::NAME
             ),
             "--disallowedTools",
-            "Read,Edit",
+            "Read,Write,Edit,MultiEdit",
         ])
         .args(match mode {
             ClaudeSessionMode::Start => ["--session-id".to_string(), session_id.to_string()],
             ClaudeSessionMode::Resume => ["--resume".to_string(), session_id.to_string()],
         })
         .args(command.args.iter().map(|arg| arg.as_str()))
+        .envs(command.env.iter().flatten())
+        .env("ANTHROPIC_API_KEY", api_key.key)
         .current_dir(root_dir)
         .stdin(std::process::Stdio::piped())
         .stdout(std::process::Stdio::piped())
-        .stderr(std::process::Stdio::inherit())
+        .stderr(std::process::Stdio::piped())
         .kill_on_drop(true)
         .spawn()?;
 
     Ok(child)
 }
 
+fn claude_version(path: PathBuf, cx: &mut AsyncApp) -> Task<Result<semver::Version>> {
+    cx.background_spawn(async move {
+        let output = new_smol_command(path).arg("--version").output().await?;
+        let output = String::from_utf8(output.stdout)?;
+        let version = output
+            .trim()
+            .strip_suffix(" (Claude Code)")
+            .context("parsing Claude version")?;
+        let version = semver::Version::parse(version)?;
+        anyhow::Ok(version)
+    })
+}
+
+fn claude_help(path: PathBuf, cx: &mut AsyncApp) -> Task<Result<String>> {
+    cx.background_spawn(async move {
+        let output = new_smol_command(path).arg("--help").output().await?;
+        let output = String::from_utf8(output.stdout)?;
+        anyhow::Ok(output)
+    })
+}
+
 struct ClaudeAgentSession {
     outgoing_tx: UnboundedSender<SdkMessage>,
     turn_state: Rc<RefCell<TurnState>>,
@@ -370,7 +464,7 @@ enum TurnState {
 }
 
 impl TurnState {
-    fn is_cancelled(&self) -> bool {
+    fn is_canceled(&self) -> bool {
         matches!(self, TurnState::CancelConfirmed { .. })
     }
 
@@ -420,10 +514,10 @@ impl ClaudeAgentSession {
                 for chunk in message.content.chunks() {
                     match chunk {
                         ContentChunk::Text { text } | ContentChunk::UntaggedText(text) => {
-                            if !turn_state.borrow().is_cancelled() {
+                            if !turn_state.borrow().is_canceled() {
                                 thread
                                     .update(cx, |thread, cx| {
-                                        thread.push_user_content_block(text.into(), cx)
+                                        thread.push_user_content_block(None, text.into(), cx)
                                     })
                                     .log_err();
                             }
@@ -435,17 +529,24 @@ impl ClaudeAgentSession {
                             let content = content.to_string();
                             thread
                                 .update(cx, |thread, cx| {
+                                    let id = acp::ToolCallId(tool_use_id.into());
+                                    let set_new_content = !content.is_empty()
+                                        && thread.tool_call(&id).is_none_or(|(_, tool_call)| {
+                                            // preserve rich diff if we have one
+                                            tool_call.diffs().next().is_none()
+                                        });
+
                                     thread.update_tool_call(
                                         acp::ToolCallUpdate {
-                                            id: acp::ToolCallId(tool_use_id.into()),
+                                            id,
                                             fields: acp::ToolCallUpdateFields {
-                                                status: if turn_state.borrow().is_cancelled() {
-                                                    // Do not set to completed if turn was cancelled
+                                                status: if turn_state.borrow().is_canceled() {
+                                                    // Do not set to completed if turn was canceled
                                                     None
                                                 } else {
                                                     Some(acp::ToolCallStatus::Completed)
                                                 },
-                                                content: (!content.is_empty())
+                                                content: set_new_content
                                                     .then(|| vec![content.into()]),
                                                 ..Default::default()
                                             },
@@ -463,10 +564,17 @@ impl ClaudeAgentSession {
                                 chunk
                             );
                         }
+                        ContentChunk::Image { source } => {
+                            if !turn_state.borrow().is_canceled() {
+                                thread
+                                    .update(cx, |thread, cx| {
+                                        thread.push_user_content_block(None, source.into(), cx)
+                                    })
+                                    .log_err();
+                            }
+                        }
 
-                        ContentChunk::Image
-                        | ContentChunk::Document
-                        | ContentChunk::WebSearchToolResult => {
+                        ContentChunk::Document | ContentChunk::WebSearchToolResult => {
                             thread
                                 .update(cx, |thread, cx| {
                                     thread.push_assistant_content_block(
@@ -541,8 +649,9 @@ impl ClaudeAgentSession {
                                         thread.upsert_tool_call(
                                             claude_tool.as_acp(acp::ToolCallId(id.into())),
                                             cx,
-                                        );
+                                        )?;
                                     }
+                                    anyhow::Ok(())
                                 })
                                 .log_err();
                         }
@@ -551,7 +660,14 @@ impl ClaudeAgentSession {
                                 "Should not get tool results with role: assistant. should we handle this?"
                             );
                         }
-                        ContentChunk::Image | ContentChunk::Document => {
+                        ContentChunk::Image { source } => {
+                            thread
+                                .update(cx, |thread, cx| {
+                                    thread.push_assistant_content_block(source.into(), false, cx)
+                                })
+                                .log_err();
+                        }
+                        ContentChunk::Document => {
                             thread
                                 .update(cx, |thread, cx| {
                                     thread.push_assistant_content_block(
@@ -572,14 +688,13 @@ impl ClaudeAgentSession {
                 ..
             } => {
                 let turn_state = turn_state.take();
-                let was_cancelled = turn_state.is_cancelled();
+                let was_canceled = turn_state.is_canceled();
                 let Some(end_turn_tx) = turn_state.end_tx() else {
                     debug_panic!("Received `SdkMessage::Result` but there wasn't an active turn");
                     return;
                 };
 
-                if is_error || (!was_cancelled && subtype == ResultErrorType::ErrorDuringExecution)
-                {
+                if is_error || (!was_canceled && subtype == ResultErrorType::ErrorDuringExecution) {
                     end_turn_tx
                         .send(Err(anyhow!(
                             "Error: {}",
@@ -680,7 +795,7 @@ impl Content {
     pub fn chunks(self) -> impl Iterator<Item = ContentChunk> {
         match self {
             Self::Chunks(chunks) => chunks.into_iter(),
-            Self::UntaggedText(text) => vec![ContentChunk::Text { text: text.clone() }].into_iter(),
+            Self::UntaggedText(text) => vec![ContentChunk::Text { text }].into_iter(),
         }
     }
 }
@@ -718,14 +833,44 @@ enum ContentChunk {
         thinking: String,
     },
     RedactedThinking,
+    Image {
+        source: ImageSource,
+    },
     // TODO
-    Image,
     Document,
     WebSearchToolResult,
     #[serde(untagged)]
     UntaggedText(String),
 }
 
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(tag = "type", rename_all = "snake_case")]
+enum ImageSource {
+    Base64 { data: String, media_type: String },
+    Url { url: String },
+}
+
+impl Into<acp::ContentBlock> for ImageSource {
+    fn into(self) -> acp::ContentBlock {
+        match self {
+            ImageSource::Base64 { data, media_type } => {
+                acp::ContentBlock::Image(acp::ImageContent {
+                    annotations: None,
+                    data,
+                    mime_type: media_type,
+                    uri: None,
+                })
+            }
+            ImageSource::Url { url } => acp::ContentBlock::Image(acp::ImageContent {
+                annotations: None,
+                data: "".to_string(),
+                mime_type: "".to_string(),
+                uri: Some(url),
+            }),
+        }
+    }
+}
+
 impl Display for ContentChunk {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         match self {
@@ -734,7 +879,7 @@ impl Display for ContentChunk {
             ContentChunk::RedactedThinking => write!(f, "Thinking: [REDACTED]"),
             ContentChunk::UntaggedText(text) => write!(f, "{}", text),
             ContentChunk::ToolResult { content, .. } => write!(f, "{}", content),
-            ContentChunk::Image
+            ContentChunk::Image { .. }
             | ContentChunk::Document
             | ContentChunk::ToolUse { .. }
             | ContentChunk::WebSearchToolResult => {
@@ -846,6 +991,75 @@ impl Display for ResultErrorType {
     }
 }
 
+fn acp_content_to_claude(prompt: Vec<acp::ContentBlock>) -> Vec<ContentChunk> {
+    let mut content = Vec::with_capacity(prompt.len());
+    let mut context = Vec::with_capacity(prompt.len());
+
+    for chunk in prompt {
+        match chunk {
+            acp::ContentBlock::Text(text_content) => {
+                content.push(ContentChunk::Text {
+                    text: text_content.text,
+                });
+            }
+            acp::ContentBlock::ResourceLink(resource_link) => {
+                match MentionUri::parse(&resource_link.uri) {
+                    Ok(uri) => {
+                        content.push(ContentChunk::Text {
+                            text: format!("{}", uri.as_link()),
+                        });
+                    }
+                    Err(_) => {
+                        content.push(ContentChunk::Text {
+                            text: resource_link.uri,
+                        });
+                    }
+                }
+            }
+            acp::ContentBlock::Resource(resource) => match resource.resource {
+                acp::EmbeddedResourceResource::TextResourceContents(resource) => {
+                    match MentionUri::parse(&resource.uri) {
+                        Ok(uri) => {
+                            content.push(ContentChunk::Text {
+                                text: format!("{}", uri.as_link()),
+                            });
+                        }
+                        Err(_) => {
+                            content.push(ContentChunk::Text {
+                                text: resource.uri.clone(),
+                            });
+                        }
+                    }
+
+                    context.push(ContentChunk::Text {
+                        text: format!(
+                            "\n<context ref=\"{}\">\n{}\n</context>",
+                            resource.uri, resource.text
+                        ),
+                    });
+                }
+                acp::EmbeddedResourceResource::BlobResourceContents(_) => {
+                    // Unsupported by SDK
+                }
+            },
+            acp::ContentBlock::Image(acp::ImageContent {
+                data, mime_type, ..
+            }) => content.push(ContentChunk::Image {
+                source: ImageSource::Base64 {
+                    data,
+                    media_type: mime_type,
+                },
+            }),
+            acp::ContentBlock::Audio(_) => {
+                // Unsupported by SDK
+            }
+        }
+    }
+
+    content.extend(context);
+    content
+}
+
 fn new_request_id() -> String {
     use rand::Rng;
     // In the Claude Code TS SDK they just generate a random 12 character string,
@@ -879,7 +1093,7 @@ pub(crate) mod tests {
     use gpui::TestAppContext;
     use serde_json::json;
 
-    crate::common_e2e_tests!(ClaudeCode, allow_option_id = "allow");
+    crate::common_e2e_tests!(async |_, _, _| ClaudeCode, allow_option_id = "allow");
 
     pub fn local_command() -> AgentServerCommand {
         AgentServerCommand {
@@ -911,7 +1125,7 @@ pub(crate) mod tests {
 
         thread.read_with(cx, |thread, _| {
             entries_len = thread.plan().entries.len();
-            assert!(thread.plan().entries.len() > 0, "Empty plan");
+            assert!(!thread.plan().entries.is_empty(), "Empty plan");
         });
 
         thread
@@ -1062,4 +1276,100 @@ pub(crate) mod tests {
             _ => panic!("Expected ToolResult variant"),
         }
     }
+
+    #[test]
+    fn test_acp_content_to_claude() {
+        let acp_content = vec![
+            acp::ContentBlock::Text(acp::TextContent {
+                text: "Hello world".to_string(),
+                annotations: None,
+            }),
+            acp::ContentBlock::Image(acp::ImageContent {
+                data: "base64data".to_string(),
+                mime_type: "image/png".to_string(),
+                annotations: None,
+                uri: None,
+            }),
+            acp::ContentBlock::ResourceLink(acp::ResourceLink {
+                uri: "file:///path/to/example.rs".to_string(),
+                name: "example.rs".to_string(),
+                annotations: None,
+                description: None,
+                mime_type: None,
+                size: None,
+                title: None,
+            }),
+            acp::ContentBlock::Resource(acp::EmbeddedResource {
+                annotations: None,
+                resource: acp::EmbeddedResourceResource::TextResourceContents(
+                    acp::TextResourceContents {
+                        mime_type: None,
+                        text: "fn main() { println!(\"Hello!\"); }".to_string(),
+                        uri: "file:///path/to/code.rs".to_string(),
+                    },
+                ),
+            }),
+            acp::ContentBlock::ResourceLink(acp::ResourceLink {
+                uri: "invalid_uri_format".to_string(),
+                name: "invalid.txt".to_string(),
+                annotations: None,
+                description: None,
+                mime_type: None,
+                size: None,
+                title: None,
+            }),
+        ];
+
+        let claude_content = acp_content_to_claude(acp_content);
+
+        assert_eq!(claude_content.len(), 6);
+
+        match &claude_content[0] {
+            ContentChunk::Text { text } => assert_eq!(text, "Hello world"),
+            _ => panic!("Expected Text chunk"),
+        }
+
+        match &claude_content[1] {
+            ContentChunk::Image { source } => match source {
+                ImageSource::Base64 { data, media_type } => {
+                    assert_eq!(data, "base64data");
+                    assert_eq!(media_type, "image/png");
+                }
+                _ => panic!("Expected Base64 image source"),
+            },
+            _ => panic!("Expected Image chunk"),
+        }
+
+        match &claude_content[2] {
+            ContentChunk::Text { text } => {
+                assert!(text.contains("example.rs"));
+                assert!(text.contains("file:///path/to/example.rs"));
+            }
+            _ => panic!("Expected Text chunk for ResourceLink"),
+        }
+
+        match &claude_content[3] {
+            ContentChunk::Text { text } => {
+                assert!(text.contains("code.rs"));
+                assert!(text.contains("file:///path/to/code.rs"));
+            }
+            _ => panic!("Expected Text chunk for Resource"),
+        }
+
+        match &claude_content[4] {
+            ContentChunk::Text { text } => {
+                assert_eq!(text, "invalid_uri_format");
+            }
+            _ => panic!("Expected Text chunk for invalid URI"),
+        }
+
+        match &claude_content[5] {
+            ContentChunk::Text { text } => {
+                assert!(text.contains("<context ref=\"file:///path/to/code.rs\">"));
+                assert!(text.contains("fn main() { println!(\"Hello!\"); }"));
+                assert!(text.contains("</context>"));
+            }
+            _ => panic!("Expected Text chunk for context"),
+        }
+    }
 }

crates/agent_servers/src/claude/edit_tool.rs 🔗

@@ -0,0 +1,178 @@
+use acp_thread::AcpThread;
+use anyhow::Result;
+use context_server::{
+    listener::{McpServerTool, ToolResponse},
+    types::{ToolAnnotations, ToolResponseContent},
+};
+use gpui::{AsyncApp, WeakEntity};
+use language::unified_diff;
+use util::markdown::MarkdownCodeBlock;
+
+use crate::tools::EditToolParams;
+
+#[derive(Clone)]
+pub struct EditTool {
+    thread_rx: watch::Receiver<WeakEntity<AcpThread>>,
+}
+
+impl EditTool {
+    pub fn new(thread_rx: watch::Receiver<WeakEntity<AcpThread>>) -> Self {
+        Self { thread_rx }
+    }
+}
+
+impl McpServerTool for EditTool {
+    type Input = EditToolParams;
+    type Output = ();
+
+    const NAME: &'static str = "Edit";
+
+    fn annotations(&self) -> ToolAnnotations {
+        ToolAnnotations {
+            title: Some("Edit file".to_string()),
+            read_only_hint: Some(false),
+            destructive_hint: Some(false),
+            open_world_hint: Some(false),
+            idempotent_hint: Some(false),
+        }
+    }
+
+    async fn run(
+        &self,
+        input: Self::Input,
+        cx: &mut AsyncApp,
+    ) -> Result<ToolResponse<Self::Output>> {
+        let mut thread_rx = self.thread_rx.clone();
+        let Some(thread) = thread_rx.recv().await?.upgrade() else {
+            anyhow::bail!("Thread closed");
+        };
+
+        let content = thread
+            .update(cx, |thread, cx| {
+                thread.read_text_file(input.abs_path.clone(), None, None, true, cx)
+            })?
+            .await?;
+
+        let (new_content, diff) = cx
+            .background_executor()
+            .spawn(async move {
+                let new_content = content.replace(&input.old_text, &input.new_text);
+                if new_content == content {
+                    return Err(anyhow::anyhow!("Failed to find `old_text`",));
+                }
+                let diff = unified_diff(&content, &new_content);
+
+                Ok((new_content, diff))
+            })
+            .await?;
+
+        thread
+            .update(cx, |thread, cx| {
+                thread.write_text_file(input.abs_path, new_content, cx)
+            })?
+            .await?;
+
+        Ok(ToolResponse {
+            content: vec![ToolResponseContent::Text {
+                text: MarkdownCodeBlock {
+                    tag: "diff",
+                    text: diff.as_str().trim_end_matches('\n'),
+                }
+                .to_string(),
+            }],
+            structured_content: (),
+        })
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use std::rc::Rc;
+
+    use acp_thread::{AgentConnection, StubAgentConnection};
+    use gpui::{Entity, TestAppContext};
+    use indoc::indoc;
+    use project::{FakeFs, Project};
+    use serde_json::json;
+    use settings::SettingsStore;
+    use util::path;
+
+    use super::*;
+
+    #[gpui::test]
+    async fn old_text_not_found(cx: &mut TestAppContext) {
+        let (_thread, tool) = init_test(cx).await;
+
+        let result = tool
+            .run(
+                EditToolParams {
+                    abs_path: path!("/root/file.txt").into(),
+                    old_text: "hi".into(),
+                    new_text: "bye".into(),
+                },
+                &mut cx.to_async(),
+            )
+            .await;
+
+        assert_eq!(result.unwrap_err().to_string(), "Failed to find `old_text`");
+    }
+
+    #[gpui::test]
+    async fn found_and_replaced(cx: &mut TestAppContext) {
+        let (_thread, tool) = init_test(cx).await;
+
+        let result = tool
+            .run(
+                EditToolParams {
+                    abs_path: path!("/root/file.txt").into(),
+                    old_text: "hello".into(),
+                    new_text: "hi".into(),
+                },
+                &mut cx.to_async(),
+            )
+            .await;
+
+        assert_eq!(
+            result.unwrap().content[0].text().unwrap(),
+            indoc! {
+                r"
+                ```diff
+                @@ -1,1 +1,1 @@
+                -hello
+                +hi
+                ```
+                "
+            }
+        );
+    }
+
+    async fn init_test(cx: &mut TestAppContext) -> (Entity<AcpThread>, EditTool) {
+        cx.update(|cx| {
+            let settings_store = SettingsStore::test(cx);
+            cx.set_global(settings_store);
+            language::init(cx);
+            Project::init_settings(cx);
+        });
+
+        let connection = Rc::new(StubAgentConnection::new());
+        let fs = FakeFs::new(cx.executor());
+        fs.insert_tree(
+            path!("/root"),
+            json!({
+                "file.txt": "hello"
+            }),
+        )
+        .await;
+        let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
+        let (mut thread_tx, thread_rx) = watch::channel(WeakEntity::new_invalid());
+
+        let thread = cx
+            .update(|cx| connection.new_thread(project, path!("/test").as_ref(), cx))
+            .await
+            .unwrap();
+
+        thread_tx.send(thread.downgrade()).unwrap();
+
+        (thread, EditTool::new(thread_rx))
+    }
+}

crates/agent_servers/src/claude/mcp_server.rs 🔗

@@ -1,18 +1,22 @@
 use std::path::PathBuf;
+use std::sync::Arc;
 
-use crate::claude::tools::{ClaudeTool, EditToolParams, ReadToolParams};
+use crate::claude::edit_tool::EditTool;
+use crate::claude::permission_tool::PermissionTool;
+use crate::claude::read_tool::ReadTool;
+use crate::claude::write_tool::WriteTool;
 use acp_thread::AcpThread;
-use agent_client_protocol as acp;
-use anyhow::{Context, Result};
+#[cfg(not(test))]
+use anyhow::Context as _;
+use anyhow::Result;
 use collections::HashMap;
-use context_server::listener::{McpServerTool, ToolResponse};
 use context_server::types::{
     Implementation, InitializeParams, InitializeResponse, ProtocolVersion, ServerCapabilities,
-    ToolAnnotations, ToolResponseContent, ToolsCapabilities, requests,
+    ToolsCapabilities, requests,
 };
 use gpui::{App, AsyncApp, Task, WeakEntity};
-use schemars::JsonSchema;
-use serde::{Deserialize, Serialize};
+use project::Fs;
+use serde::Serialize;
 
 pub struct ClaudeZedMcpServer {
     server: context_server::listener::McpServer,
@@ -23,20 +27,16 @@ pub const SERVER_NAME: &str = "zed";
 impl ClaudeZedMcpServer {
     pub async fn new(
         thread_rx: watch::Receiver<WeakEntity<AcpThread>>,
+        fs: Arc<dyn Fs>,
         cx: &AsyncApp,
     ) -> Result<Self> {
         let mut mcp_server = context_server::listener::McpServer::new(cx).await?;
         mcp_server.handle_request::<requests::Initialize>(Self::handle_initialize);
 
-        mcp_server.add_tool(PermissionTool {
-            thread_rx: thread_rx.clone(),
-        });
-        mcp_server.add_tool(ReadTool {
-            thread_rx: thread_rx.clone(),
-        });
-        mcp_server.add_tool(EditTool {
-            thread_rx: thread_rx.clone(),
-        });
+        mcp_server.add_tool(PermissionTool::new(fs.clone(), thread_rx.clone()));
+        mcp_server.add_tool(ReadTool::new(thread_rx.clone()));
+        mcp_server.add_tool(EditTool::new(thread_rx.clone()));
+        mcp_server.add_tool(WriteTool::new(thread_rx.clone()));
 
         Ok(Self { server: mcp_server })
     }
@@ -97,206 +97,3 @@ pub struct McpServerConfig {
     #[serde(skip_serializing_if = "Option::is_none")]
     pub env: Option<HashMap<String, String>>,
 }
-
-// Tools
-
-#[derive(Clone)]
-pub struct PermissionTool {
-    thread_rx: watch::Receiver<WeakEntity<AcpThread>>,
-}
-
-#[derive(Deserialize, JsonSchema, Debug)]
-pub struct PermissionToolParams {
-    tool_name: String,
-    input: serde_json::Value,
-    tool_use_id: Option<String>,
-}
-
-#[derive(Serialize)]
-#[serde(rename_all = "camelCase")]
-pub struct PermissionToolResponse {
-    behavior: PermissionToolBehavior,
-    updated_input: serde_json::Value,
-}
-
-#[derive(Serialize)]
-#[serde(rename_all = "snake_case")]
-enum PermissionToolBehavior {
-    Allow,
-    Deny,
-}
-
-impl McpServerTool for PermissionTool {
-    type Input = PermissionToolParams;
-    type Output = ();
-
-    const NAME: &'static str = "Confirmation";
-
-    fn description(&self) -> &'static str {
-        "Request permission for tool calls"
-    }
-
-    async fn run(
-        &self,
-        input: Self::Input,
-        cx: &mut AsyncApp,
-    ) -> Result<ToolResponse<Self::Output>> {
-        let mut thread_rx = self.thread_rx.clone();
-        let Some(thread) = thread_rx.recv().await?.upgrade() else {
-            anyhow::bail!("Thread closed");
-        };
-
-        let claude_tool = ClaudeTool::infer(&input.tool_name, input.input.clone());
-        let tool_call_id = acp::ToolCallId(input.tool_use_id.context("Tool ID required")?.into());
-        let allow_option_id = acp::PermissionOptionId("allow".into());
-        let reject_option_id = acp::PermissionOptionId("reject".into());
-
-        let chosen_option = thread
-            .update(cx, |thread, cx| {
-                thread.request_tool_call_authorization(
-                    claude_tool.as_acp(tool_call_id),
-                    vec![
-                        acp::PermissionOption {
-                            id: allow_option_id.clone(),
-                            name: "Allow".into(),
-                            kind: acp::PermissionOptionKind::AllowOnce,
-                        },
-                        acp::PermissionOption {
-                            id: reject_option_id.clone(),
-                            name: "Reject".into(),
-                            kind: acp::PermissionOptionKind::RejectOnce,
-                        },
-                    ],
-                    cx,
-                )
-            })?
-            .await?;
-
-        let response = if chosen_option == allow_option_id {
-            PermissionToolResponse {
-                behavior: PermissionToolBehavior::Allow,
-                updated_input: input.input,
-            }
-        } else {
-            debug_assert_eq!(chosen_option, reject_option_id);
-            PermissionToolResponse {
-                behavior: PermissionToolBehavior::Deny,
-                updated_input: input.input,
-            }
-        };
-
-        Ok(ToolResponse {
-            content: vec![ToolResponseContent::Text {
-                text: serde_json::to_string(&response)?,
-            }],
-            structured_content: (),
-        })
-    }
-}
-
-#[derive(Clone)]
-pub struct ReadTool {
-    thread_rx: watch::Receiver<WeakEntity<AcpThread>>,
-}
-
-impl McpServerTool for ReadTool {
-    type Input = ReadToolParams;
-    type Output = ();
-
-    const NAME: &'static str = "Read";
-
-    fn description(&self) -> &'static str {
-        "Read the contents of a file. In sessions with mcp__zed__Read always use it instead of Read as it contains the most up-to-date contents."
-    }
-
-    fn annotations(&self) -> ToolAnnotations {
-        ToolAnnotations {
-            title: Some("Read file".to_string()),
-            read_only_hint: Some(true),
-            destructive_hint: Some(false),
-            open_world_hint: Some(false),
-            idempotent_hint: None,
-        }
-    }
-
-    async fn run(
-        &self,
-        input: Self::Input,
-        cx: &mut AsyncApp,
-    ) -> Result<ToolResponse<Self::Output>> {
-        let mut thread_rx = self.thread_rx.clone();
-        let Some(thread) = thread_rx.recv().await?.upgrade() else {
-            anyhow::bail!("Thread closed");
-        };
-
-        let content = thread
-            .update(cx, |thread, cx| {
-                thread.read_text_file(input.abs_path, input.offset, input.limit, false, cx)
-            })?
-            .await?;
-
-        Ok(ToolResponse {
-            content: vec![ToolResponseContent::Text { text: content }],
-            structured_content: (),
-        })
-    }
-}
-
-#[derive(Clone)]
-pub struct EditTool {
-    thread_rx: watch::Receiver<WeakEntity<AcpThread>>,
-}
-
-impl McpServerTool for EditTool {
-    type Input = EditToolParams;
-    type Output = ();
-
-    const NAME: &'static str = "Edit";
-
-    fn description(&self) -> &'static str {
-        "Edits a file. In sessions with mcp__zed__Edit always use it instead of Edit as it will show the diff to the user better."
-    }
-
-    fn annotations(&self) -> ToolAnnotations {
-        ToolAnnotations {
-            title: Some("Edit file".to_string()),
-            read_only_hint: Some(false),
-            destructive_hint: Some(false),
-            open_world_hint: Some(false),
-            idempotent_hint: Some(false),
-        }
-    }
-
-    async fn run(
-        &self,
-        input: Self::Input,
-        cx: &mut AsyncApp,
-    ) -> Result<ToolResponse<Self::Output>> {
-        let mut thread_rx = self.thread_rx.clone();
-        let Some(thread) = thread_rx.recv().await?.upgrade() else {
-            anyhow::bail!("Thread closed");
-        };
-
-        let content = thread
-            .update(cx, |thread, cx| {
-                thread.read_text_file(input.abs_path.clone(), None, None, true, cx)
-            })?
-            .await?;
-
-        let new_content = content.replace(&input.old_text, &input.new_text);
-        if new_content == content {
-            return Err(anyhow::anyhow!("The old_text was not found in the content"));
-        }
-
-        thread
-            .update(cx, |thread, cx| {
-                thread.write_text_file(input.abs_path, new_content, cx)
-            })?
-            .await?;
-
-        Ok(ToolResponse {
-            content: vec![],
-            structured_content: (),
-        })
-    }
-}

crates/agent_servers/src/claude/permission_tool.rs 🔗

@@ -0,0 +1,158 @@
+use std::sync::Arc;
+
+use acp_thread::AcpThread;
+use agent_client_protocol as acp;
+use agent_settings::AgentSettings;
+use anyhow::{Context as _, Result};
+use context_server::{
+    listener::{McpServerTool, ToolResponse},
+    types::ToolResponseContent,
+};
+use gpui::{AsyncApp, WeakEntity};
+use project::Fs;
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+use settings::{Settings as _, update_settings_file};
+use util::debug_panic;
+
+use crate::tools::ClaudeTool;
+
+#[derive(Clone)]
+pub struct PermissionTool {
+    fs: Arc<dyn Fs>,
+    thread_rx: watch::Receiver<WeakEntity<AcpThread>>,
+}
+
+/// Request permission for tool calls
+#[derive(Deserialize, JsonSchema, Debug)]
+pub struct PermissionToolParams {
+    tool_name: String,
+    input: serde_json::Value,
+    tool_use_id: Option<String>,
+}
+
+#[derive(Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct PermissionToolResponse {
+    behavior: PermissionToolBehavior,
+    updated_input: serde_json::Value,
+}
+
+#[derive(Serialize)]
+#[serde(rename_all = "snake_case")]
+enum PermissionToolBehavior {
+    Allow,
+    Deny,
+}
+
+impl PermissionTool {
+    pub fn new(fs: Arc<dyn Fs>, thread_rx: watch::Receiver<WeakEntity<AcpThread>>) -> Self {
+        Self { fs, thread_rx }
+    }
+}
+
+impl McpServerTool for PermissionTool {
+    type Input = PermissionToolParams;
+    type Output = ();
+
+    const NAME: &'static str = "Confirmation";
+
+    async fn run(
+        &self,
+        input: Self::Input,
+        cx: &mut AsyncApp,
+    ) -> Result<ToolResponse<Self::Output>> {
+        if agent_settings::AgentSettings::try_read_global(cx, |settings| {
+            settings.always_allow_tool_actions
+        })
+        .unwrap_or(false)
+        {
+            let response = PermissionToolResponse {
+                behavior: PermissionToolBehavior::Allow,
+                updated_input: input.input,
+            };
+
+            return Ok(ToolResponse {
+                content: vec![ToolResponseContent::Text {
+                    text: serde_json::to_string(&response)?,
+                }],
+                structured_content: (),
+            });
+        }
+
+        let mut thread_rx = self.thread_rx.clone();
+        let Some(thread) = thread_rx.recv().await?.upgrade() else {
+            anyhow::bail!("Thread closed");
+        };
+
+        let claude_tool = ClaudeTool::infer(&input.tool_name, input.input.clone());
+        let tool_call_id = acp::ToolCallId(input.tool_use_id.context("Tool ID required")?.into());
+
+        const ALWAYS_ALLOW: &str = "always_allow";
+        const ALLOW: &str = "allow";
+        const REJECT: &str = "reject";
+
+        let chosen_option = thread
+            .update(cx, |thread, cx| {
+                thread.request_tool_call_authorization(
+                    claude_tool.as_acp(tool_call_id).into(),
+                    vec![
+                        acp::PermissionOption {
+                            id: acp::PermissionOptionId(ALWAYS_ALLOW.into()),
+                            name: "Always Allow".into(),
+                            kind: acp::PermissionOptionKind::AllowAlways,
+                        },
+                        acp::PermissionOption {
+                            id: acp::PermissionOptionId(ALLOW.into()),
+                            name: "Allow".into(),
+                            kind: acp::PermissionOptionKind::AllowOnce,
+                        },
+                        acp::PermissionOption {
+                            id: acp::PermissionOptionId(REJECT.into()),
+                            name: "Reject".into(),
+                            kind: acp::PermissionOptionKind::RejectOnce,
+                        },
+                    ],
+                    cx,
+                )
+            })??
+            .await?;
+
+        let response = match chosen_option.0.as_ref() {
+            ALWAYS_ALLOW => {
+                cx.update(|cx| {
+                    update_settings_file::<AgentSettings>(self.fs.clone(), cx, |settings, _| {
+                        settings.set_always_allow_tool_actions(true);
+                    });
+                })?;
+
+                PermissionToolResponse {
+                    behavior: PermissionToolBehavior::Allow,
+                    updated_input: input.input,
+                }
+            }
+            ALLOW => PermissionToolResponse {
+                behavior: PermissionToolBehavior::Allow,
+                updated_input: input.input,
+            },
+            REJECT => PermissionToolResponse {
+                behavior: PermissionToolBehavior::Deny,
+                updated_input: input.input,
+            },
+            opt => {
+                debug_panic!("Unexpected option: {}", opt);
+                PermissionToolResponse {
+                    behavior: PermissionToolBehavior::Deny,
+                    updated_input: input.input,
+                }
+            }
+        };
+
+        Ok(ToolResponse {
+            content: vec![ToolResponseContent::Text {
+                text: serde_json::to_string(&response)?,
+            }],
+            structured_content: (),
+        })
+    }
+}

crates/agent_servers/src/claude/read_tool.rs 🔗

@@ -0,0 +1,59 @@
+use acp_thread::AcpThread;
+use anyhow::Result;
+use context_server::{
+    listener::{McpServerTool, ToolResponse},
+    types::{ToolAnnotations, ToolResponseContent},
+};
+use gpui::{AsyncApp, WeakEntity};
+
+use crate::tools::ReadToolParams;
+
+#[derive(Clone)]
+pub struct ReadTool {
+    thread_rx: watch::Receiver<WeakEntity<AcpThread>>,
+}
+
+impl ReadTool {
+    pub fn new(thread_rx: watch::Receiver<WeakEntity<AcpThread>>) -> Self {
+        Self { thread_rx }
+    }
+}
+
+impl McpServerTool for ReadTool {
+    type Input = ReadToolParams;
+    type Output = ();
+
+    const NAME: &'static str = "Read";
+
+    fn annotations(&self) -> ToolAnnotations {
+        ToolAnnotations {
+            title: Some("Read file".to_string()),
+            read_only_hint: Some(true),
+            destructive_hint: Some(false),
+            open_world_hint: Some(false),
+            idempotent_hint: None,
+        }
+    }
+
+    async fn run(
+        &self,
+        input: Self::Input,
+        cx: &mut AsyncApp,
+    ) -> Result<ToolResponse<Self::Output>> {
+        let mut thread_rx = self.thread_rx.clone();
+        let Some(thread) = thread_rx.recv().await?.upgrade() else {
+            anyhow::bail!("Thread closed");
+        };
+
+        let content = thread
+            .update(cx, |thread, cx| {
+                thread.read_text_file(input.abs_path, input.offset, input.limit, false, cx)
+            })?
+            .await?;
+
+        Ok(ToolResponse {
+            content: vec![ToolResponseContent::Text { text: content }],
+            structured_content: (),
+        })
+    }
+}

crates/agent_servers/src/claude/tools.rs 🔗

@@ -34,6 +34,7 @@ impl ClaudeTool {
             // Known tools
             "mcp__zed__Read" => Self::ReadFile(serde_json::from_value(input).log_err()),
             "mcp__zed__Edit" => Self::Edit(serde_json::from_value(input).log_err()),
+            "mcp__zed__Write" => Self::Write(serde_json::from_value(input).log_err()),
             "MultiEdit" => Self::MultiEdit(serde_json::from_value(input).log_err()),
             "Write" => Self::Write(serde_json::from_value(input).log_err()),
             "LS" => Self::Ls(serde_json::from_value(input).log_err()),
@@ -57,7 +58,7 @@ impl ClaudeTool {
                     Self::Terminal(None)
                 } else {
                     Self::Other {
-                        name: tool_name.to_string(),
+                        name: tool_name,
                         input,
                     }
                 }
@@ -93,7 +94,7 @@ impl ClaudeTool {
             }
             Self::MultiEdit(None) => "Multi Edit".into(),
             Self::Write(Some(params)) => {
-                format!("Write {}", params.file_path.display())
+                format!("Write {}", params.abs_path.display())
             }
             Self::Write(None) => "Write".into(),
             Self::Glob(Some(params)) => {
@@ -153,7 +154,7 @@ impl ClaudeTool {
             }],
             Self::Write(Some(params)) => vec![acp::ToolCallContent::Diff {
                 diff: acp::Diff {
-                    path: params.file_path.clone(),
+                    path: params.abs_path.clone(),
                     old_text: None,
                     new_text: params.content.clone(),
                 },
@@ -229,7 +230,10 @@ impl ClaudeTool {
                     line: None,
                 }]
             }
-            Self::Write(Some(WriteToolParams { file_path, .. })) => {
+            Self::Write(Some(WriteToolParams {
+                abs_path: file_path,
+                ..
+            })) => {
                 vec![acp::ToolCallLocation {
                     path: file_path.clone(),
                     line: None,
@@ -302,6 +306,20 @@ impl ClaudeTool {
     }
 }
 
+/// Edit a file.
+///
+/// In sessions with mcp__zed__Edit always use it instead of Edit as it will
+/// allow the user to conveniently review changes.
+///
+/// File editing instructions:
+/// - The `old_text` param must match existing file content, including indentation.
+/// - The `old_text` param must come from the actual file, not an outline.
+/// - The `old_text` section must not be empty.
+/// - Be minimal with replacements:
+///     - For unique lines, include only those lines.
+///     - For non-unique lines, include enough context to identify them.
+/// - Do not escape quotes, newlines, or other characters.
+/// - Only edit the specified file.
 #[derive(Deserialize, JsonSchema, Debug)]
 pub struct EditToolParams {
     /// The absolute path to the file to read.
@@ -312,6 +330,11 @@ pub struct EditToolParams {
     pub new_text: String,
 }
 
+/// Reads the content of the given file in the project.
+///
+/// Never attempt to read a path that hasn't been previously mentioned.
+///
+/// In sessions with mcp__zed__Read always use it instead of Read as it contains the most up-to-date contents.
 #[derive(Deserialize, JsonSchema, Debug)]
 pub struct ReadToolParams {
     /// The absolute path to the file to read.
@@ -324,11 +347,15 @@ pub struct ReadToolParams {
     pub limit: Option<u32>,
 }
 
+/// Writes content to the specified file in the project.
+///
+/// In sessions with mcp__zed__Write always use it instead of Write as it will
+/// allow the user to conveniently review changes.
 #[derive(Deserialize, JsonSchema, Debug)]
 pub struct WriteToolParams {
-    /// Absolute path for new file
-    pub file_path: PathBuf,
-    /// File content
+    /// The absolute path of the file to write.
+    pub abs_path: PathBuf,
+    /// The full content to write.
     pub content: String,
 }
 

crates/agent_servers/src/claude/write_tool.rs 🔗

@@ -0,0 +1,59 @@
+use acp_thread::AcpThread;
+use anyhow::Result;
+use context_server::{
+    listener::{McpServerTool, ToolResponse},
+    types::ToolAnnotations,
+};
+use gpui::{AsyncApp, WeakEntity};
+
+use crate::tools::WriteToolParams;
+
+#[derive(Clone)]
+pub struct WriteTool {
+    thread_rx: watch::Receiver<WeakEntity<AcpThread>>,
+}
+
+impl WriteTool {
+    pub fn new(thread_rx: watch::Receiver<WeakEntity<AcpThread>>) -> Self {
+        Self { thread_rx }
+    }
+}
+
+impl McpServerTool for WriteTool {
+    type Input = WriteToolParams;
+    type Output = ();
+
+    const NAME: &'static str = "Write";
+
+    fn annotations(&self) -> ToolAnnotations {
+        ToolAnnotations {
+            title: Some("Write file".to_string()),
+            read_only_hint: Some(false),
+            destructive_hint: Some(false),
+            open_world_hint: Some(false),
+            idempotent_hint: Some(false),
+        }
+    }
+
+    async fn run(
+        &self,
+        input: Self::Input,
+        cx: &mut AsyncApp,
+    ) -> Result<ToolResponse<Self::Output>> {
+        let mut thread_rx = self.thread_rx.clone();
+        let Some(thread) = thread_rx.recv().await?.upgrade() else {
+            anyhow::bail!("Thread closed");
+        };
+
+        thread
+            .update(cx, |thread, cx| {
+                thread.write_text_file(input.abs_path, input.content, cx)
+            })?
+            .await?;
+
+        Ok(ToolResponse {
+            content: vec![],
+            structured_content: (),
+        })
+    }
+}

crates/agent_servers/src/e2e_tests.rs 🔗

@@ -4,21 +4,30 @@ use std::{
     time::Duration,
 };
 
-use crate::{AgentServer, AgentServerSettings, AllAgentServersSettings};
+use crate::AgentServer;
 use acp_thread::{AcpThread, AgentThreadEntry, ToolCall, ToolCallStatus};
 use agent_client_protocol as acp;
 
 use futures::{FutureExt, StreamExt, channel::mpsc, select};
-use gpui::{Entity, TestAppContext};
+use gpui::{AppContext, Entity, TestAppContext};
 use indoc::indoc;
 use project::{FakeFs, Project};
-use settings::{Settings, SettingsStore};
 use util::path;
 
-pub async fn test_basic(server: impl AgentServer + 'static, cx: &mut TestAppContext) {
-    let fs = init_test(cx).await;
-    let project = Project::test(fs, [], cx).await;
-    let thread = new_test_thread(server, project.clone(), "/private/tmp", cx).await;
+pub async fn test_basic<T, F>(server: F, cx: &mut TestAppContext)
+where
+    T: AgentServer + 'static,
+    F: AsyncFn(&Arc<dyn fs::Fs>, &Entity<Project>, &mut TestAppContext) -> T,
+{
+    let fs = init_test(cx).await as Arc<dyn fs::Fs>;
+    let project = Project::test(fs.clone(), [], cx).await;
+    let thread = new_test_thread(
+        server(&fs, &project, cx).await,
+        project.clone(),
+        "/private/tmp",
+        cx,
+    )
+    .await;
 
     thread
         .update(cx, |thread, cx| thread.send_raw("Hello from Zed!", cx))
@@ -42,8 +51,12 @@ pub async fn test_basic(server: impl AgentServer + 'static, cx: &mut TestAppCont
     });
 }
 
-pub async fn test_path_mentions(server: impl AgentServer + 'static, cx: &mut TestAppContext) {
-    let _fs = init_test(cx).await;
+pub async fn test_path_mentions<T, F>(server: F, cx: &mut TestAppContext)
+where
+    T: AgentServer + 'static,
+    F: AsyncFn(&Arc<dyn fs::Fs>, &Entity<Project>, &mut TestAppContext) -> T,
+{
+    let fs = init_test(cx).await as _;
 
     let tempdir = tempfile::tempdir().unwrap();
     std::fs::write(
@@ -56,7 +69,13 @@ pub async fn test_path_mentions(server: impl AgentServer + 'static, cx: &mut Tes
     )
     .expect("failed to write file");
     let project = Project::example([tempdir.path()], &mut cx.to_async()).await;
-    let thread = new_test_thread(server, project.clone(), tempdir.path(), cx).await;
+    let thread = new_test_thread(
+        server(&fs, &project, cx).await,
+        project.clone(),
+        tempdir.path(),
+        cx,
+    )
+    .await;
     thread
         .update(cx, |thread, cx| {
             thread.send(
@@ -110,15 +129,25 @@ pub async fn test_path_mentions(server: impl AgentServer + 'static, cx: &mut Tes
     drop(tempdir);
 }
 
-pub async fn test_tool_call(server: impl AgentServer + 'static, cx: &mut TestAppContext) {
-    let _fs = init_test(cx).await;
+pub async fn test_tool_call<T, F>(server: F, cx: &mut TestAppContext)
+where
+    T: AgentServer + 'static,
+    F: AsyncFn(&Arc<dyn fs::Fs>, &Entity<Project>, &mut TestAppContext) -> T,
+{
+    let fs = init_test(cx).await as _;
 
     let tempdir = tempfile::tempdir().unwrap();
     let foo_path = tempdir.path().join("foo");
     std::fs::write(&foo_path, "Lorem ipsum dolor").expect("failed to write file");
 
     let project = Project::example([tempdir.path()], &mut cx.to_async()).await;
-    let thread = new_test_thread(server, project.clone(), "/private/tmp", cx).await;
+    let thread = new_test_thread(
+        server(&fs, &project, cx).await,
+        project.clone(),
+        "/private/tmp",
+        cx,
+    )
+    .await;
 
     thread
         .update(cx, |thread, cx| {
@@ -134,7 +163,9 @@ pub async fn test_tool_call(server: impl AgentServer + 'static, cx: &mut TestApp
             matches!(
                 entry,
                 AgentThreadEntry::ToolCall(ToolCall {
-                    status: ToolCallStatus::Allowed { .. },
+                    status: ToolCallStatus::Pending
+                        | ToolCallStatus::InProgress
+                        | ToolCallStatus::Completed,
                     ..
                 })
             )
@@ -150,14 +181,23 @@ pub async fn test_tool_call(server: impl AgentServer + 'static, cx: &mut TestApp
     drop(tempdir);
 }
 
-pub async fn test_tool_call_with_permission(
-    server: impl AgentServer + 'static,
+pub async fn test_tool_call_with_permission<T, F>(
+    server: F,
     allow_option_id: acp::PermissionOptionId,
     cx: &mut TestAppContext,
-) {
-    let fs = init_test(cx).await;
-    let project = Project::test(fs, [path!("/private/tmp").as_ref()], cx).await;
-    let thread = new_test_thread(server, project.clone(), "/private/tmp", cx).await;
+) where
+    T: AgentServer + 'static,
+    F: AsyncFn(&Arc<dyn fs::Fs>, &Entity<Project>, &mut TestAppContext) -> T,
+{
+    let fs = init_test(cx).await as Arc<dyn fs::Fs>;
+    let project = Project::test(fs.clone(), [path!("/private/tmp").as_ref()], cx).await;
+    let thread = new_test_thread(
+        server(&fs, &project, cx).await,
+        project.clone(),
+        "/private/tmp",
+        cx,
+    )
+    .await;
     let full_turn = thread.update(cx, |thread, cx| {
         thread.send_raw(
             r#"Run exactly `touch hello.txt && echo "Hello, world!" | tee hello.txt` in the terminal."#,
@@ -212,7 +252,9 @@ pub async fn test_tool_call_with_permission(
         assert!(thread.entries().iter().any(|entry| matches!(
             entry,
             AgentThreadEntry::ToolCall(ToolCall {
-                status: ToolCallStatus::Allowed { .. },
+                status: ToolCallStatus::Pending
+                    | ToolCallStatus::InProgress
+                    | ToolCallStatus::Completed,
                 ..
             })
         )));
@@ -223,7 +265,9 @@ pub async fn test_tool_call_with_permission(
     thread.read_with(cx, |thread, cx| {
         let AgentThreadEntry::ToolCall(ToolCall {
             content,
-            status: ToolCallStatus::Allowed { .. },
+            status: ToolCallStatus::Pending
+                | ToolCallStatus::InProgress
+                | ToolCallStatus::Completed,
             ..
         }) = thread
             .entries()
@@ -241,11 +285,21 @@ pub async fn test_tool_call_with_permission(
     });
 }
 
-pub async fn test_cancel(server: impl AgentServer + 'static, cx: &mut TestAppContext) {
-    let fs = init_test(cx).await;
-
-    let project = Project::test(fs, [path!("/private/tmp").as_ref()], cx).await;
-    let thread = new_test_thread(server, project.clone(), "/private/tmp", cx).await;
+pub async fn test_cancel<T, F>(server: F, cx: &mut TestAppContext)
+where
+    T: AgentServer + 'static,
+    F: AsyncFn(&Arc<dyn fs::Fs>, &Entity<Project>, &mut TestAppContext) -> T,
+{
+    let fs = init_test(cx).await as Arc<dyn fs::Fs>;
+
+    let project = Project::test(fs.clone(), [path!("/private/tmp").as_ref()], cx).await;
+    let thread = new_test_thread(
+        server(&fs, &project, cx).await,
+        project.clone(),
+        "/private/tmp",
+        cx,
+    )
+    .await;
     let _ = thread.update(cx, |thread, cx| {
         thread.send_raw(
             r#"Run exactly `touch hello.txt && echo "Hello, world!" | tee hello.txt` in the terminal."#,
@@ -310,10 +364,20 @@ pub async fn test_cancel(server: impl AgentServer + 'static, cx: &mut TestAppCon
     });
 }
 
-pub async fn test_thread_drop(server: impl AgentServer + 'static, cx: &mut TestAppContext) {
-    let fs = init_test(cx).await;
-    let project = Project::test(fs, [], cx).await;
-    let thread = new_test_thread(server, project.clone(), "/private/tmp", cx).await;
+pub async fn test_thread_drop<T, F>(server: F, cx: &mut TestAppContext)
+where
+    T: AgentServer + 'static,
+    F: AsyncFn(&Arc<dyn fs::Fs>, &Entity<Project>, &mut TestAppContext) -> T,
+{
+    let fs = init_test(cx).await as Arc<dyn fs::Fs>;
+    let project = Project::test(fs.clone(), [], cx).await;
+    let thread = new_test_thread(
+        server(&fs, &project, cx).await,
+        project.clone(),
+        "/private/tmp",
+        cx,
+    )
+    .await;
 
     thread
         .update(cx, |thread, cx| thread.send_raw("Hello from test!", cx))
@@ -380,25 +444,39 @@ macro_rules! common_e2e_tests {
         }
     };
 }
+pub use common_e2e_tests;
 
 // Helpers
 
 pub async fn init_test(cx: &mut TestAppContext) -> Arc<FakeFs> {
+    #[cfg(test)]
+    use settings::Settings;
+
     env_logger::try_init().ok();
 
     cx.update(|cx| {
-        let settings_store = SettingsStore::test(cx);
+        let settings_store = settings::SettingsStore::test(cx);
         cx.set_global(settings_store);
         Project::init_settings(cx);
         language::init(cx);
+        gpui_tokio::init(cx);
+        let http_client = reqwest_client::ReqwestClient::user_agent("agent tests").unwrap();
+        cx.set_http_client(Arc::new(http_client));
+        client::init_settings(cx);
+        let client = client::Client::production(cx);
+        let user_store = cx.new(|cx| client::UserStore::new(client.clone(), cx));
+        language_model::init(client.clone(), cx);
+        language_models::init(user_store, client, cx);
+        agent_settings::init(cx);
         crate::settings::init(cx);
 
+        #[cfg(test)]
         crate::AllAgentServersSettings::override_global(
-            AllAgentServersSettings {
-                claude: Some(AgentServerSettings {
+            crate::AllAgentServersSettings {
+                claude: Some(crate::AgentServerSettings {
                     command: crate::claude::tests::local_command(),
                 }),
-                gemini: Some(AgentServerSettings {
+                gemini: Some(crate::AgentServerSettings {
                     command: crate::gemini::tests::local_command(),
                 }),
             },
@@ -422,12 +500,9 @@ pub async fn new_test_thread(
         .await
         .unwrap();
 
-    let thread = connection
-        .new_thread(project.clone(), current_dir.as_ref(), &mut cx.to_async())
+    cx.update(|cx| connection.new_thread(project.clone(), current_dir.as_ref(), cx))
         .await
-        .unwrap();
-
-    thread
+        .unwrap()
 }
 
 pub async fn run_until_first_tool_call(
@@ -465,7 +540,7 @@ pub fn get_zed_path() -> PathBuf {
 
     while zed_path
         .file_name()
-        .map_or(true, |name| name.to_string_lossy() != "debug")
+        .is_none_or(|name| name.to_string_lossy() != "debug")
     {
         if !zed_path.pop() {
             panic!("Could not find target directory");

crates/agent_servers/src/gemini.rs 🔗

@@ -1,10 +1,11 @@
-use std::path::Path;
 use std::rc::Rc;
+use std::{any::Any, path::Path};
 
 use crate::{AgentServer, AgentServerCommand};
 use acp_thread::{AgentConnection, LoadError};
 use anyhow::Result;
 use gpui::{Entity, Task};
+use language_models::provider::google::GoogleLanguageModelProvider;
 use project::Project;
 use settings::SettingsStore;
 use ui::App;
@@ -18,15 +19,15 @@ const ACP_ARG: &str = "--experimental-acp";
 
 impl AgentServer for Gemini {
     fn name(&self) -> &'static str {
-        "Gemini"
+        "Gemini CLI"
     }
 
     fn empty_state_headline(&self) -> &'static str {
-        "Welcome to Gemini"
+        "Welcome to Gemini CLI"
     }
 
     fn empty_state_message(&self) -> &'static str {
-        "Ask questions, edit files, run commands.\nBe specific for the best results."
+        "Ask questions, edit files, run commands"
     }
 
     fn logo(&self) -> ui::IconName {
@@ -47,12 +48,20 @@ impl AgentServer for Gemini {
                 settings.get::<AllAgentServersSettings>(None).gemini.clone()
             })?;
 
-            let Some(command) =
+            let Some(mut command) =
                 AgentServerCommand::resolve("gemini", &[ACP_ARG], None, settings, &project, cx).await
             else {
-                anyhow::bail!("Failed to find gemini binary");
+                return Err(LoadError::NotInstalled {
+                    error_message: "Failed to find Gemini CLI binary".into(),
+                    install_message: "Install Gemini CLI".into(),
+                    install_command: "npm install -g @google/gemini-cli@preview".into()
+                }.into());
             };
 
+            if let Some(api_key)= cx.update(GoogleLanguageModelProvider::api_key)?.await.ok() {
+                command.env.get_or_insert_default().insert("GEMINI_API_KEY".to_owned(), api_key.key);
+            }
+
             let result = crate::acp::connect(server_name, command.clone(), &root_dir, cx).await;
             if result.is_err() {
                 let version_fut = util::command::new_smol_command(&command.path)
@@ -75,17 +84,22 @@ impl AgentServer for Gemini {
                 if !supported {
                     return Err(LoadError::Unsupported {
                         error_message: format!(
-                            "Your installed version of Gemini {} doesn't support the Agentic Coding Protocol (ACP).",
+                            "Your installed version of Gemini CLI ({}, version {}) doesn't support the Agentic Coding Protocol (ACP).",
+                            command.path.to_string_lossy(),
                             current_version
                         ).into(),
-                        upgrade_message: "Upgrade Gemini to Latest".into(),
-                        upgrade_command: "npm install -g @google/gemini-cli@latest".into(),
+                        upgrade_message: "Upgrade Gemini CLI to latest".into(),
+                        upgrade_command: "npm install -g @google/gemini-cli@preview".into(),
                     }.into())
                 }
             }
             result
         })
     }
+
+    fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
+        self
+    }
 }
 
 #[cfg(test)]
@@ -94,7 +108,7 @@ pub(crate) mod tests {
     use crate::AgentServerCommand;
     use std::path::Path;
 
-    crate::common_e2e_tests!(Gemini, allow_option_id = "proceed_once");
+    crate::common_e2e_tests!(async |_, _, _| Gemini, allow_option_id = "proceed_once");
 
     pub fn local_command() -> AgentServerCommand {
         let cli_path = Path::new(env!("CARGO_MANIFEST_DIR"))

crates/agent_settings/src/agent_profile.rs 🔗

@@ -48,6 +48,20 @@ pub struct AgentProfileSettings {
     pub context_servers: IndexMap<Arc<str>, ContextServerPreset>,
 }
 
+impl AgentProfileSettings {
+    pub fn is_tool_enabled(&self, tool_name: &str) -> bool {
+        self.tools.get(tool_name) == Some(&true)
+    }
+
+    pub fn is_context_server_tool_enabled(&self, server_id: &str, tool_name: &str) -> bool {
+        self.enable_all_context_servers
+            || self
+                .context_servers
+                .get(server_id)
+                .is_some_and(|preset| preset.tools.get(tool_name) == Some(&true))
+    }
+}
+
 #[derive(Debug, Clone, Default)]
 pub struct ContextServerPreset {
     pub tools: IndexMap<Arc<str>, bool>,

crates/agent_settings/src/agent_settings.rs 🔗

@@ -15,6 +15,8 @@ pub use crate::agent_profile::*;
 
 pub const SUMMARIZE_THREAD_PROMPT: &str =
     include_str!("../../agent/src/prompts/summarize_thread_prompt.txt");
+pub const SUMMARIZE_THREAD_DETAILED_PROMPT: &str =
+    include_str!("../../agent/src/prompts/summarize_thread_detailed_prompt.txt");
 
 pub fn init(cx: &mut App) {
     AgentSettings::register(cx);
@@ -116,15 +118,15 @@ pub struct LanguageModelParameters {
 
 impl LanguageModelParameters {
     pub fn matches(&self, model: &Arc<dyn LanguageModel>) -> bool {
-        if let Some(provider) = &self.provider {
-            if provider.0 != model.provider_id().0 {
-                return false;
-            }
+        if let Some(provider) = &self.provider
+            && provider.0 != model.provider_id().0
+        {
+            return false;
         }
-        if let Some(setting_model) = &self.model {
-            if *setting_model != model.id().0 {
-                return false;
-            }
+        if let Some(setting_model) = &self.model
+            && *setting_model != model.id().0
+        {
+            return false;
         }
         true
     }
@@ -309,7 +311,7 @@ pub struct AgentSettingsContent {
     ///
     /// Default: true
     expand_terminal_card: Option<bool>,
-    /// Whether to always use cmd-enter (or ctrl-enter on Linux) to send messages in the agent panel.
+    /// Whether to always use cmd-enter (or ctrl-enter on Linux or Windows) to send messages in the agent panel.
     ///
     /// Default: false
     use_modifier_to_send: Option<bool>,
@@ -442,10 +444,6 @@ impl Settings for AgentSettings {
                 &mut settings.inline_alternatives,
                 value.inline_alternatives.clone(),
             );
-            merge(
-                &mut settings.always_allow_tool_actions,
-                value.always_allow_tool_actions,
-            );
             merge(
                 &mut settings.notify_when_agent_waiting,
                 value.notify_when_agent_waiting,
@@ -507,6 +505,19 @@ impl Settings for AgentSettings {
             }
         }
 
+        debug_assert!(
+            !sources.default.always_allow_tool_actions.unwrap_or(false),
+            "For security, agent.always_allow_tool_actions should always be false in default.json. If it's true, that is a bug that should be fixed!"
+        );
+
+        // For security reasons, only trust the user's global settings for whether to always allow tool actions.
+        // If this could be overridden locally, an attacker could (e.g. by committing to source control and
+        // convincing you to switch branches) modify your project-local settings to disable the agent's safety checks.
+        settings.always_allow_tool_actions = sources
+            .user
+            .and_then(|setting| setting.always_allow_tool_actions)
+            .unwrap_or(false);
+
         Ok(settings)
     }
 

crates/agent_ui/Cargo.toml 🔗

@@ -17,6 +17,7 @@ test-support = ["gpui/test-support", "language/test-support"]
 
 [dependencies]
 acp_thread.workspace = true
+action_log.workspace = true
 agent-client-protocol.workspace = true
 agent.workspace = true
 agent2.workspace = true
@@ -49,7 +50,6 @@ fuzzy.workspace = true
 gpui.workspace = true
 html_to_markdown.workspace = true
 http_client.workspace = true
-indexed_docs.workspace = true
 indoc.workspace = true
 inventory.workspace = true
 itertools.workspace = true
@@ -92,6 +92,7 @@ time.workspace = true
 time_format.workspace = true
 ui.workspace = true
 ui_input.workspace = true
+url.workspace = true
 urlencoding.workspace = true
 util.workspace = true
 uuid.workspace = true
@@ -101,8 +102,13 @@ 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
 buffer_diff = { workspace = true, features = ["test-support"] }
+db = { workspace = true, features = ["test-support"] }
 editor = { workspace = true, features = ["test-support"] }
 gpui = { workspace = true, "features" = ["test-support"] }
 indoc.workspace = true

crates/agent_ui/src/acp.rs 🔗

@@ -1,6 +1,12 @@
 mod completion_provider;
-mod message_history;
+mod entry_view_state;
+mod message_editor;
+mod model_selector;
+mod model_selector_popover;
+mod thread_history;
 mod thread_view;
 
-pub use message_history::MessageHistory;
+pub use model_selector::AcpModelSelector;
+pub use model_selector_popover::AcpModelSelectorPopover;
+pub use thread_history::*;
 pub use thread_view::AcpThreadView;

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

@@ -1,101 +1,222 @@
+use std::cell::Cell;
 use std::ops::Range;
-use std::path::Path;
+use std::rc::Rc;
 use std::sync::Arc;
 use std::sync::atomic::AtomicBool;
 
+use acp_thread::MentionUri;
+use agent_client_protocol as acp;
+use agent2::{HistoryEntry, HistoryStore};
 use anyhow::Result;
-use collections::HashMap;
-use editor::display_map::CreaseId;
 use editor::{CompletionProvider, Editor, ExcerptId};
-use file_icons::FileIcons;
+use fuzzy::{StringMatch, StringMatchCandidate};
 use gpui::{App, Entity, Task, WeakEntity};
 use language::{Buffer, CodeLabel, HighlightId};
 use lsp::CompletionContext;
-use parking_lot::Mutex;
-use project::{Completion, CompletionIntent, CompletionResponse, ProjectPath, WorktreeId};
+use project::{
+    Completion, CompletionIntent, CompletionResponse, Project, ProjectPath, Symbol, WorktreeId,
+};
+use prompt_store::PromptStore;
 use rope::Point;
-use text::{Anchor, ToPoint};
+use text::{Anchor, ToPoint as _};
 use ui::prelude::*;
 use workspace::Workspace;
 
-use crate::context_picker::MentionLink;
-use crate::context_picker::file_context_picker::{extract_file_name_and_directory, search_files};
-
-#[derive(Default)]
-pub struct MentionSet {
-    paths_by_crease_id: HashMap<CreaseId, ProjectPath>,
+use crate::AgentPanel;
+use crate::acp::message_editor::MessageEditor;
+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::{
+    ContextPickerAction, ContextPickerEntry, ContextPickerMode, selection_ranges,
+};
+
+pub(crate) enum Match {
+    File(FileMatch),
+    Symbol(SymbolMatch),
+    Thread(HistoryEntry),
+    RecentThread(HistoryEntry),
+    Fetch(SharedString),
+    Rules(RulesContextEntry),
+    Entry(EntryMatch),
 }
 
-impl MentionSet {
-    pub fn insert(&mut self, crease_id: CreaseId, path: ProjectPath) {
-        self.paths_by_crease_id.insert(crease_id, path);
-    }
-
-    pub fn path_for_crease_id(&self, crease_id: CreaseId) -> Option<ProjectPath> {
-        self.paths_by_crease_id.get(&crease_id).cloned()
-    }
+pub struct EntryMatch {
+    mat: Option<StringMatch>,
+    entry: ContextPickerEntry,
+}
 
-    pub fn drain(&mut self) -> impl Iterator<Item = CreaseId> {
-        self.paths_by_crease_id.drain().map(|(id, _)| id)
+impl Match {
+    pub fn score(&self) -> f64 {
+        match self {
+            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::Rules(_) => 1.,
+            Match::Fetch(_) => 1.,
+        }
     }
 }
 
 pub struct ContextPickerCompletionProvider {
+    message_editor: WeakEntity<MessageEditor>,
     workspace: WeakEntity<Workspace>,
-    editor: WeakEntity<Editor>,
-    mention_set: Arc<Mutex<MentionSet>>,
+    history_store: Entity<HistoryStore>,
+    prompt_store: Option<Entity<PromptStore>>,
+    prompt_capabilities: Rc<Cell<acp::PromptCapabilities>>,
 }
 
 impl ContextPickerCompletionProvider {
     pub fn new(
-        mention_set: Arc<Mutex<MentionSet>>,
+        message_editor: WeakEntity<MessageEditor>,
         workspace: WeakEntity<Workspace>,
-        editor: WeakEntity<Editor>,
+        history_store: Entity<HistoryStore>,
+        prompt_store: Option<Entity<PromptStore>>,
+        prompt_capabilities: Rc<Cell<acp::PromptCapabilities>>,
     ) -> Self {
         Self {
-            mention_set,
+            message_editor,
             workspace,
-            editor,
+            history_store,
+            prompt_store,
+            prompt_capabilities,
+        }
+    }
+
+    fn completion_for_entry(
+        entry: ContextPickerEntry,
+        source_range: Range<Anchor>,
+        message_editor: WeakEntity<MessageEditor>,
+        workspace: &Entity<Workspace>,
+        cx: &mut App,
+    ) -> Option<Completion> {
+        match entry {
+            ContextPickerEntry::Mode(mode) => Some(Completion {
+                replace_range: source_range,
+                new_text: format!("@{} ", mode.keyword()),
+                label: CodeLabel::plain(mode.label().to_string(), None),
+                icon_path: Some(mode.icon().path().into()),
+                documentation: None,
+                source: project::CompletionSource::Custom,
+                insert_text_mode: None,
+                // This ensures that when a user accepts this completion, the
+                // completion menu will still be shown after "@category " is
+                // inserted
+                confirm: Some(Arc::new(|_, _, _| true)),
+            }),
+            ContextPickerEntry::Action(action) => {
+                Self::completion_for_action(action, source_range, message_editor, workspace, cx)
+            }
+        }
+    }
+
+    fn completion_for_thread(
+        thread_entry: HistoryEntry,
+        source_range: Range<Anchor>,
+        recent: bool,
+        editor: WeakEntity<MessageEditor>,
+        cx: &mut App,
+    ) -> Completion {
+        let uri = thread_entry.mention_uri();
+
+        let icon_for_completion = if recent {
+            IconName::HistoryRerun.path().into()
+        } else {
+            uri.icon_path(cx)
+        };
+
+        let new_text = format!("{} ", uri.as_link());
+
+        let new_text_len = new_text.len();
+        Completion {
+            replace_range: source_range.clone(),
+            new_text,
+            label: CodeLabel::plain(thread_entry.title().to_string(), None),
+            documentation: None,
+            insert_text_mode: None,
+            source: project::CompletionSource::Custom,
+            icon_path: Some(icon_for_completion),
+            confirm: Some(confirm_completion_callback(
+                thread_entry.title().clone(),
+                source_range.start,
+                new_text_len - 1,
+                editor,
+                uri,
+            )),
         }
     }
 
-    fn completion_for_path(
+    fn completion_for_rules(
+        rule: RulesContextEntry,
+        source_range: Range<Anchor>,
+        editor: WeakEntity<MessageEditor>,
+        cx: &mut App,
+    ) -> Completion {
+        let uri = MentionUri::Rule {
+            id: rule.prompt_id.into(),
+            name: rule.title.to_string(),
+        };
+        let new_text = format!("{} ", uri.as_link());
+        let new_text_len = new_text.len();
+        let icon_path = uri.icon_path(cx);
+        Completion {
+            replace_range: source_range.clone(),
+            new_text,
+            label: CodeLabel::plain(rule.title.to_string(), None),
+            documentation: None,
+            insert_text_mode: None,
+            source: project::CompletionSource::Custom,
+            icon_path: Some(icon_path),
+            confirm: Some(confirm_completion_callback(
+                rule.title,
+                source_range.start,
+                new_text_len - 1,
+                editor,
+                uri,
+            )),
+        }
+    }
+
+    pub(crate) fn completion_for_path(
         project_path: ProjectPath,
         path_prefix: &str,
         is_recent: bool,
         is_directory: bool,
-        excerpt_id: ExcerptId,
         source_range: Range<Anchor>,
-        editor: Entity<Editor>,
-        mention_set: Arc<Mutex<MentionSet>>,
-        cx: &App,
-    ) -> Completion {
+        message_editor: WeakEntity<MessageEditor>,
+        project: Entity<Project>,
+        cx: &mut App,
+    ) -> Option<Completion> {
         let (file_name, directory) =
-            extract_file_name_and_directory(&project_path.path, path_prefix);
+            crate::context_picker::file_context_picker::extract_file_name_and_directory(
+                &project_path.path,
+                path_prefix,
+            );
 
         let label =
             build_code_label_for_full_path(&file_name, directory.as_ref().map(|s| s.as_ref()), cx);
-        let full_path = if let Some(directory) = directory {
-            format!("{}{}", directory, file_name)
-        } else {
-            file_name.to_string()
-        };
 
-        let crease_icon_path = if is_directory {
-            FileIcons::get_folder_icon(false, cx).unwrap_or_else(|| IconName::Folder.path().into())
+        let abs_path = project.read(cx).absolute_path(&project_path, cx)?;
+
+        let uri = if is_directory {
+            MentionUri::Directory { abs_path }
         } else {
-            FileIcons::get_icon(Path::new(&full_path), cx)
-                .unwrap_or_else(|| IconName::File.path().into())
+            MentionUri::File { abs_path }
         };
+
+        let crease_icon_path = uri.icon_path(cx);
         let completion_icon_path = if is_recent {
             IconName::HistoryRerun.path().into()
         } else {
-            crease_icon_path.clone()
+            crease_icon_path
         };
 
-        let new_text = format!("{} ", MentionLink::for_file(&file_name, &full_path));
+        let new_text = format!("{} ", uri.as_link());
         let new_text_len = new_text.len();
-        Completion {
+        Some(Completion {
             replace_range: source_range.clone(),
             new_text,
             label,
@@ -104,28 +225,409 @@ impl ContextPickerCompletionProvider {
             icon_path: Some(completion_icon_path),
             insert_text_mode: None,
             confirm: Some(confirm_completion_callback(
-                crease_icon_path,
                 file_name,
-                project_path,
-                excerpt_id,
                 source_range.start,
                 new_text_len - 1,
-                editor,
-                mention_set,
+                message_editor,
+                uri,
             )),
+        })
+    }
+
+    fn completion_for_symbol(
+        symbol: Symbol,
+        source_range: Range<Anchor>,
+        message_editor: WeakEntity<MessageEditor>,
+        workspace: Entity<Workspace>,
+        cx: &mut App,
+    ) -> Option<Completion> {
+        let project = workspace.read(cx).project().clone();
+
+        let label = CodeLabel::plain(symbol.name.clone(), None);
+
+        let abs_path = project.read(cx).absolute_path(&symbol.path, cx)?;
+        let uri = MentionUri::Symbol {
+            path: abs_path,
+            name: symbol.name.clone(),
+            line_range: symbol.range.start.0.row..symbol.range.end.0.row,
+        };
+        let new_text = format!("{} ", uri.as_link());
+        let new_text_len = new_text.len();
+        let icon_path = uri.icon_path(cx);
+        Some(Completion {
+            replace_range: source_range.clone(),
+            new_text,
+            label,
+            documentation: None,
+            source: project::CompletionSource::Custom,
+            icon_path: Some(icon_path),
+            insert_text_mode: None,
+            confirm: Some(confirm_completion_callback(
+                symbol.name.into(),
+                source_range.start,
+                new_text_len - 1,
+                message_editor,
+                uri,
+            )),
+        })
+    }
+
+    fn completion_for_fetch(
+        source_range: Range<Anchor>,
+        url_to_fetch: SharedString,
+        message_editor: WeakEntity<MessageEditor>,
+        cx: &mut App,
+    ) -> Option<Completion> {
+        let new_text = format!("@fetch {} ", url_to_fetch);
+        let url_to_fetch = url::Url::parse(url_to_fetch.as_ref())
+            .or_else(|_| url::Url::parse(&format!("https://{url_to_fetch}")))
+            .ok()?;
+        let mention_uri = MentionUri::Fetch {
+            url: url_to_fetch.clone(),
+        };
+        let icon_path = mention_uri.icon_path(cx);
+        Some(Completion {
+            replace_range: source_range.clone(),
+            new_text: new_text.clone(),
+            label: CodeLabel::plain(url_to_fetch.to_string(), None),
+            documentation: None,
+            source: project::CompletionSource::Custom,
+            icon_path: Some(icon_path),
+            insert_text_mode: None,
+            confirm: Some(confirm_completion_callback(
+                url_to_fetch.to_string().into(),
+                source_range.start,
+                new_text.len() - 1,
+                message_editor,
+                mention_uri,
+            )),
+        })
+    }
+
+    pub(crate) fn completion_for_action(
+        action: ContextPickerAction,
+        source_range: Range<Anchor>,
+        message_editor: WeakEntity<MessageEditor>,
+        workspace: &Entity<Workspace>,
+        cx: &mut App,
+    ) -> Option<Completion> {
+        let (new_text, on_action) = match action {
+            ContextPickerAction::AddSelections => {
+                const PLACEHOLDER: &str = "selection ";
+                let selections = selection_ranges(workspace, cx)
+                    .into_iter()
+                    .enumerate()
+                    .map(|(ix, (buffer, range))| {
+                        (
+                            buffer,
+                            range,
+                            (PLACEHOLDER.len() * ix)..(PLACEHOLDER.len() * (ix + 1) - 1),
+                        )
+                    })
+                    .collect::<Vec<_>>();
+
+                let new_text: String = PLACEHOLDER.repeat(selections.len());
+
+                let callback = Arc::new({
+                    let source_range = source_range.clone();
+                    move |_, window: &mut Window, cx: &mut App| {
+                        let selections = selections.clone();
+                        let message_editor = message_editor.clone();
+                        let source_range = source_range.clone();
+                        window.defer(cx, move |window, cx| {
+                            message_editor
+                                .update(cx, |message_editor, cx| {
+                                    message_editor.confirm_mention_for_selection(
+                                        source_range,
+                                        selections,
+                                        window,
+                                        cx,
+                                    )
+                                })
+                                .ok();
+                        });
+                        false
+                    }
+                });
+
+                (new_text, callback)
+            }
+        };
+
+        Some(Completion {
+            replace_range: source_range,
+            new_text,
+            label: CodeLabel::plain(action.label().to_string(), None),
+            icon_path: Some(action.icon().path().into()),
+            documentation: None,
+            source: project::CompletionSource::Custom,
+            insert_text_mode: None,
+            // This ensures that when a user accepts this completion, the
+            // completion menu will still be shown after "@category " is
+            // inserted
+            confirm: Some(on_action),
+        })
+    }
+
+    fn search(
+        &self,
+        mode: Option<ContextPickerMode>,
+        query: String,
+        cancellation_flag: Arc<AtomicBool>,
+        cx: &mut App,
+    ) -> Task<Vec<Match>> {
+        let Some(workspace) = self.workspace.upgrade() else {
+            return Task::ready(Vec::default());
+        };
+        match mode {
+            Some(ContextPickerMode::File) => {
+                let search_files_task = search_files(query, cancellation_flag, &workspace, cx);
+                cx.background_spawn(async move {
+                    search_files_task
+                        .await
+                        .into_iter()
+                        .map(Match::File)
+                        .collect()
+                })
+            }
+
+            Some(ContextPickerMode::Symbol) => {
+                let search_symbols_task = search_symbols(query, cancellation_flag, &workspace, cx);
+                cx.background_spawn(async move {
+                    search_symbols_task
+                        .await
+                        .into_iter()
+                        .map(Match::Symbol)
+                        .collect()
+                })
+            }
+
+            Some(ContextPickerMode::Thread) => {
+                let search_threads_task =
+                    search_threads(query, cancellation_flag, &self.history_store, cx);
+                cx.background_spawn(async move {
+                    search_threads_task
+                        .await
+                        .into_iter()
+                        .map(Match::Thread)
+                        .collect()
+                })
+            }
+
+            Some(ContextPickerMode::Fetch) => {
+                if !query.is_empty() {
+                    Task::ready(vec![Match::Fetch(query.into())])
+                } else {
+                    Task::ready(Vec::new())
+                }
+            }
+
+            Some(ContextPickerMode::Rules) => {
+                if let Some(prompt_store) = self.prompt_store.as_ref() {
+                    let search_rules_task =
+                        search_rules(query, cancellation_flag, prompt_store, cx);
+                    cx.background_spawn(async move {
+                        search_rules_task
+                            .await
+                            .into_iter()
+                            .map(Match::Rules)
+                            .collect::<Vec<_>>()
+                    })
+                } else {
+                    Task::ready(Vec::new())
+                }
+            }
+
+            None if query.is_empty() => {
+                let mut matches = self.recent_context_picker_entries(&workspace, cx);
+
+                matches.extend(
+                    self.available_context_picker_entries(&workspace, cx)
+                        .into_iter()
+                        .map(|mode| {
+                            Match::Entry(EntryMatch {
+                                entry: mode,
+                                mat: None,
+                            })
+                        }),
+                );
+
+                Task::ready(matches)
+            }
+            None => {
+                let executor = cx.background_executor().clone();
+
+                let search_files_task =
+                    search_files(query.clone(), cancellation_flag, &workspace, cx);
+
+                let entries = self.available_context_picker_entries(&workspace, cx);
+                let entry_candidates = entries
+                    .iter()
+                    .enumerate()
+                    .map(|(ix, entry)| StringMatchCandidate::new(ix, entry.keyword()))
+                    .collect::<Vec<_>>();
+
+                cx.background_spawn(async move {
+                    let mut matches = search_files_task
+                        .await
+                        .into_iter()
+                        .map(Match::File)
+                        .collect::<Vec<_>>();
+
+                    let entry_matches = fuzzy::match_strings(
+                        &entry_candidates,
+                        &query,
+                        false,
+                        true,
+                        100,
+                        &Arc::new(AtomicBool::default()),
+                        executor,
+                    )
+                    .await;
+
+                    matches.extend(entry_matches.into_iter().map(|mat| {
+                        Match::Entry(EntryMatch {
+                            entry: entries[mat.candidate_id],
+                            mat: Some(mat),
+                        })
+                    }));
+
+                    matches.sort_by(|a, b| {
+                        b.score()
+                            .partial_cmp(&a.score())
+                            .unwrap_or(std::cmp::Ordering::Equal)
+                    });
+
+                    matches
+                })
+            }
         }
     }
+
+    fn recent_context_picker_entries(
+        &self,
+        workspace: &Entity<Workspace>,
+        cx: &mut App,
+    ) -> Vec<Match> {
+        let mut recent = Vec::with_capacity(6);
+
+        let mut mentions = self
+            .message_editor
+            .read_with(cx, |message_editor, _cx| message_editor.mentions())
+            .unwrap_or_default();
+        let workspace = workspace.read(cx);
+        let project = workspace.project().read(cx);
+
+        if let Some(agent_panel) = workspace.panel::<AgentPanel>(cx)
+            && let Some(thread) = agent_panel.read(cx).active_agent_thread(cx)
+        {
+            let thread = thread.read(cx);
+            mentions.insert(MentionUri::Thread {
+                id: thread.session_id().clone(),
+                name: thread.title().into(),
+            });
+        }
+
+        recent.extend(
+            workspace
+                .recent_navigation_history_iter(cx)
+                .filter(|(_, abs_path)| {
+                    abs_path.as_ref().is_none_or(|path| {
+                        !mentions.contains(&MentionUri::File {
+                            abs_path: path.clone(),
+                        })
+                    })
+                })
+                .take(4)
+                .filter_map(|(project_path, _)| {
+                    project
+                        .worktree_for_id(project_path.worktree_id, cx)
+                        .map(|worktree| {
+                            let path_prefix = worktree.read(cx).root_name().into();
+                            Match::File(FileMatch {
+                                mat: fuzzy::PathMatch {
+                                    score: 1.,
+                                    positions: Vec::new(),
+                                    worktree_id: project_path.worktree_id.to_usize(),
+                                    path: project_path.path,
+                                    path_prefix,
+                                    is_dir: false,
+                                    distance_to_relative_ancestor: 0,
+                                },
+                                is_recent: true,
+                            })
+                        })
+                }),
+        );
+
+        if self.prompt_capabilities.get().embedded_context {
+            const RECENT_COUNT: usize = 2;
+            let threads = self
+                .history_store
+                .read(cx)
+                .recently_opened_entries(cx)
+                .into_iter()
+                .filter(|thread| !mentions.contains(&thread.mention_uri()))
+                .take(RECENT_COUNT)
+                .collect::<Vec<_>>();
+
+            recent.extend(threads.into_iter().map(Match::RecentThread));
+        }
+
+        recent
+    }
+
+    fn available_context_picker_entries(
+        &self,
+        workspace: &Entity<Workspace>,
+        cx: &mut App,
+    ) -> Vec<ContextPickerEntry> {
+        let embedded_context = self.prompt_capabilities.get().embedded_context;
+        let mut entries = if embedded_context {
+            vec![
+                ContextPickerEntry::Mode(ContextPickerMode::File),
+                ContextPickerEntry::Mode(ContextPickerMode::Symbol),
+                ContextPickerEntry::Mode(ContextPickerMode::Thread),
+            ]
+        } else {
+            // File is always available, but we don't need a mode entry
+            vec![]
+        };
+
+        let has_selection = workspace
+            .read(cx)
+            .active_item(cx)
+            .and_then(|item| item.downcast::<Editor>())
+            .is_some_and(|editor| {
+                editor.update(cx, |editor, cx| editor.has_non_empty_selection(cx))
+            });
+        if has_selection {
+            entries.push(ContextPickerEntry::Action(
+                ContextPickerAction::AddSelections,
+            ));
+        }
+
+        if embedded_context {
+            if self.prompt_store.is_some() {
+                entries.push(ContextPickerEntry::Mode(ContextPickerMode::Rules));
+            }
+
+            entries.push(ContextPickerEntry::Mode(ContextPickerMode::Fetch));
+        }
+
+        entries
+    }
 }
 
 fn build_code_label_for_full_path(file_name: &str, directory: Option<&str>, cx: &App) -> CodeLabel {
     let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId);
     let mut label = CodeLabel::default();
 
-    label.push_str(&file_name, None);
+    label.push_str(file_name, None);
     label.push_str(" ", None);
 
     if let Some(directory) = directory {
-        label.push_str(&directory, comment_id);
+        label.push_str(directory, comment_id);
     }
 
     label.filter_range = 0..label.text().len();
@@ -136,7 +638,7 @@ fn build_code_label_for_full_path(file_name: &str, directory: Option<&str>, cx:
 impl CompletionProvider for ContextPickerCompletionProvider {
     fn completions(
         &self,
-        excerpt_id: ExcerptId,
+        _excerpt_id: ExcerptId,
         buffer: &Entity<Buffer>,
         buffer_position: Anchor,
         _trigger: CompletionContext,
@@ -149,7 +651,11 @@ impl CompletionProvider for ContextPickerCompletionProvider {
             let offset_to_line = buffer.point_to_offset(line_start);
             let mut lines = buffer.text_for_range(line_start..position).lines();
             let line = lines.next()?;
-            MentionCompletion::try_parse(line, offset_to_line)
+            MentionCompletion::try_parse(
+                self.prompt_capabilities.get().embedded_context,
+                line,
+                offset_to_line,
+            )
         });
         let Some(state) = state else {
             return Task::ready(Ok(Vec::new()));
@@ -159,44 +665,88 @@ impl CompletionProvider for ContextPickerCompletionProvider {
             return Task::ready(Ok(Vec::new()));
         };
 
+        let project = workspace.read(cx).project().clone();
         let snapshot = buffer.read(cx).snapshot();
         let source_range = snapshot.anchor_before(state.source_range.start)
             ..snapshot.anchor_after(state.source_range.end);
 
-        let editor = self.editor.clone();
-        let mention_set = self.mention_set.clone();
-        let MentionCompletion { argument, .. } = state;
+        let editor = self.message_editor.clone();
+
+        let MentionCompletion { mode, argument, .. } = state;
         let query = argument.unwrap_or_else(|| "".to_string());
 
-        let search_task = search_files(query.clone(), Arc::<AtomicBool>::default(), &workspace, cx);
+        let search_task = self.search(mode, query, Arc::<AtomicBool>::default(), cx);
 
         cx.spawn(async move |_, cx| {
             let matches = search_task.await;
-            let Some(editor) = editor.upgrade() else {
-                return Ok(Vec::new());
-            };
 
             let completions = cx.update(|cx| {
                 matches
                     .into_iter()
-                    .map(|mat| {
-                        let path_match = &mat.mat;
-                        let project_path = ProjectPath {
-                            worktree_id: WorktreeId::from_usize(path_match.worktree_id),
-                            path: path_match.path.clone(),
-                        };
-
-                        Self::completion_for_path(
-                            project_path,
-                            &path_match.path_prefix,
-                            mat.is_recent,
-                            path_match.is_dir,
-                            excerpt_id,
+                    .filter_map(|mat| match mat {
+                        Match::File(FileMatch { mat, is_recent }) => {
+                            let project_path = ProjectPath {
+                                worktree_id: WorktreeId::from_usize(mat.worktree_id),
+                                path: mat.path.clone(),
+                            };
+
+                            Self::completion_for_path(
+                                project_path,
+                                &mat.path_prefix,
+                                is_recent,
+                                mat.is_dir,
+                                source_range.clone(),
+                                editor.clone(),
+                                project.clone(),
+                                cx,
+                            )
+                        }
+
+                        Match::Symbol(SymbolMatch { symbol, .. }) => Self::completion_for_symbol(
+                            symbol,
                             source_range.clone(),
                             editor.clone(),
-                            mention_set.clone(),
+                            workspace.clone(),
                             cx,
-                        )
+                        ),
+
+                        Match::Thread(thread) => Some(Self::completion_for_thread(
+                            thread,
+                            source_range.clone(),
+                            false,
+                            editor.clone(),
+                            cx,
+                        )),
+
+                        Match::RecentThread(thread) => Some(Self::completion_for_thread(
+                            thread,
+                            source_range.clone(),
+                            true,
+                            editor.clone(),
+                            cx,
+                        )),
+
+                        Match::Rules(user_rules) => Some(Self::completion_for_rules(
+                            user_rules,
+                            source_range.clone(),
+                            editor.clone(),
+                            cx,
+                        )),
+
+                        Match::Fetch(url) => Self::completion_for_fetch(
+                            source_range.clone(),
+                            url,
+                            editor.clone(),
+                            cx,
+                        ),
+
+                        Match::Entry(EntryMatch { entry, .. }) => Self::completion_for_entry(
+                            entry,
+                            source_range.clone(),
+                            editor.clone(),
+                            &workspace,
+                            cx,
+                        ),
                     })
                     .collect()
             })?;
@@ -225,12 +775,16 @@ impl CompletionProvider for ContextPickerCompletionProvider {
         let offset_to_line = buffer.point_to_offset(line_start);
         let mut lines = buffer.text_for_range(line_start..position).lines();
         if let Some(line) = lines.next() {
-            MentionCompletion::try_parse(line, offset_to_line)
-                .map(|completion| {
-                    completion.source_range.start <= offset_to_line + position.column as usize
-                        && completion.source_range.end >= offset_to_line + position.column as usize
-                })
-                .unwrap_or(false)
+            MentionCompletion::try_parse(
+                self.prompt_capabilities.get().embedded_context,
+                line,
+                offset_to_line,
+            )
+            .map(|completion| {
+                completion.source_range.start <= offset_to_line + position.column as usize
+                    && completion.source_range.end >= offset_to_line + position.column as usize
+            })
+            .unwrap_or(false)
         } else {
             false
         }
@@ -245,36 +799,69 @@ 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(cx);
+    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_icon_path: SharedString,
     crease_text: SharedString,
-    project_path: ProjectPath,
-    excerpt_id: ExcerptId,
     start: Anchor,
     content_len: usize,
-    editor: Entity<Editor>,
-    mention_set: Arc<Mutex<MentionSet>>,
+    message_editor: WeakEntity<MessageEditor>,
+    mention_uri: MentionUri,
 ) -> Arc<dyn Fn(CompletionIntent, &mut Window, &mut App) -> bool + Send + Sync> {
     Arc::new(move |_, window, cx| {
+        let message_editor = message_editor.clone();
         let crease_text = crease_text.clone();
-        let crease_icon_path = crease_icon_path.clone();
-        let editor = editor.clone();
-        let project_path = project_path.clone();
-        let mention_set = mention_set.clone();
+        let mention_uri = mention_uri.clone();
         window.defer(cx, move |window, cx| {
-            let crease_id = crate::context_picker::insert_crease_for_mention(
-                excerpt_id,
-                start,
-                content_len,
-                crease_text.clone(),
-                crease_icon_path,
-                editor.clone(),
-                window,
-                cx,
-            );
-            if let Some(crease_id) = crease_id {
-                mention_set.lock().insert(crease_id, project_path);
-            }
+            message_editor
+                .clone()
+                .update(cx, |message_editor, cx| {
+                    message_editor
+                        .confirm_completion(
+                            crease_text,
+                            start,
+                            content_len,
+                            mention_uri,
+                            window,
+                            cx,
+                        )
+                        .detach();
+                })
+                .ok();
         });
         false
     })
@@ -283,11 +870,12 @@ fn confirm_completion_callback(
 #[derive(Debug, Default, PartialEq)]
 struct MentionCompletion {
     source_range: Range<usize>,
+    mode: Option<ContextPickerMode>,
     argument: Option<String>,
 }
 
 impl MentionCompletion {
-    fn try_parse(line: &str, offset_to_line: usize) -> Option<Self> {
+    fn try_parse(allow_non_file_mentions: bool, line: &str, offset_to_line: usize) -> Option<Self> {
         let last_mention_start = line.rfind('@')?;
         if last_mention_start >= line.len() {
             return Some(Self::default());
@@ -296,23 +884,45 @@ impl MentionCompletion {
             && line
                 .chars()
                 .nth(last_mention_start - 1)
-                .map_or(false, |c| !c.is_whitespace())
+                .is_some_and(|c| !c.is_whitespace())
         {
             return None;
         }
 
         let rest_of_line = &line[last_mention_start + 1..];
+
+        let mut mode = None;
         let mut argument = None;
 
         let mut parts = rest_of_line.split_whitespace();
         let mut end = last_mention_start + 1;
-        if let Some(argument_text) = parts.next() {
-            end += argument_text.len();
-            argument = Some(argument_text.to_string());
+        if let Some(mode_text) = parts.next() {
+            end += mode_text.len();
+
+            if let Some(parsed_mode) = ContextPickerMode::try_from(mode_text).ok()
+                && (allow_non_file_mentions || matches!(parsed_mode, ContextPickerMode::File))
+            {
+                mode = Some(parsed_mode);
+            } else {
+                argument = Some(mode_text.to_string());
+            }
+            match rest_of_line[mode_text.len()..].find(|c: char| !c.is_whitespace()) {
+                Some(whitespace_count) => {
+                    if let Some(argument_text) = parts.next() {
+                        argument = Some(argument_text.to_string());
+                        end += whitespace_count + argument_text.len();
+                    }
+                }
+                None => {
+                    // Rest of line is entirely whitespace
+                    end += rest_of_line.len() - mode_text.len();
+                }
+            }
         }
 
         Some(Self {
             source_range: last_mention_start + offset_to_line..end + offset_to_line,
+            mode,
             argument,
         })
     }
@@ -321,254 +931,96 @@ impl MentionCompletion {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use gpui::{EventEmitter, FocusHandle, Focusable, TestAppContext, VisualTestContext};
-    use project::{Project, ProjectPath};
-    use serde_json::json;
-    use settings::SettingsStore;
-    use std::{ops::Deref, rc::Rc};
-    use util::path;
-    use workspace::{AppState, Item};
 
     #[test]
     fn test_mention_completion_parse() {
-        assert_eq!(MentionCompletion::try_parse("Lorem Ipsum", 0), None);
+        assert_eq!(MentionCompletion::try_parse(true, "Lorem Ipsum", 0), None);
 
         assert_eq!(
-            MentionCompletion::try_parse("Lorem @", 0),
+            MentionCompletion::try_parse(true, "Lorem @", 0),
             Some(MentionCompletion {
                 source_range: 6..7,
+                mode: None,
                 argument: None,
             })
         );
 
         assert_eq!(
-            MentionCompletion::try_parse("Lorem @main", 0),
+            MentionCompletion::try_parse(true, "Lorem @file", 0),
             Some(MentionCompletion {
                 source_range: 6..11,
-                argument: Some("main".to_string()),
+                mode: Some(ContextPickerMode::File),
+                argument: None,
             })
         );
 
-        assert_eq!(MentionCompletion::try_parse("test@", 0), None);
-    }
-
-    struct AtMentionEditor(Entity<Editor>);
-
-    impl Item for AtMentionEditor {
-        type Event = ();
-
-        fn include_in_nav_history() -> bool {
-            false
-        }
-
-        fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
-            "Test".into()
-        }
-    }
-
-    impl EventEmitter<()> for AtMentionEditor {}
-
-    impl Focusable for AtMentionEditor {
-        fn focus_handle(&self, cx: &App) -> FocusHandle {
-            self.0.read(cx).focus_handle(cx).clone()
-        }
-    }
-
-    impl Render for AtMentionEditor {
-        fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
-            self.0.clone().into_any_element()
-        }
-    }
-
-    #[gpui::test]
-    async fn test_context_completion_provider(cx: &mut TestAppContext) {
-        init_test(cx);
-
-        let app_state = cx.update(AppState::test);
-
-        cx.update(|cx| {
-            language::init(cx);
-            editor::init(cx);
-            workspace::init(app_state.clone(), cx);
-            Project::init_settings(cx);
-        });
-
-        app_state
-            .fs
-            .as_fake()
-            .insert_tree(
-                path!("/dir"),
-                json!({
-                    "editor": "",
-                    "a": {
-                        "one.txt": "",
-                        "two.txt": "",
-                        "three.txt": "",
-                        "four.txt": ""
-                    },
-                    "b": {
-                        "five.txt": "",
-                        "six.txt": "",
-                        "seven.txt": "",
-                        "eight.txt": "",
-                    }
-                }),
-            )
-            .await;
-
-        let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
-        let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
-        let workspace = window.root(cx).unwrap();
-
-        let worktree = project.update(cx, |project, cx| {
-            let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
-            assert_eq!(worktrees.len(), 1);
-            worktrees.pop().unwrap()
-        });
-        let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
-
-        let mut cx = VisualTestContext::from_window(*window.deref(), cx);
-
-        let paths = vec![
-            path!("a/one.txt"),
-            path!("a/two.txt"),
-            path!("a/three.txt"),
-            path!("a/four.txt"),
-            path!("b/five.txt"),
-            path!("b/six.txt"),
-            path!("b/seven.txt"),
-            path!("b/eight.txt"),
-        ];
-
-        let mut opened_editors = Vec::new();
-        for path in paths {
-            let buffer = workspace
-                .update_in(&mut cx, |workspace, window, cx| {
-                    workspace.open_path(
-                        ProjectPath {
-                            worktree_id,
-                            path: Path::new(path).into(),
-                        },
-                        None,
-                        false,
-                        window,
-                        cx,
-                    )
-                })
-                .await
-                .unwrap();
-            opened_editors.push(buffer);
-        }
-
-        let editor = workspace.update_in(&mut cx, |workspace, window, cx| {
-            let editor = cx.new(|cx| {
-                Editor::new(
-                    editor::EditorMode::full(),
-                    multi_buffer::MultiBuffer::build_simple("", cx),
-                    None,
-                    window,
-                    cx,
-                )
-            });
-            workspace.active_pane().update(cx, |pane, cx| {
-                pane.add_item(
-                    Box::new(cx.new(|_| AtMentionEditor(editor.clone()))),
-                    true,
-                    true,
-                    None,
-                    window,
-                    cx,
-                );
-            });
-            editor
-        });
-
-        let mention_set = Arc::new(Mutex::new(MentionSet::default()));
+        assert_eq!(
+            MentionCompletion::try_parse(true, "Lorem @file ", 0),
+            Some(MentionCompletion {
+                source_range: 6..12,
+                mode: Some(ContextPickerMode::File),
+                argument: None,
+            })
+        );
 
-        let editor_entity = editor.downgrade();
-        editor.update_in(&mut cx, |editor, window, cx| {
-            window.focus(&editor.focus_handle(cx));
-            editor.set_completion_provider(Some(Rc::new(ContextPickerCompletionProvider::new(
-                mention_set.clone(),
-                workspace.downgrade(),
-                editor_entity,
-            ))));
-        });
+        assert_eq!(
+            MentionCompletion::try_parse(true, "Lorem @file main.rs", 0),
+            Some(MentionCompletion {
+                source_range: 6..19,
+                mode: Some(ContextPickerMode::File),
+                argument: Some("main.rs".to_string()),
+            })
+        );
 
-        cx.simulate_input("Lorem ");
+        assert_eq!(
+            MentionCompletion::try_parse(true, "Lorem @file main.rs ", 0),
+            Some(MentionCompletion {
+                source_range: 6..19,
+                mode: Some(ContextPickerMode::File),
+                argument: Some("main.rs".to_string()),
+            })
+        );
 
-        editor.update(&mut cx, |editor, cx| {
-            assert_eq!(editor.text(cx), "Lorem ");
-            assert!(!editor.has_visible_completions_menu());
-        });
+        assert_eq!(
+            MentionCompletion::try_parse(true, "Lorem @file main.rs Ipsum", 0),
+            Some(MentionCompletion {
+                source_range: 6..19,
+                mode: Some(ContextPickerMode::File),
+                argument: Some("main.rs".to_string()),
+            })
+        );
 
-        cx.simulate_input("@");
-
-        editor.update(&mut cx, |editor, cx| {
-            assert_eq!(editor.text(cx), "Lorem @");
-            assert!(editor.has_visible_completions_menu());
-            assert_eq!(
-                current_completion_labels(editor),
-                &[
-                    "eight.txt dir/b/",
-                    "seven.txt dir/b/",
-                    "six.txt dir/b/",
-                    "five.txt dir/b/",
-                    "four.txt dir/a/",
-                    "three.txt dir/a/",
-                    "two.txt dir/a/",
-                    "one.txt dir/a/",
-                    "dir ",
-                    "a dir/",
-                    "four.txt dir/a/",
-                    "one.txt dir/a/",
-                    "three.txt dir/a/",
-                    "two.txt dir/a/",
-                    "b dir/",
-                    "eight.txt dir/b/",
-                    "five.txt dir/b/",
-                    "seven.txt dir/b/",
-                    "six.txt dir/b/",
-                    "editor dir/"
-                ]
-            );
-        });
+        assert_eq!(
+            MentionCompletion::try_parse(true, "Lorem @main", 0),
+            Some(MentionCompletion {
+                source_range: 6..11,
+                mode: None,
+                argument: Some("main".to_string()),
+            })
+        );
 
-        // Select and confirm "File"
-        editor.update_in(&mut cx, |editor, window, cx| {
-            assert!(editor.has_visible_completions_menu());
-            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
-            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
-            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
-            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
-            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
-        });
+        assert_eq!(MentionCompletion::try_parse(true, "test@", 0), None);
 
-        cx.run_until_parked();
+        // Allowed non-file mentions
 
-        editor.update(&mut cx, |editor, cx| {
-            assert_eq!(editor.text(cx), "Lorem [@four.txt](@file:dir/a/four.txt) ");
-        });
-    }
+        assert_eq!(
+            MentionCompletion::try_parse(true, "Lorem @symbol main", 0),
+            Some(MentionCompletion {
+                source_range: 6..18,
+                mode: Some(ContextPickerMode::Symbol),
+                argument: Some("main".to_string()),
+            })
+        );
 
-    fn current_completion_labels(editor: &Editor) -> Vec<String> {
-        let completions = editor.current_completions().expect("Missing completions");
-        completions
-            .into_iter()
-            .map(|completion| completion.label.text.to_string())
-            .collect::<Vec<_>>()
-    }
+        // Disallowed non-file mentions
 
-    pub(crate) fn init_test(cx: &mut TestAppContext) {
-        cx.update(|cx| {
-            let store = SettingsStore::test(cx);
-            cx.set_global(store);
-            theme::init(theme::LoadThemes::JustBase, cx);
-            client::init_settings(cx);
-            language::init(cx);
-            Project::init_settings(cx);
-            workspace::init_settings(cx);
-            editor::init_settings(cx);
-        });
+        assert_eq!(
+            MentionCompletion::try_parse(false, "Lorem @symbol main", 0),
+            Some(MentionCompletion {
+                source_range: 6..18,
+                mode: None,
+                argument: Some("main".to_string()),
+            })
+        );
     }
 }

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

@@ -0,0 +1,477 @@
+use std::ops::Range;
+
+use acp_thread::{AcpThread, AgentThreadEntry};
+use agent_client_protocol::ToolCallId;
+use agent2::HistoryStore;
+use collections::HashMap;
+use editor::{Editor, EditorMode, MinimapVisibility};
+use gpui::{
+    AnyEntity, App, AppContext as _, Entity, EntityId, EventEmitter, Focusable,
+    TextStyleRefinement, WeakEntity, Window,
+};
+use language::language_settings::SoftWrap;
+use project::Project;
+use prompt_store::PromptStore;
+use settings::Settings as _;
+use terminal_view::TerminalView;
+use theme::ThemeSettings;
+use ui::{Context, TextSize};
+use workspace::Workspace;
+
+use crate::acp::message_editor::{MessageEditor, MessageEditorEvent};
+
+pub struct EntryViewState {
+    workspace: WeakEntity<Workspace>,
+    project: Entity<Project>,
+    history_store: Entity<HistoryStore>,
+    prompt_store: Option<Entity<PromptStore>>,
+    entries: Vec<Entry>,
+    prevent_slash_commands: bool,
+}
+
+impl EntryViewState {
+    pub fn new(
+        workspace: WeakEntity<Workspace>,
+        project: Entity<Project>,
+        history_store: Entity<HistoryStore>,
+        prompt_store: Option<Entity<PromptStore>>,
+        prevent_slash_commands: bool,
+    ) -> Self {
+        Self {
+            workspace,
+            project,
+            history_store,
+            prompt_store,
+            entries: Vec::new(),
+            prevent_slash_commands,
+        }
+    }
+
+    pub fn entry(&self, index: usize) -> Option<&Entry> {
+        self.entries.get(index)
+    }
+
+    pub fn sync_entry(
+        &mut self,
+        index: usize,
+        thread: &Entity<AcpThread>,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        let Some(thread_entry) = thread.read(cx).entries().get(index) else {
+            return;
+        };
+
+        match thread_entry {
+            AgentThreadEntry::UserMessage(message) => {
+                let has_id = message.id.is_some();
+                let chunks = message.chunks.clone();
+                if let Some(Entry::UserMessage(editor)) = self.entries.get_mut(index) {
+                    if !editor.focus_handle(cx).is_focused(window) {
+                        // Only update if we are not editing.
+                        // If we are, cancelling the edit will set the message to the newest content.
+                        editor.update(cx, |editor, cx| {
+                            editor.set_message(chunks, window, cx);
+                        });
+                    }
+                } else {
+                    let message_editor = cx.new(|cx| {
+                        let mut editor = MessageEditor::new(
+                            self.workspace.clone(),
+                            self.project.clone(),
+                            self.history_store.clone(),
+                            self.prompt_store.clone(),
+                            "Edit message - @ to include context",
+                            self.prevent_slash_commands,
+                            editor::EditorMode::AutoHeight {
+                                min_lines: 1,
+                                max_lines: None,
+                            },
+                            window,
+                            cx,
+                        );
+                        if !has_id {
+                            editor.set_read_only(true, cx);
+                        }
+                        editor.set_message(chunks, window, cx);
+                        editor
+                    });
+                    cx.subscribe(&message_editor, move |_, editor, event, cx| {
+                        cx.emit(EntryViewEvent {
+                            entry_index: index,
+                            view_event: ViewEvent::MessageEditorEvent(editor, *event),
+                        })
+                    })
+                    .detach();
+                    self.set_entry(index, Entry::UserMessage(message_editor));
+                }
+            }
+            AgentThreadEntry::ToolCall(tool_call) => {
+                let id = tool_call.id.clone();
+                let terminals = tool_call.terminals().cloned().collect::<Vec<_>>();
+                let diffs = tool_call.diffs().cloned().collect::<Vec<_>>();
+
+                let views = if let Some(Entry::Content(views)) = self.entries.get_mut(index) {
+                    views
+                } else {
+                    self.set_entry(index, Entry::empty());
+                    let Some(Entry::Content(views)) = self.entries.get_mut(index) else {
+                        unreachable!()
+                    };
+                    views
+                };
+
+                for terminal in terminals {
+                    views.entry(terminal.entity_id()).or_insert_with(|| {
+                        let element = create_terminal(
+                            self.workspace.clone(),
+                            self.project.clone(),
+                            terminal.clone(),
+                            window,
+                            cx,
+                        )
+                        .into_any();
+                        cx.emit(EntryViewEvent {
+                            entry_index: index,
+                            view_event: ViewEvent::NewTerminal(id.clone()),
+                        });
+                        element
+                    });
+                }
+
+                for diff in diffs {
+                    views.entry(diff.entity_id()).or_insert_with(|| {
+                        let element = create_editor_diff(diff.clone(), window, cx).into_any();
+                        cx.emit(EntryViewEvent {
+                            entry_index: index,
+                            view_event: ViewEvent::NewDiff(id.clone()),
+                        });
+                        element
+                    });
+                }
+            }
+            AgentThreadEntry::AssistantMessage(_) => {
+                if index == self.entries.len() {
+                    self.entries.push(Entry::empty())
+                }
+            }
+        };
+    }
+
+    fn set_entry(&mut self, index: usize, entry: Entry) {
+        if index == self.entries.len() {
+            self.entries.push(entry);
+        } else {
+            self.entries[index] = entry;
+        }
+    }
+
+    pub fn remove(&mut self, range: Range<usize>) {
+        self.entries.drain(range);
+    }
+
+    pub fn settings_changed(&mut self, cx: &mut App) {
+        for entry in self.entries.iter() {
+            match entry {
+                Entry::UserMessage { .. } => {}
+                Entry::Content(response_views) => {
+                    for view in response_views.values() {
+                        if let Ok(diff_editor) = view.clone().downcast::<Editor>() {
+                            diff_editor.update(cx, |diff_editor, cx| {
+                                diff_editor.set_text_style_refinement(
+                                    diff_editor_text_style_refinement(cx),
+                                );
+                                cx.notify();
+                            })
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
+
+impl EventEmitter<EntryViewEvent> for EntryViewState {}
+
+pub struct EntryViewEvent {
+    pub entry_index: usize,
+    pub view_event: ViewEvent,
+}
+
+pub enum ViewEvent {
+    NewDiff(ToolCallId),
+    NewTerminal(ToolCallId),
+    MessageEditorEvent(Entity<MessageEditor>, MessageEditorEvent),
+}
+
+#[derive(Debug)]
+pub enum Entry {
+    UserMessage(Entity<MessageEditor>),
+    Content(HashMap<EntityId, AnyEntity>),
+}
+
+impl Entry {
+    pub fn message_editor(&self) -> Option<&Entity<MessageEditor>> {
+        match self {
+            Self::UserMessage(editor) => Some(editor),
+            Entry::Content(_) => None,
+        }
+    }
+
+    pub fn editor_for_diff(&self, diff: &Entity<acp_thread::Diff>) -> Option<Entity<Editor>> {
+        self.content_map()?
+            .get(&diff.entity_id())
+            .cloned()
+            .map(|entity| entity.downcast::<Editor>().unwrap())
+    }
+
+    pub fn terminal(
+        &self,
+        terminal: &Entity<acp_thread::Terminal>,
+    ) -> Option<Entity<TerminalView>> {
+        self.content_map()?
+            .get(&terminal.entity_id())
+            .cloned()
+            .map(|entity| entity.downcast::<TerminalView>().unwrap())
+    }
+
+    fn content_map(&self) -> Option<&HashMap<EntityId, AnyEntity>> {
+        match self {
+            Self::Content(map) => Some(map),
+            _ => None,
+        }
+    }
+
+    fn empty() -> Self {
+        Self::Content(HashMap::default())
+    }
+
+    #[cfg(test)]
+    pub fn has_content(&self) -> bool {
+        match self {
+            Self::Content(map) => !map.is_empty(),
+            Self::UserMessage(_) => false,
+        }
+    }
+}
+
+fn create_terminal(
+    workspace: WeakEntity<Workspace>,
+    project: Entity<Project>,
+    terminal: Entity<acp_thread::Terminal>,
+    window: &mut Window,
+    cx: &mut App,
+) -> Entity<TerminalView> {
+    cx.new(|cx| {
+        let mut view = TerminalView::new(
+            terminal.read(cx).inner().clone(),
+            workspace.clone(),
+            None,
+            project.downgrade(),
+            window,
+            cx,
+        );
+        view.set_embedded_mode(Some(1000), cx);
+        view
+    })
+}
+
+fn create_editor_diff(
+    diff: Entity<acp_thread::Diff>,
+    window: &mut Window,
+    cx: &mut App,
+) -> Entity<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,
+            },
+            diff.read(cx).multibuffer().clone(),
+            None,
+            window,
+            cx,
+        );
+        editor.set_show_gutter(false, cx);
+        editor.disable_inline_diagnostics();
+        editor.disable_expand_excerpt_buttons(cx);
+        editor.set_show_vertical_scrollbar(false, cx);
+        editor.set_minimap_visibility(MinimapVisibility::Disabled, window, cx);
+        editor.set_soft_wrap_mode(SoftWrap::None, cx);
+        editor.scroll_manager.set_forbid_vertical_scroll(true);
+        editor.set_show_indent_guides(false, cx);
+        editor.set_read_only(true);
+        editor.set_show_breakpoints(false, cx);
+        editor.set_show_code_actions(false, cx);
+        editor.set_show_git_diff_gutter(false, cx);
+        editor.set_expand_all_diff_hunks(cx);
+        editor.set_text_style_refinement(diff_editor_text_style_refinement(cx));
+        editor
+    })
+}
+
+fn diff_editor_text_style_refinement(cx: &mut App) -> TextStyleRefinement {
+    TextStyleRefinement {
+        font_size: Some(
+            TextSize::Small
+                .rems(cx)
+                .to_pixels(ThemeSettings::get_global(cx).agent_font_size(cx))
+                .into(),
+        ),
+        ..Default::default()
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use std::{path::Path, rc::Rc};
+
+    use acp_thread::{AgentConnection, StubAgentConnection};
+    use agent_client_protocol as acp;
+    use agent_settings::AgentSettings;
+    use agent2::HistoryStore;
+    use assistant_context::ContextStore;
+    use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind};
+    use editor::{EditorSettings, RowInfo};
+    use fs::FakeFs;
+    use gpui::{AppContext as _, SemanticVersion, TestAppContext};
+
+    use crate::acp::entry_view_state::EntryViewState;
+    use multi_buffer::MultiBufferRow;
+    use pretty_assertions::assert_matches;
+    use project::Project;
+    use serde_json::json;
+    use settings::{Settings as _, SettingsStore};
+    use theme::ThemeSettings;
+    use util::path;
+    use workspace::Workspace;
+
+    #[gpui::test]
+    async fn test_diff_sync(cx: &mut TestAppContext) {
+        init_test(cx);
+        let fs = FakeFs::new(cx.executor());
+        fs.insert_tree(
+            "/project",
+            json!({
+                "hello.txt": "hi world"
+            }),
+        )
+        .await;
+        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
+
+        let (workspace, cx) =
+            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
+
+        let tool_call = acp::ToolCall {
+            id: acp::ToolCallId("tool".into()),
+            title: "Tool call".into(),
+            kind: acp::ToolKind::Other,
+            status: acp::ToolCallStatus::InProgress,
+            content: vec![acp::ToolCallContent::Diff {
+                diff: acp::Diff {
+                    path: "/project/hello.txt".into(),
+                    old_text: Some("hi world".into()),
+                    new_text: "hello world".into(),
+                },
+            }],
+            locations: vec![],
+            raw_input: None,
+            raw_output: None,
+        };
+        let connection = Rc::new(StubAgentConnection::new());
+        let thread = cx
+            .update(|_, cx| {
+                connection
+                    .clone()
+                    .new_thread(project.clone(), Path::new(path!("/project")), cx)
+            })
+            .await
+            .unwrap();
+        let session_id = thread.update(cx, |thread, _| thread.session_id().clone());
+
+        cx.update(|_, cx| {
+            connection.send_update(session_id, acp::SessionUpdate::ToolCall(tool_call), cx)
+        });
+
+        let context_store = cx.new(|cx| ContextStore::fake(project.clone(), cx));
+        let history_store = cx.new(|cx| HistoryStore::new(context_store, cx));
+
+        let view_state = cx.new(|_cx| {
+            EntryViewState::new(
+                workspace.downgrade(),
+                project.clone(),
+                history_store,
+                None,
+                false,
+            )
+        });
+
+        view_state.update_in(cx, |view_state, window, cx| {
+            view_state.sync_entry(0, &thread, window, cx)
+        });
+
+        let diff = thread.read_with(cx, |thread, _cx| {
+            thread
+                .entries()
+                .get(0)
+                .unwrap()
+                .diffs()
+                .next()
+                .unwrap()
+                .clone()
+        });
+
+        cx.run_until_parked();
+
+        let diff_editor = view_state.read_with(cx, |view_state, _cx| {
+            view_state.entry(0).unwrap().editor_for_diff(&diff).unwrap()
+        });
+        assert_eq!(
+            diff_editor.read_with(cx, |editor, cx| editor.text(cx)),
+            "hi world\nhello world"
+        );
+        let row_infos = diff_editor.read_with(cx, |editor, cx| {
+            let multibuffer = editor.buffer().read(cx);
+            multibuffer
+                .snapshot(cx)
+                .row_infos(MultiBufferRow(0))
+                .collect::<Vec<_>>()
+        });
+        assert_matches!(
+            row_infos.as_slice(),
+            [
+                RowInfo {
+                    multibuffer_row: Some(MultiBufferRow(0)),
+                    diff_status: Some(DiffHunkStatus {
+                        kind: DiffHunkStatusKind::Deleted,
+                        ..
+                    }),
+                    ..
+                },
+                RowInfo {
+                    multibuffer_row: Some(MultiBufferRow(1)),
+                    diff_status: Some(DiffHunkStatus {
+                        kind: DiffHunkStatusKind::Added,
+                        ..
+                    }),
+                    ..
+                }
+            ]
+        );
+    }
+
+    fn init_test(cx: &mut TestAppContext) {
+        cx.update(|cx| {
+            let settings_store = SettingsStore::test(cx);
+            cx.set_global(settings_store);
+            language::init(cx);
+            Project::init_settings(cx);
+            AgentSettings::register(cx);
+            workspace::init_settings(cx);
+            ThemeSettings::register(cx);
+            release_channel::init(SemanticVersion::default(), cx);
+            EditorSettings::register(cx);
+        });
+    }
+}

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

@@ -0,0 +1,2464 @@
+use crate::{
+    acp::completion_provider::ContextPickerCompletionProvider,
+    context_picker::{ContextPickerAction, fetch_context_picker::fetch_url_content},
+};
+use acp_thread::{MentionUri, selection_name};
+use agent_client_protocol as acp;
+use agent_servers::AgentServer;
+use agent2::HistoryStore;
+use anyhow::{Context as _, Result, anyhow};
+use assistant_slash_commands::codeblock_fence_for_path;
+use collections::{HashMap, HashSet};
+use editor::{
+    Addon, Anchor, AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement,
+    EditorEvent, EditorMode, EditorStyle, ExcerptId, FoldPlaceholder, MultiBuffer,
+    SemanticsProvider, ToOffset,
+    actions::Paste,
+    display_map::{Crease, CreaseId, FoldId},
+};
+use futures::{
+    FutureExt as _, TryFutureExt as _,
+    future::{Shared, join_all, try_join_all},
+};
+use gpui::{
+    AppContext, ClipboardEntry, Context, Entity, EventEmitter, FocusHandle, Focusable,
+    HighlightStyle, Image, ImageFormat, Img, KeyContext, Subscription, Task, TextStyle,
+    UnderlineStyle, WeakEntity,
+};
+use language::{Buffer, Language};
+use language_model::LanguageModelImage;
+use project::{CompletionIntent, Project, ProjectItem, ProjectPath, Worktree};
+use prompt_store::PromptStore;
+use rope::Point;
+use settings::Settings;
+use std::{
+    cell::Cell,
+    ffi::OsStr,
+    fmt::Write,
+    ops::Range,
+    path::{Path, PathBuf},
+    rc::Rc,
+    sync::Arc,
+    time::Duration,
+};
+use text::{OffsetRangeExt, ToOffset as _};
+use theme::ThemeSettings;
+use ui::{
+    ActiveTheme, AnyElement, App, ButtonCommon, ButtonLike, ButtonStyle, Color, Icon, IconName,
+    IconSize, InteractiveElement, IntoElement, Label, LabelCommon, LabelSize, ParentElement,
+    Render, SelectableButton, SharedString, Styled, TextSize, TintColor, Toggleable, Window, div,
+    h_flex, px,
+};
+use url::Url;
+use util::ResultExt;
+use workspace::{
+    Toast, Workspace,
+    notifications::{NotificationId, NotifyResultExt as _},
+};
+use zed_actions::agent::Chat;
+
+const PARSE_SLASH_COMMAND_DEBOUNCE: Duration = Duration::from_millis(50);
+
+pub struct MessageEditor {
+    mention_set: MentionSet,
+    editor: Entity<Editor>,
+    project: Entity<Project>,
+    workspace: WeakEntity<Workspace>,
+    history_store: Entity<HistoryStore>,
+    prompt_store: Option<Entity<PromptStore>>,
+    prevent_slash_commands: bool,
+    prompt_capabilities: Rc<Cell<acp::PromptCapabilities>>,
+    _subscriptions: Vec<Subscription>,
+    _parse_slash_command_task: Task<()>,
+}
+
+#[derive(Clone, Copy, Debug)]
+pub enum MessageEditorEvent {
+    Send,
+    Cancel,
+    Focus,
+}
+
+impl EventEmitter<MessageEditorEvent> for MessageEditor {}
+
+impl MessageEditor {
+    pub fn new(
+        workspace: WeakEntity<Workspace>,
+        project: Entity<Project>,
+        history_store: Entity<HistoryStore>,
+        prompt_store: Option<Entity<PromptStore>>,
+        placeholder: impl Into<Arc<str>>,
+        prevent_slash_commands: bool,
+        mode: EditorMode,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) -> Self {
+        let language = Language::new(
+            language::LanguageConfig {
+                completion_query_characters: HashSet::from_iter(['.', '-', '_', '@']),
+                ..Default::default()
+            },
+            None,
+        );
+        let prompt_capabilities = Rc::new(Cell::new(acp::PromptCapabilities::default()));
+        let completion_provider = ContextPickerCompletionProvider::new(
+            cx.weak_entity(),
+            workspace.clone(),
+            history_store.clone(),
+            prompt_store.clone(),
+            prompt_capabilities.clone(),
+        );
+        let semantics_provider = Rc::new(SlashCommandSemanticsProvider {
+            range: Cell::new(None),
+        });
+        let mention_set = MentionSet::default();
+        let editor = cx.new(|cx| {
+            let buffer = cx.new(|cx| Buffer::local("", cx).with_language(Arc::new(language), cx));
+            let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
+
+            let mut editor = Editor::new(mode, buffer, None, window, cx);
+            editor.set_placeholder_text(placeholder, cx);
+            editor.set_show_indent_guides(false, cx);
+            editor.set_soft_wrap();
+            editor.set_use_modal_editing(true);
+            editor.set_completion_provider(Some(Rc::new(completion_provider)));
+            editor.set_context_menu_options(ContextMenuOptions {
+                min_entries_visible: 12,
+                max_entries_visible: 12,
+                placement: Some(ContextMenuPlacement::Above),
+            });
+            if prevent_slash_commands {
+                editor.set_semantics_provider(Some(semantics_provider.clone()));
+            }
+            editor.register_addon(MessageEditorAddon::new());
+            editor
+        });
+
+        cx.on_focus(&editor.focus_handle(cx), window, |_, _, cx| {
+            cx.emit(MessageEditorEvent::Focus)
+        })
+        .detach();
+
+        let mut subscriptions = Vec::new();
+        if prevent_slash_commands {
+            subscriptions.push(cx.subscribe_in(&editor, window, {
+                let semantics_provider = semantics_provider.clone();
+                move |this, editor, event, window, cx| {
+                    if let EditorEvent::Edited { .. } = event {
+                        this.highlight_slash_command(
+                            semantics_provider.clone(),
+                            editor.clone(),
+                            window,
+                            cx,
+                        );
+                    }
+                }
+            }));
+        }
+
+        Self {
+            editor,
+            project,
+            mention_set,
+            workspace,
+            history_store,
+            prompt_store,
+            prevent_slash_commands,
+            prompt_capabilities,
+            _subscriptions: subscriptions,
+            _parse_slash_command_task: Task::ready(()),
+        }
+    }
+
+    pub fn insert_thread_summary(
+        &mut self,
+        thread: agent2::DbThreadMetadata,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        let start = self.editor.update(cx, |editor, cx| {
+            editor.set_text(format!("{}\n", thread.title), window, cx);
+            editor
+                .buffer()
+                .read(cx)
+                .snapshot(cx)
+                .anchor_before(Point::zero())
+                .text_anchor
+        });
+
+        self.confirm_completion(
+            thread.title.clone(),
+            start,
+            thread.title.len(),
+            MentionUri::Thread {
+                id: thread.id.clone(),
+                name: thread.title.to_string(),
+            },
+            window,
+            cx,
+        )
+        .detach();
+    }
+
+    pub fn set_prompt_capabilities(&mut self, capabilities: acp::PromptCapabilities) {
+        self.prompt_capabilities.set(capabilities);
+    }
+
+    #[cfg(test)]
+    pub(crate) fn editor(&self) -> &Entity<Editor> {
+        &self.editor
+    }
+
+    #[cfg(test)]
+    pub(crate) fn mention_set(&mut self) -> &mut MentionSet {
+        &mut self.mention_set
+    }
+
+    pub fn is_empty(&self, cx: &App) -> bool {
+        self.editor.read(cx).is_empty(cx)
+    }
+
+    pub fn mentions(&self) -> HashSet<MentionUri> {
+        self.mention_set
+            .uri_by_crease_id
+            .values()
+            .cloned()
+            .collect()
+    }
+
+    pub fn confirm_completion(
+        &mut self,
+        crease_text: SharedString,
+        start: text::Anchor,
+        content_len: usize,
+        mention_uri: MentionUri,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) -> Task<()> {
+        let snapshot = self
+            .editor
+            .update(cx, |editor, cx| editor.snapshot(window, cx));
+        let Some((excerpt_id, _, _)) = snapshot.buffer_snapshot.as_singleton() else {
+            return Task::ready(());
+        };
+        let Some(start_anchor) = snapshot
+            .buffer_snapshot
+            .anchor_in_excerpt(*excerpt_id, start)
+        else {
+            return Task::ready(());
+        };
+
+        if let MentionUri::File { abs_path, .. } = &mention_uri {
+            let extension = abs_path
+                .extension()
+                .and_then(OsStr::to_str)
+                .unwrap_or_default();
+
+            if Img::extensions().contains(&extension) && !extension.contains("svg") {
+                if !self.prompt_capabilities.get().image {
+                    struct ImagesNotAllowed;
+
+                    let end_anchor = snapshot.buffer_snapshot.anchor_before(
+                        start_anchor.to_offset(&snapshot.buffer_snapshot) + content_len + 1,
+                    );
+
+                    self.editor.update(cx, |editor, cx| {
+                        // Remove mention
+                        editor.edit([((start_anchor..end_anchor), "")], cx);
+                    });
+
+                    self.workspace
+                        .update(cx, |workspace, cx| {
+                            workspace.show_toast(
+                                Toast::new(
+                                    NotificationId::unique::<ImagesNotAllowed>(),
+                                    "This agent does not support images yet",
+                                )
+                                .autohide(),
+                                cx,
+                            );
+                        })
+                        .ok();
+                    return Task::ready(());
+                }
+
+                let project = self.project.clone();
+                let Some(project_path) = project
+                    .read(cx)
+                    .project_path_for_absolute_path(abs_path, cx)
+                else {
+                    return Task::ready(());
+                };
+                let image = cx
+                    .spawn(async move |_, cx| {
+                        let image = project
+                            .update(cx, |project, cx| project.open_image(project_path, cx))
+                            .map_err(|e| e.to_string())?
+                            .await
+                            .map_err(|e| e.to_string())?;
+                        image
+                            .read_with(cx, |image, _cx| image.image.clone())
+                            .map_err(|e| e.to_string())
+                    })
+                    .shared();
+                let Some(crease_id) = insert_crease_for_image(
+                    *excerpt_id,
+                    start,
+                    content_len,
+                    Some(abs_path.as_path().into()),
+                    image.clone(),
+                    self.editor.clone(),
+                    window,
+                    cx,
+                ) else {
+                    return Task::ready(());
+                };
+                return self.confirm_mention_for_image(
+                    crease_id,
+                    start_anchor,
+                    Some(abs_path.clone()),
+                    image,
+                    window,
+                    cx,
+                );
+            }
+        }
+
+        let Some(crease_id) = crate::context_picker::insert_crease_for_mention(
+            *excerpt_id,
+            start,
+            content_len,
+            crease_text,
+            mention_uri.icon_path(cx),
+            self.editor.clone(),
+            window,
+            cx,
+        ) else {
+            return Task::ready(());
+        };
+
+        match mention_uri {
+            MentionUri::Fetch { url } => {
+                self.confirm_mention_for_fetch(crease_id, start_anchor, url, window, cx)
+            }
+            MentionUri::Directory { abs_path } => {
+                self.confirm_mention_for_directory(crease_id, start_anchor, abs_path, window, cx)
+            }
+            MentionUri::Thread { id, name } => {
+                self.confirm_mention_for_thread(crease_id, start_anchor, id, name, window, cx)
+            }
+            MentionUri::TextThread { path, name } => self.confirm_mention_for_text_thread(
+                crease_id,
+                start_anchor,
+                path,
+                name,
+                window,
+                cx,
+            ),
+            MentionUri::File { .. }
+            | MentionUri::Symbol { .. }
+            | MentionUri::Rule { .. }
+            | MentionUri::Selection { .. } => {
+                self.mention_set.insert_uri(crease_id, mention_uri.clone());
+                Task::ready(())
+            }
+        }
+    }
+
+    fn confirm_mention_for_directory(
+        &mut self,
+        crease_id: CreaseId,
+        anchor: Anchor,
+        abs_path: PathBuf,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) -> Task<()> {
+        fn collect_files_in_path(worktree: &Worktree, path: &Path) -> Vec<(Arc<Path>, PathBuf)> {
+            let mut files = Vec::new();
+
+            for entry in worktree.child_entries(path) {
+                if entry.is_dir() {
+                    files.extend(collect_files_in_path(worktree, &entry.path));
+                } else if entry.is_file() {
+                    files.push((entry.path.clone(), worktree.full_path(&entry.path)));
+                }
+            }
+
+            files
+        }
+
+        let uri = MentionUri::Directory {
+            abs_path: abs_path.clone(),
+        };
+        let Some(project_path) = self
+            .project
+            .read(cx)
+            .project_path_for_absolute_path(&abs_path, cx)
+        else {
+            return Task::ready(());
+        };
+        let Some(entry) = self.project.read(cx).entry_for_path(&project_path, cx) else {
+            return Task::ready(());
+        };
+        let Some(worktree) = self.project.read(cx).worktree_for_entry(entry.id, cx) else {
+            return Task::ready(());
+        };
+        let project = self.project.clone();
+        let task = cx.spawn(async move |_, cx| {
+            let directory_path = entry.path.clone();
+
+            let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id())?;
+            let file_paths = worktree.read_with(cx, |worktree, _cx| {
+                collect_files_in_path(worktree, &directory_path)
+            })?;
+            let descendants_future = cx.update(|cx| {
+                join_all(file_paths.into_iter().map(|(worktree_path, full_path)| {
+                    let rel_path = worktree_path
+                        .strip_prefix(&directory_path)
+                        .log_err()
+                        .map_or_else(|| worktree_path.clone(), |rel_path| rel_path.into());
+
+                    let open_task = project.update(cx, |project, cx| {
+                        project.buffer_store().update(cx, |buffer_store, cx| {
+                            let project_path = ProjectPath {
+                                worktree_id,
+                                path: worktree_path,
+                            };
+                            buffer_store.open_buffer(project_path, cx)
+                        })
+                    });
+
+                    // TODO: report load errors instead of just logging
+                    let rope_task = cx.spawn(async move |cx| {
+                        let buffer = open_task.await.log_err()?;
+                        let rope = buffer
+                            .read_with(cx, |buffer, _cx| buffer.as_rope().clone())
+                            .log_err()?;
+                        Some((rope, buffer))
+                    });
+
+                    cx.background_spawn(async move {
+                        let (rope, buffer) = rope_task.await?;
+                        Some((rel_path, full_path, rope.to_string(), buffer))
+                    })
+                }))
+            })?;
+
+            let contents = cx
+                .background_spawn(async move {
+                    let (contents, tracked_buffers) = descendants_future
+                        .await
+                        .into_iter()
+                        .flatten()
+                        .map(|(rel_path, full_path, rope, buffer)| {
+                            ((rel_path, full_path, rope), buffer)
+                        })
+                        .unzip();
+                    (render_directory_contents(contents), tracked_buffers)
+                })
+                .await;
+            anyhow::Ok(contents)
+        });
+        let task = cx
+            .spawn(async move |_, _| task.await.map_err(|e| e.to_string()))
+            .shared();
+
+        self.mention_set
+            .directories
+            .insert(abs_path.clone(), task.clone());
+
+        let editor = self.editor.clone();
+        cx.spawn_in(window, async move |this, cx| {
+            if task.await.notify_async_err(cx).is_some() {
+                this.update(cx, |this, _| {
+                    this.mention_set.insert_uri(crease_id, uri);
+                })
+                .ok();
+            } else {
+                editor
+                    .update(cx, |editor, cx| {
+                        editor.display_map.update(cx, |display_map, cx| {
+                            display_map.unfold_intersecting(vec![anchor..anchor], true, cx);
+                        });
+                        editor.remove_creases([crease_id], cx);
+                    })
+                    .ok();
+                this.update(cx, |this, _cx| {
+                    this.mention_set.directories.remove(&abs_path);
+                })
+                .ok();
+            }
+        })
+    }
+
+    fn confirm_mention_for_fetch(
+        &mut self,
+        crease_id: CreaseId,
+        anchor: Anchor,
+        url: url::Url,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) -> Task<()> {
+        let Some(http_client) = self
+            .workspace
+            .update(cx, |workspace, _cx| workspace.client().http_client())
+            .ok()
+        else {
+            return Task::ready(());
+        };
+
+        let url_string = url.to_string();
+        let fetch = cx
+            .background_executor()
+            .spawn(async move {
+                fetch_url_content(http_client, url_string)
+                    .map_err(|e| e.to_string())
+                    .await
+            })
+            .shared();
+        self.mention_set
+            .add_fetch_result(url.clone(), fetch.clone());
+
+        cx.spawn_in(window, async move |this, cx| {
+            let fetch = fetch.await.notify_async_err(cx);
+            this.update(cx, |this, cx| {
+                if fetch.is_some() {
+                    this.mention_set
+                        .insert_uri(crease_id, MentionUri::Fetch { url });
+                } else {
+                    // Remove crease if we failed to fetch
+                    this.editor.update(cx, |editor, cx| {
+                        editor.display_map.update(cx, |display_map, cx| {
+                            display_map.unfold_intersecting(vec![anchor..anchor], true, cx);
+                        });
+                        editor.remove_creases([crease_id], cx);
+                    });
+                    this.mention_set.fetch_results.remove(&url);
+                }
+            })
+            .ok();
+        })
+    }
+
+    pub fn confirm_mention_for_selection(
+        &mut self,
+        source_range: Range<text::Anchor>,
+        selections: Vec<(Entity<Buffer>, Range<text::Anchor>, Range<usize>)>,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        let snapshot = self.editor.read(cx).buffer().read(cx).snapshot(cx);
+        let Some((&excerpt_id, _, _)) = snapshot.as_singleton() else {
+            return;
+        };
+        let Some(start) = snapshot.anchor_in_excerpt(excerpt_id, source_range.start) else {
+            return;
+        };
+
+        let offset = start.to_offset(&snapshot);
+
+        for (buffer, selection_range, range_to_fold) in selections {
+            let range = snapshot.anchor_after(offset + range_to_fold.start)
+                ..snapshot.anchor_after(offset + range_to_fold.end);
+
+            // TODO support selections from buffers with no path
+            let Some(project_path) = buffer.read(cx).project_path(cx) else {
+                continue;
+            };
+            let Some(abs_path) = self.project.read(cx).absolute_path(&project_path, cx) else {
+                continue;
+            };
+            let snapshot = buffer.read(cx).snapshot();
+
+            let point_range = selection_range.to_point(&snapshot);
+            let line_range = point_range.start.row..point_range.end.row;
+
+            let uri = MentionUri::Selection {
+                path: abs_path.clone(),
+                line_range: line_range.clone(),
+            };
+            let crease = crate::context_picker::crease_for_mention(
+                selection_name(&abs_path, &line_range).into(),
+                uri.icon_path(cx),
+                range,
+                self.editor.downgrade(),
+            );
+
+            let crease_id = self.editor.update(cx, |editor, cx| {
+                let crease_ids = editor.insert_creases(vec![crease.clone()], cx);
+                editor.fold_creases(vec![crease], false, window, cx);
+                crease_ids.first().copied().unwrap()
+            });
+
+            self.mention_set.insert_uri(crease_id, uri);
+        }
+    }
+
+    fn confirm_mention_for_thread(
+        &mut self,
+        crease_id: CreaseId,
+        anchor: Anchor,
+        id: acp::SessionId,
+        name: String,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) -> Task<()> {
+        let uri = MentionUri::Thread {
+            id: id.clone(),
+            name,
+        };
+        let server = Rc::new(agent2::NativeAgentServer::new(
+            self.project.read(cx).fs().clone(),
+            self.history_store.clone(),
+        ));
+        let connection = server.connect(Path::new(""), &self.project, cx);
+        let load_summary = cx.spawn({
+            let id = id.clone();
+            async move |_, cx| {
+                let agent = connection.await?;
+                let agent = agent.downcast::<agent2::NativeAgentConnection>().unwrap();
+                let summary = agent
+                    .0
+                    .update(cx, |agent, cx| agent.thread_summary(id, cx))?
+                    .await?;
+                anyhow::Ok(summary)
+            }
+        });
+        let task = cx
+            .spawn(async move |_, _| load_summary.await.map_err(|e| format!("{e}")))
+            .shared();
+
+        self.mention_set.insert_thread(id.clone(), task.clone());
+        self.mention_set.insert_uri(crease_id, uri);
+
+        let editor = self.editor.clone();
+        cx.spawn_in(window, async move |this, cx| {
+            if task.await.notify_async_err(cx).is_none() {
+                editor
+                    .update(cx, |editor, cx| {
+                        editor.display_map.update(cx, |display_map, cx| {
+                            display_map.unfold_intersecting(vec![anchor..anchor], true, cx);
+                        });
+                        editor.remove_creases([crease_id], cx);
+                    })
+                    .ok();
+                this.update(cx, |this, _| {
+                    this.mention_set.thread_summaries.remove(&id);
+                    this.mention_set.uri_by_crease_id.remove(&crease_id);
+                })
+                .ok();
+            }
+        })
+    }
+
+    fn confirm_mention_for_text_thread(
+        &mut self,
+        crease_id: CreaseId,
+        anchor: Anchor,
+        path: PathBuf,
+        name: String,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) -> Task<()> {
+        let uri = MentionUri::TextThread {
+            path: path.clone(),
+            name,
+        };
+        let context = self.history_store.update(cx, |text_thread_store, cx| {
+            text_thread_store.load_text_thread(path.as_path().into(), cx)
+        });
+        let task = cx
+            .spawn(async move |_, cx| {
+                let context = context.await.map_err(|e| e.to_string())?;
+                let xml = context
+                    .update(cx, |context, cx| context.to_xml(cx))
+                    .map_err(|e| e.to_string())?;
+                Ok(xml)
+            })
+            .shared();
+
+        self.mention_set
+            .insert_text_thread(path.clone(), task.clone());
+
+        let editor = self.editor.clone();
+        cx.spawn_in(window, async move |this, cx| {
+            if task.await.notify_async_err(cx).is_some() {
+                this.update(cx, |this, _| {
+                    this.mention_set.insert_uri(crease_id, uri);
+                })
+                .ok();
+            } else {
+                editor
+                    .update(cx, |editor, cx| {
+                        editor.display_map.update(cx, |display_map, cx| {
+                            display_map.unfold_intersecting(vec![anchor..anchor], true, cx);
+                        });
+                        editor.remove_creases([crease_id], cx);
+                    })
+                    .ok();
+                this.update(cx, |this, _| {
+                    this.mention_set.text_thread_summaries.remove(&path);
+                })
+                .ok();
+            }
+        })
+    }
+
+    pub fn contents(
+        &self,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) -> Task<Result<(Vec<acp::ContentBlock>, Vec<Entity<Buffer>>)>> {
+        let contents = self.mention_set.contents(
+            &self.project,
+            self.prompt_store.as_ref(),
+            &self.prompt_capabilities.get(),
+            window,
+            cx,
+        );
+        let editor = self.editor.clone();
+        let prevent_slash_commands = self.prevent_slash_commands;
+
+        cx.spawn(async move |_, cx| {
+            let contents = contents.await?;
+            let mut all_tracked_buffers = Vec::new();
+
+            editor.update(cx, |editor, cx| {
+                let mut ix = 0;
+                let mut chunks: Vec<acp::ContentBlock> = Vec::new();
+                let text = editor.text(cx);
+                editor.display_map.update(cx, |map, cx| {
+                    let snapshot = map.snapshot(cx);
+                    for (crease_id, crease) in snapshot.crease_snapshot.creases() {
+                        // Skip creases that have been edited out of the message buffer.
+                        if !crease.range().start.is_valid(&snapshot.buffer_snapshot) {
+                            continue;
+                        }
+
+                        let Some(mention) = contents.get(&crease_id) else {
+                            continue;
+                        };
+
+                        let crease_range = crease.range().to_offset(&snapshot.buffer_snapshot);
+                        if crease_range.start > ix {
+                            let chunk = if prevent_slash_commands
+                                && ix == 0
+                                && parse_slash_command(&text[ix..]).is_some()
+                            {
+                                format!(" {}", &text[ix..crease_range.start]).into()
+                            } else {
+                                text[ix..crease_range.start].into()
+                            };
+                            chunks.push(chunk);
+                        }
+                        let chunk = match mention {
+                            Mention::Text {
+                                uri,
+                                content,
+                                tracked_buffers,
+                            } => {
+                                all_tracked_buffers.extend(tracked_buffers.iter().cloned());
+                                acp::ContentBlock::Resource(acp::EmbeddedResource {
+                                    annotations: None,
+                                    resource: acp::EmbeddedResourceResource::TextResourceContents(
+                                        acp::TextResourceContents {
+                                            mime_type: None,
+                                            text: content.clone(),
+                                            uri: uri.to_uri().to_string(),
+                                        },
+                                    ),
+                                })
+                            }
+                            Mention::Image(mention_image) => {
+                                acp::ContentBlock::Image(acp::ImageContent {
+                                    annotations: None,
+                                    data: mention_image.data.to_string(),
+                                    mime_type: mention_image.format.mime_type().into(),
+                                    uri: mention_image
+                                        .abs_path
+                                        .as_ref()
+                                        .map(|path| format!("file://{}", path.display())),
+                                })
+                            }
+                            Mention::UriOnly(uri) => {
+                                acp::ContentBlock::ResourceLink(acp::ResourceLink {
+                                    name: uri.name(),
+                                    uri: uri.to_uri().to_string(),
+                                    annotations: None,
+                                    description: None,
+                                    mime_type: None,
+                                    size: None,
+                                    title: None,
+                                })
+                            }
+                        };
+                        chunks.push(chunk);
+                        ix = crease_range.end;
+                    }
+
+                    if ix < text.len() {
+                        let last_chunk = if prevent_slash_commands
+                            && ix == 0
+                            && parse_slash_command(&text[ix..]).is_some()
+                        {
+                            format!(" {}", text[ix..].trim_end())
+                        } else {
+                            text[ix..].trim_end().to_owned()
+                        };
+                        if !last_chunk.is_empty() {
+                            chunks.push(last_chunk.into());
+                        }
+                    }
+                });
+
+                (chunks, all_tracked_buffers)
+            })
+        })
+    }
+
+    pub fn clear(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+        self.editor.update(cx, |editor, cx| {
+            editor.clear(window, cx);
+            editor.remove_creases(self.mention_set.drain(), cx)
+        });
+    }
+
+    fn send(&mut self, _: &Chat, _: &mut Window, cx: &mut Context<Self>) {
+        if self.is_empty(cx) {
+            return;
+        }
+        cx.emit(MessageEditorEvent::Send)
+    }
+
+    fn cancel(&mut self, _: &editor::actions::Cancel, _: &mut Window, cx: &mut Context<Self>) {
+        cx.emit(MessageEditorEvent::Cancel)
+    }
+
+    fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
+        if !self.prompt_capabilities.get().image {
+            return;
+        }
+
+        let images = cx
+            .read_from_clipboard()
+            .map(|item| {
+                item.into_entries()
+                    .filter_map(|entry| {
+                        if let ClipboardEntry::Image(image) = entry {
+                            Some(image)
+                        } else {
+                            None
+                        }
+                    })
+                    .collect::<Vec<_>>()
+            })
+            .unwrap_or_default();
+
+        if images.is_empty() {
+            return;
+        }
+        cx.stop_propagation();
+
+        let replacement_text = "image";
+        for image in images {
+            let (excerpt_id, text_anchor, multibuffer_anchor) =
+                self.editor.update(cx, |message_editor, cx| {
+                    let snapshot = message_editor.snapshot(window, cx);
+                    let (excerpt_id, _, buffer_snapshot) =
+                        snapshot.buffer_snapshot.as_singleton().unwrap();
+
+                    let text_anchor = buffer_snapshot.anchor_before(buffer_snapshot.len());
+                    let multibuffer_anchor = snapshot
+                        .buffer_snapshot
+                        .anchor_in_excerpt(*excerpt_id, text_anchor);
+                    message_editor.edit(
+                        [(
+                            multi_buffer::Anchor::max()..multi_buffer::Anchor::max(),
+                            format!("{replacement_text} "),
+                        )],
+                        cx,
+                    );
+                    (*excerpt_id, text_anchor, multibuffer_anchor)
+                });
+
+            let content_len = replacement_text.len();
+            let Some(anchor) = multibuffer_anchor else {
+                return;
+            };
+            let task = Task::ready(Ok(Arc::new(image))).shared();
+            let Some(crease_id) = insert_crease_for_image(
+                excerpt_id,
+                text_anchor,
+                content_len,
+                None.clone(),
+                task.clone(),
+                self.editor.clone(),
+                window,
+                cx,
+            ) else {
+                return;
+            };
+            self.confirm_mention_for_image(crease_id, anchor, None, task, window, cx)
+                .detach();
+        }
+    }
+
+    pub fn insert_dragged_files(
+        &mut self,
+        paths: Vec<project::ProjectPath>,
+        added_worktrees: Vec<Entity<Worktree>>,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        let buffer = self.editor.read(cx).buffer().clone();
+        let Some(buffer) = buffer.read(cx).as_singleton() else {
+            return;
+        };
+        let mut tasks = Vec::new();
+        for path in paths {
+            let Some(entry) = self.project.read(cx).entry_for_path(&path, cx) else {
+                continue;
+            };
+            let Some(abs_path) = self.project.read(cx).absolute_path(&path, cx) else {
+                continue;
+            };
+            let path_prefix = abs_path
+                .file_name()
+                .unwrap_or(path.path.as_os_str())
+                .display()
+                .to_string();
+            let (file_name, _) =
+                crate::context_picker::file_context_picker::extract_file_name_and_directory(
+                    &path.path,
+                    &path_prefix,
+                );
+
+            let uri = if entry.is_dir() {
+                MentionUri::Directory { abs_path }
+            } else {
+                MentionUri::File { abs_path }
+            };
+
+            let new_text = format!("{} ", uri.as_link());
+            let content_len = new_text.len() - 1;
+
+            let anchor = buffer.update(cx, |buffer, _cx| buffer.anchor_before(buffer.len()));
+
+            self.editor.update(cx, |message_editor, cx| {
+                message_editor.edit(
+                    [(
+                        multi_buffer::Anchor::max()..multi_buffer::Anchor::max(),
+                        new_text,
+                    )],
+                    cx,
+                );
+            });
+            tasks.push(self.confirm_completion(file_name, anchor, content_len, uri, window, cx));
+        }
+        cx.spawn(async move |_, _| {
+            join_all(tasks).await;
+            drop(added_worktrees);
+        })
+        .detach();
+    }
+
+    pub fn insert_selections(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+        let buffer = self.editor.read(cx).buffer().clone();
+        let Some(buffer) = buffer.read(cx).as_singleton() else {
+            return;
+        };
+        let anchor = buffer.update(cx, |buffer, _cx| buffer.anchor_before(buffer.len()));
+        let Some(workspace) = self.workspace.upgrade() else {
+            return;
+        };
+        let Some(completion) = ContextPickerCompletionProvider::completion_for_action(
+            ContextPickerAction::AddSelections,
+            anchor..anchor,
+            cx.weak_entity(),
+            &workspace,
+            cx,
+        ) else {
+            return;
+        };
+        self.editor.update(cx, |message_editor, cx| {
+            message_editor.edit(
+                [(
+                    multi_buffer::Anchor::max()..multi_buffer::Anchor::max(),
+                    completion.new_text,
+                )],
+                cx,
+            );
+        });
+        if let Some(confirm) = completion.confirm {
+            confirm(CompletionIntent::Complete, window, cx);
+        }
+    }
+
+    pub fn set_read_only(&mut self, read_only: bool, cx: &mut Context<Self>) {
+        self.editor.update(cx, |message_editor, cx| {
+            message_editor.set_read_only(read_only);
+            cx.notify()
+        })
+    }
+
+    fn confirm_mention_for_image(
+        &mut self,
+        crease_id: CreaseId,
+        anchor: Anchor,
+        abs_path: Option<PathBuf>,
+        image: Shared<Task<Result<Arc<Image>, String>>>,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) -> Task<()> {
+        let editor = self.editor.clone();
+        let task = cx
+            .spawn_in(window, {
+                let abs_path = abs_path.clone();
+                async move |_, cx| {
+                    let image = image.await?;
+                    let format = image.format;
+                    let image = cx
+                        .update(|_, cx| LanguageModelImage::from_image(image, cx))
+                        .map_err(|e| e.to_string())?
+                        .await;
+                    if let Some(image) = image {
+                        Ok(MentionImage {
+                            abs_path,
+                            data: image.source,
+                            format,
+                        })
+                    } else {
+                        Err("Failed to convert image".into())
+                    }
+                }
+            })
+            .shared();
+
+        self.mention_set.insert_image(crease_id, task.clone());
+
+        cx.spawn_in(window, async move |this, cx| {
+            if task.await.notify_async_err(cx).is_some() {
+                if let Some(abs_path) = abs_path.clone() {
+                    this.update(cx, |this, _cx| {
+                        this.mention_set
+                            .insert_uri(crease_id, MentionUri::File { abs_path });
+                    })
+                    .ok();
+                }
+            } else {
+                editor
+                    .update(cx, |editor, cx| {
+                        editor.display_map.update(cx, |display_map, cx| {
+                            display_map.unfold_intersecting(vec![anchor..anchor], true, cx);
+                        });
+                        editor.remove_creases([crease_id], cx);
+                    })
+                    .ok();
+                this.update(cx, |this, _cx| {
+                    this.mention_set.images.remove(&crease_id);
+                })
+                .ok();
+            }
+        })
+    }
+
+    pub fn set_mode(&mut self, mode: EditorMode, cx: &mut Context<Self>) {
+        self.editor.update(cx, |editor, cx| {
+            editor.set_mode(mode);
+            cx.notify()
+        });
+    }
+
+    pub fn set_message(
+        &mut self,
+        message: Vec<acp::ContentBlock>,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        self.clear(window, cx);
+
+        let mut text = String::new();
+        let mut mentions = Vec::new();
+        let mut images = Vec::new();
+
+        for chunk in message {
+            match chunk {
+                acp::ContentBlock::Text(text_content) => {
+                    text.push_str(&text_content.text);
+                }
+                acp::ContentBlock::Resource(acp::EmbeddedResource {
+                    resource: acp::EmbeddedResourceResource::TextResourceContents(resource),
+                    ..
+                }) => {
+                    if let Some(mention_uri) = MentionUri::parse(&resource.uri).log_err() {
+                        let start = text.len();
+                        write!(&mut text, "{}", mention_uri.as_link()).ok();
+                        let end = text.len();
+                        mentions.push((start..end, mention_uri, resource.text));
+                    }
+                }
+                acp::ContentBlock::Image(content) => {
+                    let start = text.len();
+                    text.push_str("image");
+                    let end = text.len();
+                    images.push((start..end, content));
+                }
+                acp::ContentBlock::Audio(_)
+                | acp::ContentBlock::Resource(_)
+                | acp::ContentBlock::ResourceLink(_) => {}
+            }
+        }
+
+        let snapshot = self.editor.update(cx, |editor, cx| {
+            editor.set_text(text, window, cx);
+            editor.buffer().read(cx).snapshot(cx)
+        });
+
+        for (range, mention_uri, text) in mentions {
+            let anchor = snapshot.anchor_before(range.start);
+            let crease_id = crate::context_picker::insert_crease_for_mention(
+                anchor.excerpt_id,
+                anchor.text_anchor,
+                range.end - range.start,
+                mention_uri.name().into(),
+                mention_uri.icon_path(cx),
+                self.editor.clone(),
+                window,
+                cx,
+            );
+
+            if let Some(crease_id) = crease_id {
+                self.mention_set.insert_uri(crease_id, mention_uri.clone());
+            }
+
+            match mention_uri {
+                MentionUri::Thread { id, .. } => {
+                    self.mention_set
+                        .insert_thread(id, Task::ready(Ok(text.into())).shared());
+                }
+                MentionUri::TextThread { path, .. } => {
+                    self.mention_set
+                        .insert_text_thread(path, Task::ready(Ok(text)).shared());
+                }
+                MentionUri::Fetch { url } => {
+                    self.mention_set
+                        .add_fetch_result(url, Task::ready(Ok(text)).shared());
+                }
+                MentionUri::Directory { abs_path } => {
+                    let task = Task::ready(Ok((text, Vec::new()))).shared();
+                    self.mention_set.directories.insert(abs_path, task);
+                }
+                MentionUri::File { .. }
+                | MentionUri::Symbol { .. }
+                | MentionUri::Rule { .. }
+                | MentionUri::Selection { .. } => {}
+            }
+        }
+        for (range, content) in images {
+            let Some(format) = ImageFormat::from_mime_type(&content.mime_type) else {
+                continue;
+            };
+            let anchor = snapshot.anchor_before(range.start);
+            let abs_path = content
+                .uri
+                .as_ref()
+                .and_then(|uri| uri.strip_prefix("file://").map(|s| Path::new(s).into()));
+
+            let name = content
+                .uri
+                .as_ref()
+                .and_then(|uri| {
+                    uri.strip_prefix("file://")
+                        .and_then(|path| Path::new(path).file_name())
+                })
+                .map(|name| name.to_string_lossy().to_string())
+                .unwrap_or("Image".to_owned());
+            let crease_id = crate::context_picker::insert_crease_for_mention(
+                anchor.excerpt_id,
+                anchor.text_anchor,
+                range.end - range.start,
+                name.into(),
+                IconName::Image.path().into(),
+                self.editor.clone(),
+                window,
+                cx,
+            );
+            let data: SharedString = content.data.to_string().into();
+
+            if let Some(crease_id) = crease_id {
+                self.mention_set.insert_image(
+                    crease_id,
+                    Task::ready(Ok(MentionImage {
+                        abs_path,
+                        data,
+                        format,
+                    }))
+                    .shared(),
+                );
+            }
+        }
+        cx.notify();
+    }
+
+    fn highlight_slash_command(
+        &mut self,
+        semantics_provider: Rc<SlashCommandSemanticsProvider>,
+        editor: Entity<Editor>,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        struct InvalidSlashCommand;
+
+        self._parse_slash_command_task = cx.spawn_in(window, async move |_, cx| {
+            cx.background_executor()
+                .timer(PARSE_SLASH_COMMAND_DEBOUNCE)
+                .await;
+            editor
+                .update_in(cx, |editor, window, cx| {
+                    let snapshot = editor.snapshot(window, cx);
+                    let range = parse_slash_command(&editor.text(cx));
+                    semantics_provider.range.set(range);
+                    if let Some((start, end)) = range {
+                        editor.highlight_text::<InvalidSlashCommand>(
+                            vec![
+                                snapshot.buffer_snapshot.anchor_after(start)
+                                    ..snapshot.buffer_snapshot.anchor_before(end),
+                            ],
+                            HighlightStyle {
+                                underline: Some(UnderlineStyle {
+                                    thickness: px(1.),
+                                    color: Some(gpui::red()),
+                                    wavy: true,
+                                }),
+                                ..Default::default()
+                            },
+                            cx,
+                        );
+                    } else {
+                        editor.clear_highlights::<InvalidSlashCommand>(cx);
+                    }
+                })
+                .ok();
+        })
+    }
+
+    #[cfg(test)]
+    pub fn set_text(&mut self, text: &str, window: &mut Window, cx: &mut Context<Self>) {
+        self.editor.update(cx, |editor, cx| {
+            editor.set_text(text, window, cx);
+        });
+    }
+
+    #[cfg(test)]
+    pub fn text(&self, cx: &App) -> String {
+        self.editor.read(cx).text(cx)
+    }
+}
+
+fn render_directory_contents(entries: Vec<(Arc<Path>, PathBuf, String)>) -> String {
+    let mut output = String::new();
+    for (_relative_path, full_path, content) in entries {
+        let fence = codeblock_fence_for_path(Some(&full_path), None);
+        write!(output, "\n{fence}\n{content}\n```").unwrap();
+    }
+    output
+}
+
+impl Focusable for MessageEditor {
+    fn focus_handle(&self, cx: &App) -> FocusHandle {
+        self.editor.focus_handle(cx)
+    }
+}
+
+impl Render for MessageEditor {
+    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+        div()
+            .key_context("MessageEditor")
+            .on_action(cx.listener(Self::send))
+            .on_action(cx.listener(Self::cancel))
+            .capture_action(cx.listener(Self::paste))
+            .flex_1()
+            .child({
+                let settings = ThemeSettings::get_global(cx);
+                let font_size = TextSize::Small
+                    .rems(cx)
+                    .to_pixels(settings.agent_font_size(cx));
+                let line_height = settings.buffer_line_height.value() * font_size;
+
+                let text_style = TextStyle {
+                    color: cx.theme().colors().text,
+                    font_family: settings.buffer_font.family.clone(),
+                    font_fallbacks: settings.buffer_font.fallbacks.clone(),
+                    font_features: settings.buffer_font.features.clone(),
+                    font_size: font_size.into(),
+                    line_height: line_height.into(),
+                    ..Default::default()
+                };
+
+                EditorElement::new(
+                    &self.editor,
+                    EditorStyle {
+                        background: cx.theme().colors().editor_background,
+                        local_player: cx.theme().players().local(),
+                        text: text_style,
+                        syntax: cx.theme().syntax().clone(),
+                        ..Default::default()
+                    },
+                )
+            })
+    }
+}
+
+pub(crate) fn insert_crease_for_image(
+    excerpt_id: ExcerptId,
+    anchor: text::Anchor,
+    content_len: usize,
+    abs_path: Option<Arc<Path>>,
+    image: Shared<Task<Result<Arc<Image>, String>>>,
+    editor: Entity<Editor>,
+    window: &mut Window,
+    cx: &mut App,
+) -> Option<CreaseId> {
+    let crease_label = abs_path
+        .as_ref()
+        .and_then(|path| path.file_name())
+        .map(|name| name.to_string_lossy().to_string().into())
+        .unwrap_or(SharedString::from("Image"));
+
+    editor.update(cx, |editor, cx| {
+        let snapshot = editor.buffer().read(cx).snapshot(cx);
+
+        let start = snapshot.anchor_in_excerpt(excerpt_id, anchor)?;
+
+        let start = start.bias_right(&snapshot);
+        let end = snapshot.anchor_before(start.to_offset(&snapshot) + content_len);
+
+        let placeholder = FoldPlaceholder {
+            render: render_image_fold_icon_button(crease_label, image, cx.weak_entity()),
+            merge_adjacent: false,
+            ..Default::default()
+        };
+
+        let crease = Crease::Inline {
+            range: start..end,
+            placeholder,
+            render_toggle: None,
+            render_trailer: None,
+            metadata: None,
+        };
+
+        let ids = editor.insert_creases(vec![crease.clone()], cx);
+        editor.fold_creases(vec![crease], false, window, cx);
+
+        Some(ids[0])
+    })
+}
+
+fn render_image_fold_icon_button(
+    label: SharedString,
+    image_task: Shared<Task<Result<Arc<Image>, String>>>,
+    editor: WeakEntity<Editor>,
+) -> Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut App) -> AnyElement> {
+    Arc::new({
+        move |fold_id, fold_range, cx| {
+            let is_in_text_selection = editor
+                .update(cx, |editor, cx| editor.is_range_selected(&fold_range, cx))
+                .unwrap_or_default();
+
+            ButtonLike::new(fold_id)
+                .style(ButtonStyle::Filled)
+                .selected_style(ButtonStyle::Tinted(TintColor::Accent))
+                .toggle_state(is_in_text_selection)
+                .child(
+                    h_flex()
+                        .gap_1()
+                        .child(
+                            Icon::new(IconName::Image)
+                                .size(IconSize::XSmall)
+                                .color(Color::Muted),
+                        )
+                        .child(
+                            Label::new(label.clone())
+                                .size(LabelSize::Small)
+                                .buffer_font(cx)
+                                .single_line(),
+                        ),
+                )
+                .hoverable_tooltip({
+                    let image_task = image_task.clone();
+                    move |_, cx| {
+                        let image = image_task.peek().cloned().transpose().ok().flatten();
+                        let image_task = image_task.clone();
+                        cx.new::<ImageHover>(|cx| ImageHover {
+                            image,
+                            _task: cx.spawn(async move |this, cx| {
+                                if let Ok(image) = image_task.clone().await {
+                                    this.update(cx, |this, cx| {
+                                        if this.image.replace(image).is_none() {
+                                            cx.notify();
+                                        }
+                                    })
+                                    .ok();
+                                }
+                            }),
+                        })
+                        .into()
+                    }
+                })
+                .into_any_element()
+        }
+    })
+}
+
+struct ImageHover {
+    image: Option<Arc<Image>>,
+    _task: Task<()>,
+}
+
+impl Render for ImageHover {
+    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
+        if let Some(image) = self.image.clone() {
+            gpui::img(image).max_w_96().max_h_96().into_any_element()
+        } else {
+            gpui::Empty.into_any_element()
+        }
+    }
+}
+
+#[derive(Debug, Eq, PartialEq)]
+pub enum Mention {
+    Text {
+        uri: MentionUri,
+        content: String,
+        tracked_buffers: Vec<Entity<Buffer>>,
+    },
+    Image(MentionImage),
+    UriOnly(MentionUri),
+}
+
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct MentionImage {
+    pub abs_path: Option<PathBuf>,
+    pub data: SharedString,
+    pub format: ImageFormat,
+}
+
+#[derive(Default)]
+pub struct MentionSet {
+    uri_by_crease_id: HashMap<CreaseId, MentionUri>,
+    fetch_results: HashMap<Url, Shared<Task<Result<String, String>>>>,
+    images: HashMap<CreaseId, Shared<Task<Result<MentionImage, String>>>>,
+    thread_summaries: HashMap<acp::SessionId, Shared<Task<Result<SharedString, String>>>>,
+    text_thread_summaries: HashMap<PathBuf, Shared<Task<Result<String, String>>>>,
+    directories: HashMap<PathBuf, Shared<Task<Result<(String, Vec<Entity<Buffer>>), String>>>>,
+}
+
+impl MentionSet {
+    pub fn insert_uri(&mut self, crease_id: CreaseId, uri: MentionUri) {
+        self.uri_by_crease_id.insert(crease_id, uri);
+    }
+
+    pub fn add_fetch_result(&mut self, url: Url, content: Shared<Task<Result<String, String>>>) {
+        self.fetch_results.insert(url, content);
+    }
+
+    pub fn insert_image(
+        &mut self,
+        crease_id: CreaseId,
+        task: Shared<Task<Result<MentionImage, String>>>,
+    ) {
+        self.images.insert(crease_id, task);
+    }
+
+    fn insert_thread(
+        &mut self,
+        id: acp::SessionId,
+        task: Shared<Task<Result<SharedString, String>>>,
+    ) {
+        self.thread_summaries.insert(id, task);
+    }
+
+    fn insert_text_thread(&mut self, path: PathBuf, task: Shared<Task<Result<String, String>>>) {
+        self.text_thread_summaries.insert(path, task);
+    }
+
+    pub fn drain(&mut self) -> impl Iterator<Item = CreaseId> {
+        self.fetch_results.clear();
+        self.thread_summaries.clear();
+        self.text_thread_summaries.clear();
+        self.directories.clear();
+        self.uri_by_crease_id
+            .drain()
+            .map(|(id, _)| id)
+            .chain(self.images.drain().map(|(id, _)| id))
+    }
+
+    pub fn contents(
+        &self,
+        project: &Entity<Project>,
+        prompt_store: Option<&Entity<PromptStore>>,
+        prompt_capabilities: &acp::PromptCapabilities,
+        _window: &mut Window,
+        cx: &mut App,
+    ) -> Task<Result<HashMap<CreaseId, Mention>>> {
+        if !prompt_capabilities.embedded_context {
+            let mentions = self
+                .uri_by_crease_id
+                .iter()
+                .map(|(crease_id, uri)| (*crease_id, Mention::UriOnly(uri.clone())))
+                .collect();
+
+            return Task::ready(Ok(mentions));
+        }
+
+        let mut processed_image_creases = HashSet::default();
+
+        let mut contents = self
+            .uri_by_crease_id
+            .iter()
+            .map(|(&crease_id, uri)| {
+                match uri {
+                    MentionUri::File { abs_path, .. } => {
+                        let uri = uri.clone();
+                        let abs_path = abs_path.to_path_buf();
+
+                        if let Some(task) = self.images.get(&crease_id).cloned() {
+                            processed_image_creases.insert(crease_id);
+                            return cx.spawn(async move |_| {
+                                let image = task.await.map_err(|e| anyhow!("{e}"))?;
+                                anyhow::Ok((crease_id, Mention::Image(image)))
+                            });
+                        }
+
+                        let buffer_task = project.update(cx, |project, cx| {
+                            let path = project
+                                .find_project_path(abs_path, cx)
+                                .context("Failed to find project path")?;
+                            anyhow::Ok(project.open_buffer(path, cx))
+                        });
+                        cx.spawn(async move |cx| {
+                            let buffer = buffer_task?.await?;
+                            let content = buffer.read_with(cx, |buffer, _cx| buffer.text())?;
+
+                            anyhow::Ok((
+                                crease_id,
+                                Mention::Text {
+                                    uri,
+                                    content,
+                                    tracked_buffers: vec![buffer],
+                                },
+                            ))
+                        })
+                    }
+                    MentionUri::Directory { abs_path } => {
+                        let Some(content) = self.directories.get(abs_path).cloned() else {
+                            return Task::ready(Err(anyhow!("missing directory load task")));
+                        };
+                        let uri = uri.clone();
+                        cx.spawn(async move |_| {
+                            let (content, tracked_buffers) =
+                                content.await.map_err(|e| anyhow::anyhow!("{e}"))?;
+                            Ok((
+                                crease_id,
+                                Mention::Text {
+                                    uri,
+                                    content,
+                                    tracked_buffers,
+                                },
+                            ))
+                        })
+                    }
+                    MentionUri::Symbol {
+                        path, line_range, ..
+                    }
+                    | MentionUri::Selection {
+                        path, line_range, ..
+                    } => {
+                        let uri = uri.clone();
+                        let path_buf = path.clone();
+                        let line_range = line_range.clone();
+
+                        let buffer_task = project.update(cx, |project, cx| {
+                            let path = project
+                                .find_project_path(&path_buf, cx)
+                                .context("Failed to find project path")?;
+                            anyhow::Ok(project.open_buffer(path, cx))
+                        });
+
+                        cx.spawn(async move |cx| {
+                            let buffer = buffer_task?.await?;
+                            let content = buffer.read_with(cx, |buffer, _cx| {
+                                buffer
+                                    .text_for_range(
+                                        Point::new(line_range.start, 0)
+                                            ..Point::new(
+                                                line_range.end,
+                                                buffer.line_len(line_range.end),
+                                            ),
+                                    )
+                                    .collect()
+                            })?;
+
+                            anyhow::Ok((
+                                crease_id,
+                                Mention::Text {
+                                    uri,
+                                    content,
+                                    tracked_buffers: vec![buffer],
+                                },
+                            ))
+                        })
+                    }
+                    MentionUri::Thread { id, .. } => {
+                        let Some(content) = self.thread_summaries.get(id).cloned() else {
+                            return Task::ready(Err(anyhow!("missing thread summary")));
+                        };
+                        let uri = uri.clone();
+                        cx.spawn(async move |_| {
+                            Ok((
+                                crease_id,
+                                Mention::Text {
+                                    uri,
+                                    content: content
+                                        .await
+                                        .map_err(|e| anyhow::anyhow!("{e}"))?
+                                        .to_string(),
+                                    tracked_buffers: Vec::new(),
+                                },
+                            ))
+                        })
+                    }
+                    MentionUri::TextThread { path, .. } => {
+                        let Some(content) = self.text_thread_summaries.get(path).cloned() else {
+                            return Task::ready(Err(anyhow!("missing text thread summary")));
+                        };
+                        let uri = uri.clone();
+                        cx.spawn(async move |_| {
+                            Ok((
+                                crease_id,
+                                Mention::Text {
+                                    uri,
+                                    content: content.await.map_err(|e| anyhow::anyhow!("{e}"))?,
+                                    tracked_buffers: Vec::new(),
+                                },
+                            ))
+                        })
+                    }
+                    MentionUri::Rule { id: prompt_id, .. } => {
+                        let Some(prompt_store) = prompt_store else {
+                            return Task::ready(Err(anyhow!("missing prompt store")));
+                        };
+                        let text_task = prompt_store.read(cx).load(*prompt_id, cx);
+                        let uri = uri.clone();
+                        cx.spawn(async move |_| {
+                            // TODO: report load errors instead of just logging
+                            let text = text_task.await?;
+                            anyhow::Ok((
+                                crease_id,
+                                Mention::Text {
+                                    uri,
+                                    content: text,
+                                    tracked_buffers: Vec::new(),
+                                },
+                            ))
+                        })
+                    }
+                    MentionUri::Fetch { url } => {
+                        let Some(content) = self.fetch_results.get(url).cloned() else {
+                            return Task::ready(Err(anyhow!("missing fetch result")));
+                        };
+                        let uri = uri.clone();
+                        cx.spawn(async move |_| {
+                            Ok((
+                                crease_id,
+                                Mention::Text {
+                                    uri,
+                                    content: content.await.map_err(|e| anyhow::anyhow!("{e}"))?,
+                                    tracked_buffers: Vec::new(),
+                                },
+                            ))
+                        })
+                    }
+                }
+            })
+            .collect::<Vec<_>>();
+
+        // Handle images that didn't have a mention URI (because they were added by the paste handler).
+        contents.extend(self.images.iter().filter_map(|(crease_id, image)| {
+            if processed_image_creases.contains(crease_id) {
+                return None;
+            }
+            let crease_id = *crease_id;
+            let image = image.clone();
+            Some(cx.spawn(async move |_| {
+                Ok((
+                    crease_id,
+                    Mention::Image(image.await.map_err(|e| anyhow::anyhow!("{e}"))?),
+                ))
+            }))
+        }));
+
+        cx.spawn(async move |_cx| {
+            let contents = try_join_all(contents).await?.into_iter().collect();
+            anyhow::Ok(contents)
+        })
+    }
+}
+
+struct SlashCommandSemanticsProvider {
+    range: Cell<Option<(usize, usize)>>,
+}
+
+impl SemanticsProvider for SlashCommandSemanticsProvider {
+    fn hover(
+        &self,
+        buffer: &Entity<Buffer>,
+        position: text::Anchor,
+        cx: &mut App,
+    ) -> Option<Task<Option<Vec<project::Hover>>>> {
+        let snapshot = buffer.read(cx).snapshot();
+        let offset = position.to_offset(&snapshot);
+        let (start, end) = self.range.get()?;
+        if !(start..end).contains(&offset) {
+            return None;
+        }
+        let range = snapshot.anchor_after(start)..snapshot.anchor_after(end);
+        Some(Task::ready(Some(vec![project::Hover {
+            contents: vec![project::HoverBlock {
+                text: "Slash commands are not supported".into(),
+                kind: project::HoverBlockKind::PlainText,
+            }],
+            range: Some(range),
+            language: None,
+        }])))
+    }
+
+    fn inline_values(
+        &self,
+        _buffer_handle: Entity<Buffer>,
+        _range: Range<text::Anchor>,
+        _cx: &mut App,
+    ) -> Option<Task<anyhow::Result<Vec<project::InlayHint>>>> {
+        None
+    }
+
+    fn inlay_hints(
+        &self,
+        _buffer_handle: Entity<Buffer>,
+        _range: Range<text::Anchor>,
+        _cx: &mut App,
+    ) -> Option<Task<anyhow::Result<Vec<project::InlayHint>>>> {
+        None
+    }
+
+    fn resolve_inlay_hint(
+        &self,
+        _hint: project::InlayHint,
+        _buffer_handle: Entity<Buffer>,
+        _server_id: lsp::LanguageServerId,
+        _cx: &mut App,
+    ) -> Option<Task<anyhow::Result<project::InlayHint>>> {
+        None
+    }
+
+    fn supports_inlay_hints(&self, _buffer: &Entity<Buffer>, _cx: &mut App) -> bool {
+        false
+    }
+
+    fn document_highlights(
+        &self,
+        _buffer: &Entity<Buffer>,
+        _position: text::Anchor,
+        _cx: &mut App,
+    ) -> Option<Task<Result<Vec<project::DocumentHighlight>>>> {
+        None
+    }
+
+    fn definitions(
+        &self,
+        _buffer: &Entity<Buffer>,
+        _position: text::Anchor,
+        _kind: editor::GotoDefinitionKind,
+        _cx: &mut App,
+    ) -> Option<Task<Result<Option<Vec<project::LocationLink>>>>> {
+        None
+    }
+
+    fn range_for_rename(
+        &self,
+        _buffer: &Entity<Buffer>,
+        _position: text::Anchor,
+        _cx: &mut App,
+    ) -> Option<Task<Result<Option<Range<text::Anchor>>>>> {
+        None
+    }
+
+    fn perform_rename(
+        &self,
+        _buffer: &Entity<Buffer>,
+        _position: text::Anchor,
+        _new_name: String,
+        _cx: &mut App,
+    ) -> Option<Task<Result<project::ProjectTransaction>>> {
+        None
+    }
+}
+
+fn parse_slash_command(text: &str) -> Option<(usize, usize)> {
+    if let Some(remainder) = text.strip_prefix('/') {
+        let pos = remainder
+            .find(char::is_whitespace)
+            .unwrap_or(remainder.len());
+        let command = &remainder[..pos];
+        if !command.is_empty() && command.chars().all(char::is_alphanumeric) {
+            return Some((0, 1 + command.len()));
+        }
+    }
+    None
+}
+
+pub struct MessageEditorAddon {}
+
+impl MessageEditorAddon {
+    pub fn new() -> Self {
+        Self {}
+    }
+}
+
+impl Addon for MessageEditorAddon {
+    fn to_any(&self) -> &dyn std::any::Any {
+        self
+    }
+
+    fn to_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
+        Some(self)
+    }
+
+    fn extend_key_context(&self, key_context: &mut KeyContext, cx: &App) {
+        let settings = agent_settings::AgentSettings::get_global(cx);
+        if settings.use_modifier_to_send {
+            key_context.add("use_modifier_to_send");
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use std::{ops::Range, path::Path, sync::Arc};
+
+    use acp_thread::MentionUri;
+    use agent_client_protocol as acp;
+    use agent2::HistoryStore;
+    use assistant_context::ContextStore;
+    use editor::{AnchorRangeExt as _, Editor, EditorMode};
+    use fs::FakeFs;
+    use futures::StreamExt as _;
+    use gpui::{
+        AppContext, Entity, EventEmitter, FocusHandle, Focusable, TestAppContext, VisualTestContext,
+    };
+    use lsp::{CompletionContext, CompletionTriggerKind};
+    use project::{CompletionIntent, Project, ProjectPath};
+    use serde_json::json;
+    use text::Point;
+    use ui::{App, Context, IntoElement, Render, SharedString, Window};
+    use util::{path, uri};
+    use workspace::{AppState, Item, Workspace};
+
+    use crate::acp::{
+        message_editor::{Mention, MessageEditor},
+        thread_view::tests::init_test,
+    };
+
+    #[gpui::test]
+    async fn test_at_mention_removal(cx: &mut TestAppContext) {
+        init_test(cx);
+
+        let fs = FakeFs::new(cx.executor());
+        fs.insert_tree("/project", json!({"file": ""})).await;
+        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
+
+        let (workspace, cx) =
+            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
+
+        let context_store = cx.new(|cx| ContextStore::fake(project.clone(), cx));
+        let history_store = cx.new(|cx| HistoryStore::new(context_store, cx));
+
+        let message_editor = cx.update(|window, cx| {
+            cx.new(|cx| {
+                MessageEditor::new(
+                    workspace.downgrade(),
+                    project.clone(),
+                    history_store.clone(),
+                    None,
+                    "Test",
+                    false,
+                    EditorMode::AutoHeight {
+                        min_lines: 1,
+                        max_lines: None,
+                    },
+                    window,
+                    cx,
+                )
+            })
+        });
+        let editor = message_editor.update(cx, |message_editor, _| message_editor.editor.clone());
+
+        cx.run_until_parked();
+
+        let excerpt_id = editor.update(cx, |editor, cx| {
+            editor
+                .buffer()
+                .read(cx)
+                .excerpt_ids()
+                .into_iter()
+                .next()
+                .unwrap()
+        });
+        let completions = editor.update_in(cx, |editor, window, cx| {
+            editor.set_text("Hello @file ", window, cx);
+            let buffer = editor.buffer().read(cx).as_singleton().unwrap();
+            let completion_provider = editor.completion_provider().unwrap();
+            completion_provider.completions(
+                excerpt_id,
+                &buffer,
+                text::Anchor::MAX,
+                CompletionContext {
+                    trigger_kind: CompletionTriggerKind::TRIGGER_CHARACTER,
+                    trigger_character: Some("@".into()),
+                },
+                window,
+                cx,
+            )
+        });
+        let [_, completion]: [_; 2] = completions
+            .await
+            .unwrap()
+            .into_iter()
+            .flat_map(|response| response.completions)
+            .collect::<Vec<_>>()
+            .try_into()
+            .unwrap();
+
+        editor.update_in(cx, |editor, window, cx| {
+            let snapshot = editor.buffer().read(cx).snapshot(cx);
+            let start = snapshot
+                .anchor_in_excerpt(excerpt_id, completion.replace_range.start)
+                .unwrap();
+            let end = snapshot
+                .anchor_in_excerpt(excerpt_id, completion.replace_range.end)
+                .unwrap();
+            editor.edit([(start..end, completion.new_text)], cx);
+            (completion.confirm.unwrap())(CompletionIntent::Complete, window, cx);
+        });
+
+        cx.run_until_parked();
+
+        // Backspace over the inserted crease (and the following space).
+        editor.update_in(cx, |editor, window, cx| {
+            editor.backspace(&Default::default(), window, cx);
+            editor.backspace(&Default::default(), window, cx);
+        });
+
+        let (content, _) = message_editor
+            .update_in(cx, |message_editor, window, cx| {
+                message_editor.contents(window, cx)
+            })
+            .await
+            .unwrap();
+
+        // We don't send a resource link for the deleted crease.
+        pretty_assertions::assert_matches!(content.as_slice(), [acp::ContentBlock::Text { .. }]);
+    }
+
+    struct MessageEditorItem(Entity<MessageEditor>);
+
+    impl Item for MessageEditorItem {
+        type Event = ();
+
+        fn include_in_nav_history() -> bool {
+            false
+        }
+
+        fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
+            "Test".into()
+        }
+    }
+
+    impl EventEmitter<()> for MessageEditorItem {}
+
+    impl Focusable for MessageEditorItem {
+        fn focus_handle(&self, cx: &App) -> FocusHandle {
+            self.0.read(cx).focus_handle(cx)
+        }
+    }
+
+    impl Render for MessageEditorItem {
+        fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
+            self.0.clone().into_any_element()
+        }
+    }
+
+    #[gpui::test]
+    async fn test_context_completion_provider(cx: &mut TestAppContext) {
+        init_test(cx);
+
+        let app_state = cx.update(AppState::test);
+
+        cx.update(|cx| {
+            language::init(cx);
+            editor::init(cx);
+            workspace::init(app_state.clone(), cx);
+            Project::init_settings(cx);
+        });
+
+        app_state
+            .fs
+            .as_fake()
+            .insert_tree(
+                path!("/dir"),
+                json!({
+                    "editor": "",
+                    "a": {
+                        "one.txt": "1",
+                        "two.txt": "2",
+                        "three.txt": "3",
+                        "four.txt": "4"
+                    },
+                    "b": {
+                        "five.txt": "5",
+                        "six.txt": "6",
+                        "seven.txt": "7",
+                        "eight.txt": "8",
+                    }
+                }),
+            )
+            .await;
+
+        let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
+        let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
+        let workspace = window.root(cx).unwrap();
+
+        let worktree = project.update(cx, |project, cx| {
+            let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
+            assert_eq!(worktrees.len(), 1);
+            worktrees.pop().unwrap()
+        });
+        let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
+
+        let mut cx = VisualTestContext::from_window(*window, cx);
+
+        let paths = vec![
+            path!("a/one.txt"),
+            path!("a/two.txt"),
+            path!("a/three.txt"),
+            path!("a/four.txt"),
+            path!("b/five.txt"),
+            path!("b/six.txt"),
+            path!("b/seven.txt"),
+            path!("b/eight.txt"),
+        ];
+
+        let mut opened_editors = Vec::new();
+        for path in paths {
+            let buffer = workspace
+                .update_in(&mut cx, |workspace, window, cx| {
+                    workspace.open_path(
+                        ProjectPath {
+                            worktree_id,
+                            path: Path::new(path).into(),
+                        },
+                        None,
+                        false,
+                        window,
+                        cx,
+                    )
+                })
+                .await
+                .unwrap();
+            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 (message_editor, editor) = workspace.update_in(&mut cx, |workspace, window, cx| {
+            let workspace_handle = cx.weak_entity();
+            let message_editor = cx.new(|cx| {
+                MessageEditor::new(
+                    workspace_handle,
+                    project.clone(),
+                    history_store.clone(),
+                    None,
+                    "Test",
+                    false,
+                    EditorMode::AutoHeight {
+                        max_lines: None,
+                        min_lines: 1,
+                    },
+                    window,
+                    cx,
+                )
+            });
+            workspace.active_pane().update(cx, |pane, cx| {
+                pane.add_item(
+                    Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))),
+                    true,
+                    true,
+                    None,
+                    window,
+                    cx,
+                );
+            });
+            message_editor.read(cx).focus_handle(cx).focus(window);
+            let editor = message_editor.read(cx).editor().clone();
+            (message_editor, editor)
+        });
+
+        cx.simulate_input("Lorem @");
+
+        editor.update_in(&mut cx, |editor, window, cx| {
+            assert_eq!(editor.text(cx), "Lorem @");
+            assert!(editor.has_visible_completions_menu());
+
+            // Only files since we have default capabilities
+            assert_eq!(
+                current_completion_labels(editor),
+                &[
+                    "eight.txt dir/b/",
+                    "seven.txt dir/b/",
+                    "six.txt dir/b/",
+                    "five.txt dir/b/",
+                ]
+            );
+            editor.set_text("", window, cx);
+        });
+
+        message_editor.update(&mut cx, |editor, _cx| {
+            // Enable all prompt capabilities
+            editor.set_prompt_capabilities(acp::PromptCapabilities {
+                image: true,
+                audio: true,
+                embedded_context: true,
+            });
+        });
+
+        cx.simulate_input("Lorem ");
+
+        editor.update(&mut cx, |editor, cx| {
+            assert_eq!(editor.text(cx), "Lorem ");
+            assert!(!editor.has_visible_completions_menu());
+        });
+
+        cx.simulate_input("@");
+
+        editor.update(&mut cx, |editor, cx| {
+            assert_eq!(editor.text(cx), "Lorem @");
+            assert!(editor.has_visible_completions_menu());
+            assert_eq!(
+                current_completion_labels(editor),
+                &[
+                    "eight.txt dir/b/",
+                    "seven.txt dir/b/",
+                    "six.txt dir/b/",
+                    "five.txt dir/b/",
+                    "Files & Directories",
+                    "Symbols",
+                    "Threads",
+                    "Fetch"
+                ]
+            );
+        });
+
+        // Select and confirm "File"
+        editor.update_in(&mut cx, |editor, window, cx| {
+            assert!(editor.has_visible_completions_menu());
+            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
+            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
+            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
+            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
+            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
+        });
+
+        cx.run_until_parked();
+
+        editor.update(&mut cx, |editor, cx| {
+            assert_eq!(editor.text(cx), "Lorem @file ");
+            assert!(editor.has_visible_completions_menu());
+        });
+
+        cx.simulate_input("one");
+
+        editor.update(&mut cx, |editor, cx| {
+            assert_eq!(editor.text(cx), "Lorem @file one");
+            assert!(editor.has_visible_completions_menu());
+            assert_eq!(current_completion_labels(editor), vec!["one.txt dir/a/"]);
+        });
+
+        editor.update_in(&mut cx, |editor, window, cx| {
+            assert!(editor.has_visible_completions_menu());
+            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
+        });
+
+        let url_one = uri!("file:///dir/a/one.txt");
+        editor.update(&mut cx, |editor, cx| {
+            let text = editor.text(cx);
+            assert_eq!(text, format!("Lorem [@one.txt]({url_one}) "));
+            assert!(!editor.has_visible_completions_menu());
+            assert_eq!(fold_ranges(editor, cx).len(), 1);
+        });
+
+        let all_prompt_capabilities = acp::PromptCapabilities {
+            image: true,
+            audio: true,
+            embedded_context: true,
+        };
+
+        let contents = message_editor
+            .update_in(&mut cx, |message_editor, window, cx| {
+                message_editor.mention_set().contents(
+                    &project,
+                    None,
+                    &all_prompt_capabilities,
+                    window,
+                    cx,
+                )
+            })
+            .await
+            .unwrap()
+            .into_values()
+            .collect::<Vec<_>>();
+
+        {
+            let [Mention::Text { content, uri, .. }] = contents.as_slice() else {
+                panic!("Unexpected mentions");
+            };
+            pretty_assertions::assert_eq!(content, "1");
+            pretty_assertions::assert_eq!(uri, &url_one.parse::<MentionUri>().unwrap());
+        }
+
+        let contents = message_editor
+            .update_in(&mut cx, |message_editor, window, cx| {
+                message_editor.mention_set().contents(
+                    &project,
+                    None,
+                    &acp::PromptCapabilities::default(),
+                    window,
+                    cx,
+                )
+            })
+            .await
+            .unwrap()
+            .into_values()
+            .collect::<Vec<_>>();
+
+        {
+            let [Mention::UriOnly(uri)] = contents.as_slice() else {
+                panic!("Unexpected mentions");
+            };
+            pretty_assertions::assert_eq!(uri, &url_one.parse::<MentionUri>().unwrap());
+        }
+
+        cx.simulate_input(" ");
+
+        editor.update(&mut cx, |editor, cx| {
+            let text = editor.text(cx);
+            assert_eq!(text, format!("Lorem [@one.txt]({url_one})  "));
+            assert!(!editor.has_visible_completions_menu());
+            assert_eq!(fold_ranges(editor, cx).len(), 1);
+        });
+
+        cx.simulate_input("Ipsum ");
+
+        editor.update(&mut cx, |editor, cx| {
+            let text = editor.text(cx);
+            assert_eq!(text, format!("Lorem [@one.txt]({url_one})  Ipsum "),);
+            assert!(!editor.has_visible_completions_menu());
+            assert_eq!(fold_ranges(editor, cx).len(), 1);
+        });
+
+        cx.simulate_input("@file ");
+
+        editor.update(&mut cx, |editor, cx| {
+            let text = editor.text(cx);
+            assert_eq!(text, format!("Lorem [@one.txt]({url_one})  Ipsum @file "),);
+            assert!(editor.has_visible_completions_menu());
+            assert_eq!(fold_ranges(editor, cx).len(), 1);
+        });
+
+        editor.update_in(&mut cx, |editor, window, cx| {
+            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
+        });
+
+        cx.run_until_parked();
+
+        let contents = message_editor
+            .update_in(&mut cx, |message_editor, window, cx| {
+                message_editor.mention_set().contents(
+                    &project,
+                    None,
+                    &all_prompt_capabilities,
+                    window,
+                    cx,
+                )
+            })
+            .await
+            .unwrap()
+            .into_values()
+            .collect::<Vec<_>>();
+
+        let url_eight = uri!("file:///dir/b/eight.txt");
+
+        {
+            let [_, Mention::Text { content, uri, .. }] = contents.as_slice() else {
+                panic!("Unexpected mentions");
+            };
+            pretty_assertions::assert_eq!(content, "8");
+            pretty_assertions::assert_eq!(uri, &url_eight.parse::<MentionUri>().unwrap());
+        }
+
+        editor.update(&mut cx, |editor, cx| {
+            assert_eq!(
+                editor.text(cx),
+                format!("Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) ")
+            );
+            assert!(!editor.has_visible_completions_menu());
+            assert_eq!(fold_ranges(editor, cx).len(), 2);
+        });
+
+        let plain_text_language = Arc::new(language::Language::new(
+            language::LanguageConfig {
+                name: "Plain Text".into(),
+                matcher: language::LanguageMatcher {
+                    path_suffixes: vec!["txt".to_string()],
+                    ..Default::default()
+                },
+                ..Default::default()
+            },
+            None,
+        ));
+
+        // Register the language and fake LSP
+        let language_registry = project.read_with(&cx, |project, _| project.languages().clone());
+        language_registry.add(plain_text_language);
+
+        let mut fake_language_servers = language_registry.register_fake_lsp(
+            "Plain Text",
+            language::FakeLspAdapter {
+                capabilities: lsp::ServerCapabilities {
+                    workspace_symbol_provider: Some(lsp::OneOf::Left(true)),
+                    ..Default::default()
+                },
+                ..Default::default()
+            },
+        );
+
+        // Open the buffer to trigger LSP initialization
+        let buffer = project
+            .update(&mut cx, |project, cx| {
+                project.open_local_buffer(path!("/dir/a/one.txt"), cx)
+            })
+            .await
+            .unwrap();
+
+        // Register the buffer with language servers
+        let _handle = project.update(&mut cx, |project, cx| {
+            project.register_buffer_with_language_servers(&buffer, cx)
+        });
+
+        cx.run_until_parked();
+
+        let fake_language_server = fake_language_servers.next().await.unwrap();
+        fake_language_server.set_request_handler::<lsp::WorkspaceSymbolRequest, _, _>(
+            move |_, _| async move {
+                Ok(Some(lsp::WorkspaceSymbolResponse::Flat(vec![
+                    #[allow(deprecated)]
+                    lsp::SymbolInformation {
+                        name: "MySymbol".into(),
+                        location: lsp::Location {
+                            uri: lsp::Url::from_file_path(path!("/dir/a/one.txt")).unwrap(),
+                            range: lsp::Range::new(
+                                lsp::Position::new(0, 0),
+                                lsp::Position::new(0, 1),
+                            ),
+                        },
+                        kind: lsp::SymbolKind::CONSTANT,
+                        tags: None,
+                        container_name: None,
+                        deprecated: None,
+                    },
+                ])))
+            },
+        );
+
+        cx.simulate_input("@symbol ");
+
+        editor.update(&mut cx, |editor, cx| {
+            assert_eq!(
+                editor.text(cx),
+                format!("Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) @symbol ")
+            );
+            assert!(editor.has_visible_completions_menu());
+            assert_eq!(current_completion_labels(editor), &["MySymbol"]);
+        });
+
+        editor.update_in(&mut cx, |editor, window, cx| {
+            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
+        });
+
+        let contents = message_editor
+            .update_in(&mut cx, |message_editor, window, cx| {
+                message_editor.mention_set().contents(
+                    &project,
+                    None,
+                    &all_prompt_capabilities,
+                    window,
+                    cx,
+                )
+            })
+            .await
+            .unwrap()
+            .into_values()
+            .collect::<Vec<_>>();
+
+        {
+            let [_, _, Mention::Text { content, uri, .. }] = contents.as_slice() else {
+                panic!("Unexpected mentions");
+            };
+            pretty_assertions::assert_eq!(content, "1");
+            pretty_assertions::assert_eq!(
+                uri,
+                &format!("{url_one}?symbol=MySymbol#L1:1")
+                    .parse::<MentionUri>()
+                    .unwrap()
+            );
+        }
+
+        cx.run_until_parked();
+
+        editor.read_with(&cx, |editor, cx| {
+                assert_eq!(
+                    editor.text(cx),
+                    format!("Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) [@MySymbol]({url_one}?symbol=MySymbol#L1:1) ")
+                );
+            });
+    }
+
+    fn fold_ranges(editor: &Editor, cx: &mut App) -> Vec<Range<Point>> {
+        let snapshot = editor.buffer().read(cx).snapshot(cx);
+        editor.display_map.update(cx, |display_map, cx| {
+            display_map
+                .snapshot(cx)
+                .folds_in_range(0..snapshot.len())
+                .map(|fold| fold.range.to_point(&snapshot))
+                .collect()
+        })
+    }
+
+    fn current_completion_labels(editor: &Editor) -> Vec<String> {
+        let completions = editor.current_completions().expect("Missing completions");
+        completions
+            .into_iter()
+            .map(|completion| completion.label.text)
+            .collect::<Vec<_>>()
+    }
+}

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

@@ -1,92 +0,0 @@
-pub struct MessageHistory<T> {
-    items: Vec<T>,
-    current: Option<usize>,
-}
-
-impl<T> Default for MessageHistory<T> {
-    fn default() -> Self {
-        MessageHistory {
-            items: Vec::new(),
-            current: None,
-        }
-    }
-}
-
-impl<T> MessageHistory<T> {
-    pub fn push(&mut self, message: T) {
-        self.current.take();
-        self.items.push(message);
-    }
-
-    pub fn reset_position(&mut self) {
-        self.current.take();
-    }
-
-    pub fn prev(&mut self) -> Option<&T> {
-        if self.items.is_empty() {
-            return None;
-        }
-
-        let new_ix = self
-            .current
-            .get_or_insert(self.items.len())
-            .saturating_sub(1);
-
-        self.current = Some(new_ix);
-        self.items.get(new_ix)
-    }
-
-    pub fn next(&mut self) -> Option<&T> {
-        let current = self.current.as_mut()?;
-        *current += 1;
-
-        self.items.get(*current).or_else(|| {
-            self.current.take();
-            None
-        })
-    }
-
-    #[cfg(test)]
-    pub fn items(&self) -> &[T] {
-        &self.items
-    }
-}
-#[cfg(test)]
-mod tests {
-    use super::*;
-
-    #[test]
-    fn test_prev_next() {
-        let mut history = MessageHistory::default();
-
-        // Test empty history
-        assert_eq!(history.prev(), None);
-        assert_eq!(history.next(), None);
-
-        // Add some messages
-        history.push("first");
-        history.push("second");
-        history.push("third");
-
-        // Test prev navigation
-        assert_eq!(history.prev(), Some(&"third"));
-        assert_eq!(history.prev(), Some(&"second"));
-        assert_eq!(history.prev(), Some(&"first"));
-        assert_eq!(history.prev(), Some(&"first"));
-
-        assert_eq!(history.next(), Some(&"second"));
-
-        // Test mixed navigation
-        history.push("fourth");
-        assert_eq!(history.prev(), Some(&"fourth"));
-        assert_eq!(history.prev(), Some(&"third"));
-        assert_eq!(history.next(), Some(&"fourth"));
-        assert_eq!(history.next(), None);
-
-        // Test that push resets navigation
-        history.prev();
-        history.prev();
-        history.push("fifth");
-        assert_eq!(history.prev(), Some(&"fifth"));
-    }
-}

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

@@ -0,0 +1,472 @@
+use std::{cmp::Reverse, rc::Rc, sync::Arc};
+
+use acp_thread::{AgentModelInfo, AgentModelList, AgentModelSelector};
+use agent_client_protocol as acp;
+use anyhow::Result;
+use collections::IndexMap;
+use futures::FutureExt;
+use fuzzy::{StringMatchCandidate, match_strings};
+use gpui::{Action, AsyncWindowContext, BackgroundExecutor, DismissEvent, Task, WeakEntity};
+use ordered_float::OrderedFloat;
+use picker::{Picker, PickerDelegate};
+use ui::{
+    AnyElement, App, Context, IntoElement, ListItem, ListItemSpacing, SharedString, Window,
+    prelude::*, rems,
+};
+use util::ResultExt;
+
+pub type AcpModelSelector = Picker<AcpModelPickerDelegate>;
+
+pub fn acp_model_selector(
+    session_id: acp::SessionId,
+    selector: Rc<dyn AgentModelSelector>,
+    window: &mut Window,
+    cx: &mut Context<AcpModelSelector>,
+) -> AcpModelSelector {
+    let delegate = AcpModelPickerDelegate::new(session_id, selector, window, cx);
+    Picker::list(delegate, window, cx)
+        .show_scrollbar(true)
+        .width(rems(20.))
+        .max_height(Some(rems(20.).into()))
+}
+
+enum AcpModelPickerEntry {
+    Separator(SharedString),
+    Model(AgentModelInfo),
+}
+
+pub struct AcpModelPickerDelegate {
+    session_id: acp::SessionId,
+    selector: Rc<dyn AgentModelSelector>,
+    filtered_entries: Vec<AcpModelPickerEntry>,
+    models: Option<AgentModelList>,
+    selected_index: usize,
+    selected_model: Option<AgentModelInfo>,
+    _refresh_models_task: Task<()>,
+}
+
+impl AcpModelPickerDelegate {
+    fn new(
+        session_id: acp::SessionId,
+        selector: Rc<dyn AgentModelSelector>,
+        window: &mut Window,
+        cx: &mut Context<AcpModelSelector>,
+    ) -> Self {
+        let mut rx = selector.watch(cx);
+        let refresh_models_task = cx.spawn_in(window, {
+            let session_id = session_id.clone();
+            async move |this, cx| {
+                async fn refresh(
+                    this: &WeakEntity<Picker<AcpModelPickerDelegate>>,
+                    session_id: &acp::SessionId,
+                    cx: &mut AsyncWindowContext,
+                ) -> Result<()> {
+                    let (models_task, selected_model_task) = this.update(cx, |this, cx| {
+                        (
+                            this.delegate.selector.list_models(cx),
+                            this.delegate.selector.selected_model(session_id, cx),
+                        )
+                    })?;
+
+                    let (models, selected_model) = futures::join!(models_task, selected_model_task);
+
+                    this.update_in(cx, |this, window, cx| {
+                        this.delegate.models = models.ok();
+                        this.delegate.selected_model = selected_model.ok();
+                        this.delegate.update_matches(this.query(cx), window, cx)
+                    })?
+                    .await;
+
+                    Ok(())
+                }
+
+                refresh(&this, &session_id, cx).await.log_err();
+                while let Ok(()) = rx.recv().await {
+                    refresh(&this, &session_id, cx).await.log_err();
+                }
+            }
+        });
+
+        Self {
+            session_id,
+            selector,
+            filtered_entries: Vec::new(),
+            models: None,
+            selected_model: None,
+            selected_index: 0,
+            _refresh_models_task: refresh_models_task,
+        }
+    }
+
+    pub fn active_model(&self) -> Option<&AgentModelInfo> {
+        self.selected_model.as_ref()
+    }
+}
+
+impl PickerDelegate for AcpModelPickerDelegate {
+    type ListItem = AnyElement;
+
+    fn match_count(&self) -> usize {
+        self.filtered_entries.len()
+    }
+
+    fn selected_index(&self) -> usize {
+        self.selected_index
+    }
+
+    fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context<Picker<Self>>) {
+        self.selected_index = ix.min(self.filtered_entries.len().saturating_sub(1));
+        cx.notify();
+    }
+
+    fn can_select(
+        &mut self,
+        ix: usize,
+        _window: &mut Window,
+        _cx: &mut Context<Picker<Self>>,
+    ) -> bool {
+        match self.filtered_entries.get(ix) {
+            Some(AcpModelPickerEntry::Model(_)) => true,
+            Some(AcpModelPickerEntry::Separator(_)) | None => false,
+        }
+    }
+
+    fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
+        "Select a model…".into()
+    }
+
+    fn update_matches(
+        &mut self,
+        query: String,
+        window: &mut Window,
+        cx: &mut Context<Picker<Self>>,
+    ) -> Task<()> {
+        cx.spawn_in(window, async move |this, cx| {
+            let filtered_models = match this
+                .read_with(cx, |this, cx| {
+                    this.delegate.models.clone().map(move |models| {
+                        fuzzy_search(models, query, cx.background_executor().clone())
+                    })
+                })
+                .ok()
+                .flatten()
+            {
+                Some(task) => task.await,
+                None => AgentModelList::Flat(vec![]),
+            };
+
+            this.update_in(cx, |this, window, cx| {
+                this.delegate.filtered_entries =
+                    info_list_to_picker_entries(filtered_models).collect();
+                // Finds the currently selected model in the list
+                let new_index = this
+                    .delegate
+                    .selected_model
+                    .as_ref()
+                    .and_then(|selected| {
+                        this.delegate.filtered_entries.iter().position(|entry| {
+                            if let AcpModelPickerEntry::Model(model_info) = entry {
+                                model_info.id == selected.id
+                            } else {
+                                false
+                            }
+                        })
+                    })
+                    .unwrap_or(0);
+                this.set_selected_index(new_index, Some(picker::Direction::Down), true, window, cx);
+                cx.notify();
+            })
+            .ok();
+        })
+    }
+
+    fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
+        if let Some(AcpModelPickerEntry::Model(model_info)) =
+            self.filtered_entries.get(self.selected_index)
+        {
+            self.selector
+                .select_model(self.session_id.clone(), model_info.id.clone(), cx)
+                .detach_and_log_err(cx);
+            self.selected_model = Some(model_info.clone());
+            let current_index = self.selected_index;
+            self.set_selected_index(current_index, window, cx);
+
+            cx.emit(DismissEvent);
+        }
+    }
+
+    fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
+        cx.emit(DismissEvent);
+    }
+
+    fn render_match(
+        &self,
+        ix: usize,
+        selected: bool,
+        _: &mut Window,
+        cx: &mut Context<Picker<Self>>,
+    ) -> Option<Self::ListItem> {
+        match self.filtered_entries.get(ix)? {
+            AcpModelPickerEntry::Separator(title) => Some(
+                div()
+                    .px_2()
+                    .pb_1()
+                    .when(ix > 1, |this| {
+                        this.mt_1()
+                            .pt_2()
+                            .border_t_1()
+                            .border_color(cx.theme().colors().border_variant)
+                    })
+                    .child(
+                        Label::new(title)
+                            .size(LabelSize::XSmall)
+                            .color(Color::Muted),
+                    )
+                    .into_any_element(),
+            ),
+            AcpModelPickerEntry::Model(model_info) => {
+                let is_selected = Some(model_info) == self.selected_model.as_ref();
+
+                let model_icon_color = if is_selected {
+                    Color::Accent
+                } else {
+                    Color::Muted
+                };
+
+                Some(
+                    ListItem::new(ix)
+                        .inset(true)
+                        .spacing(ListItemSpacing::Sparse)
+                        .toggle_state(selected)
+                        .start_slot::<Icon>(model_info.icon.map(|icon| {
+                            Icon::new(icon)
+                                .color(model_icon_color)
+                                .size(IconSize::Small)
+                        }))
+                        .child(
+                            h_flex()
+                                .w_full()
+                                .pl_0p5()
+                                .gap_1p5()
+                                .w(px(240.))
+                                .child(Label::new(model_info.name.clone()).truncate()),
+                        )
+                        .end_slot(div().pr_3().when(is_selected, |this| {
+                            this.child(
+                                Icon::new(IconName::Check)
+                                    .color(Color::Accent)
+                                    .size(IconSize::Small),
+                            )
+                        }))
+                        .into_any_element(),
+                )
+            }
+        }
+    }
+
+    fn render_footer(
+        &self,
+        _: &mut Window,
+        cx: &mut Context<Picker<Self>>,
+    ) -> Option<gpui::AnyElement> {
+        Some(
+            h_flex()
+                .w_full()
+                .border_t_1()
+                .border_color(cx.theme().colors().border_variant)
+                .p_1()
+                .gap_4()
+                .justify_between()
+                .child(
+                    Button::new("configure", "Configure")
+                        .icon(IconName::Settings)
+                        .icon_size(IconSize::Small)
+                        .icon_color(Color::Muted)
+                        .icon_position(IconPosition::Start)
+                        .on_click(|_, window, cx| {
+                            window.dispatch_action(
+                                zed_actions::agent::OpenSettings.boxed_clone(),
+                                cx,
+                            );
+                        }),
+                )
+                .into_any(),
+        )
+    }
+}
+
+fn info_list_to_picker_entries(
+    model_list: AgentModelList,
+) -> impl Iterator<Item = AcpModelPickerEntry> {
+    match model_list {
+        AgentModelList::Flat(list) => {
+            itertools::Either::Left(list.into_iter().map(AcpModelPickerEntry::Model))
+        }
+        AgentModelList::Grouped(index_map) => {
+            itertools::Either::Right(index_map.into_iter().flat_map(|(group_name, models)| {
+                std::iter::once(AcpModelPickerEntry::Separator(group_name.0))
+                    .chain(models.into_iter().map(AcpModelPickerEntry::Model))
+            }))
+        }
+    }
+}
+
+async fn fuzzy_search(
+    model_list: AgentModelList,
+    query: String,
+    executor: BackgroundExecutor,
+) -> AgentModelList {
+    async fn fuzzy_search_list(
+        model_list: Vec<AgentModelInfo>,
+        query: &str,
+        executor: BackgroundExecutor,
+    ) -> Vec<AgentModelInfo> {
+        let candidates = model_list
+            .iter()
+            .enumerate()
+            .map(|(ix, model)| {
+                StringMatchCandidate::new(ix, &format!("{}/{}", model.id, model.name))
+            })
+            .collect::<Vec<_>>();
+        let mut matches = match_strings(
+            &candidates,
+            query,
+            false,
+            true,
+            100,
+            &Default::default(),
+            executor,
+        )
+        .await;
+
+        matches.sort_unstable_by_key(|mat| {
+            let candidate = &candidates[mat.candidate_id];
+            (Reverse(OrderedFloat(mat.score)), candidate.id)
+        });
+
+        matches
+            .into_iter()
+            .map(|mat| model_list[mat.candidate_id].clone())
+            .collect()
+    }
+
+    match model_list {
+        AgentModelList::Flat(model_list) => {
+            AgentModelList::Flat(fuzzy_search_list(model_list, &query, executor).await)
+        }
+        AgentModelList::Grouped(index_map) => {
+            let groups =
+                futures::future::join_all(index_map.into_iter().map(|(group_name, models)| {
+                    fuzzy_search_list(models, &query, executor.clone())
+                        .map(|results| (group_name, results))
+                }))
+                .await;
+            AgentModelList::Grouped(IndexMap::from_iter(
+                groups
+                    .into_iter()
+                    .filter(|(_, results)| !results.is_empty()),
+            ))
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use gpui::TestAppContext;
+
+    use super::*;
+
+    fn create_model_list(grouped_models: Vec<(&str, Vec<&str>)>) -> AgentModelList {
+        AgentModelList::Grouped(IndexMap::from_iter(grouped_models.into_iter().map(
+            |(group, models)| {
+                (
+                    acp_thread::AgentModelGroupName(group.to_string().into()),
+                    models
+                        .into_iter()
+                        .map(|model| acp_thread::AgentModelInfo {
+                            id: acp_thread::AgentModelId(model.to_string().into()),
+                            name: model.to_string().into(),
+                            icon: None,
+                        })
+                        .collect::<Vec<_>>(),
+                )
+            },
+        )))
+    }
+
+    fn assert_models_eq(result: AgentModelList, expected: Vec<(&str, Vec<&str>)>) {
+        let AgentModelList::Grouped(groups) = result else {
+            panic!("Expected LanguageModelInfoList::Grouped, got {:?}", result);
+        };
+
+        assert_eq!(
+            groups.len(),
+            expected.len(),
+            "Number of groups doesn't match"
+        );
+
+        for (i, (expected_group, expected_models)) in expected.iter().enumerate() {
+            let (actual_group, actual_models) = groups.get_index(i).unwrap();
+            assert_eq!(
+                actual_group.0.as_ref(),
+                *expected_group,
+                "Group at position {} doesn't match expected group",
+                i
+            );
+            assert_eq!(
+                actual_models.len(),
+                expected_models.len(),
+                "Number of models in group {} doesn't match",
+                expected_group
+            );
+
+            for (j, expected_model_name) in expected_models.iter().enumerate() {
+                assert_eq!(
+                    actual_models[j].name, *expected_model_name,
+                    "Model at position {} in group {} doesn't match expected model",
+                    j, expected_group
+                );
+            }
+        }
+    }
+
+    #[gpui::test]
+    async fn test_fuzzy_match(cx: &mut TestAppContext) {
+        let models = create_model_list(vec![
+            (
+                "zed",
+                vec![
+                    "Claude 3.7 Sonnet",
+                    "Claude 3.7 Sonnet Thinking",
+                    "gpt-4.1",
+                    "gpt-4.1-nano",
+                ],
+            ),
+            ("openai", vec!["gpt-3.5-turbo", "gpt-4.1", "gpt-4.1-nano"]),
+            ("ollama", vec!["mistral", "deepseek"]),
+        ]);
+
+        // Results should preserve models order whenever possible.
+        // In the case below, `zed/gpt-4.1` and `openai/gpt-4.1` have identical
+        // similarity scores, but `zed/gpt-4.1` was higher in the models list,
+        // so it should appear first in the results.
+        let results = fuzzy_search(models.clone(), "41".into(), cx.executor()).await;
+        assert_models_eq(
+            results,
+            vec![
+                ("zed", vec!["gpt-4.1", "gpt-4.1-nano"]),
+                ("openai", vec!["gpt-4.1", "gpt-4.1-nano"]),
+            ],
+        );
+
+        // Fuzzy search
+        let results = fuzzy_search(models.clone(), "4n".into(), cx.executor()).await;
+        assert_models_eq(
+            results,
+            vec![
+                ("zed", vec!["gpt-4.1-nano"]),
+                ("openai", vec!["gpt-4.1-nano"]),
+            ],
+        );
+    }
+}

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

@@ -0,0 +1,85 @@
+use std::rc::Rc;
+
+use acp_thread::AgentModelSelector;
+use agent_client_protocol as acp;
+use gpui::{Entity, FocusHandle};
+use picker::popover_menu::PickerPopoverMenu;
+use ui::{
+    ButtonLike, Context, IntoElement, PopoverMenuHandle, SharedString, Tooltip, Window, prelude::*,
+};
+use zed_actions::agent::ToggleModelSelector;
+
+use crate::acp::{AcpModelSelector, model_selector::acp_model_selector};
+
+pub struct AcpModelSelectorPopover {
+    selector: Entity<AcpModelSelector>,
+    menu_handle: PopoverMenuHandle<AcpModelSelector>,
+    focus_handle: FocusHandle,
+}
+
+impl AcpModelSelectorPopover {
+    pub(crate) fn new(
+        session_id: acp::SessionId,
+        selector: Rc<dyn AgentModelSelector>,
+        menu_handle: PopoverMenuHandle<AcpModelSelector>,
+        focus_handle: FocusHandle,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) -> Self {
+        Self {
+            selector: cx.new(move |cx| acp_model_selector(session_id, selector, window, cx)),
+            menu_handle,
+            focus_handle,
+        }
+    }
+
+    pub fn toggle(&self, window: &mut Window, cx: &mut Context<Self>) {
+        self.menu_handle.toggle(window, cx);
+    }
+}
+
+impl Render for AcpModelSelectorPopover {
+    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+        let model = self.selector.read(cx).delegate.active_model();
+        let model_name = model
+            .as_ref()
+            .map(|model| model.name.clone())
+            .unwrap_or_else(|| SharedString::from("Select a Model"));
+
+        let model_icon = model.as_ref().and_then(|model| model.icon);
+
+        let focus_handle = self.focus_handle.clone();
+
+        PickerPopoverMenu::new(
+            self.selector.clone(),
+            ButtonLike::new("active-model")
+                .when_some(model_icon, |this, icon| {
+                    this.child(Icon::new(icon).color(Color::Muted).size(IconSize::XSmall))
+                })
+                .child(
+                    Label::new(model_name)
+                        .color(Color::Muted)
+                        .size(LabelSize::Small)
+                        .ml_0p5(),
+                )
+                .child(
+                    Icon::new(IconName::ChevronDown)
+                        .color(Color::Muted)
+                        .size(IconSize::XSmall),
+                ),
+            move |window, cx| {
+                Tooltip::for_action_in(
+                    "Change Model",
+                    &ToggleModelSelector,
+                    &focus_handle,
+                    window,
+                    cx,
+                )
+            },
+            gpui::Corner::BottomRight,
+            cx,
+        )
+        .with_handle(self.menu_handle.clone())
+        .render(window, cx)
+    }
+}

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

@@ -0,0 +1,902 @@
+use crate::acp::AcpThreadView;
+use crate::{AgentPanel, RemoveSelectedThread};
+use agent2::{HistoryEntry, HistoryStore};
+use chrono::{Datelike as _, Local, NaiveDate, TimeDelta};
+use editor::{Editor, EditorEvent};
+use fuzzy::{StringMatch, StringMatchCandidate};
+use gpui::{
+    App, Empty, Entity, EventEmitter, FocusHandle, Focusable, ScrollStrategy, Stateful, Task,
+    UniformListScrollHandle, WeakEntity, Window, uniform_list,
+};
+use std::{fmt::Display, ops::Range, sync::Arc};
+use time::{OffsetDateTime, UtcOffset};
+use ui::{
+    HighlightedLabel, IconButtonShape, ListItem, ListItemSpacing, Scrollbar, ScrollbarState,
+    Tooltip, prelude::*,
+};
+use util::ResultExt;
+
+pub struct AcpThreadHistory {
+    pub(crate) history_store: Entity<HistoryStore>,
+    scroll_handle: UniformListScrollHandle,
+    selected_index: usize,
+    hovered_index: Option<usize>,
+    search_editor: Entity<Editor>,
+    all_entries: Arc<Vec<HistoryEntry>>,
+    // When the search is empty, we display date separators between history entries
+    // This vector contains an enum of either a separator or an actual entry
+    separated_items: Vec<ListItemType>,
+    // Maps entry indexes to list item indexes
+    separated_item_indexes: Vec<u32>,
+    _separated_items_task: Option<Task<()>>,
+    search_state: SearchState,
+    scrollbar_visibility: bool,
+    scrollbar_state: ScrollbarState,
+    local_timezone: UtcOffset,
+    _subscriptions: Vec<gpui::Subscription>,
+}
+
+enum SearchState {
+    Empty,
+    Searching {
+        query: SharedString,
+        _task: Task<()>,
+    },
+    Searched {
+        query: SharedString,
+        matches: Vec<StringMatch>,
+    },
+}
+
+enum ListItemType {
+    BucketSeparator(TimeBucket),
+    Entry {
+        index: usize,
+        format: EntryTimeFormat,
+    },
+}
+
+pub enum ThreadHistoryEvent {
+    Open(HistoryEntry),
+}
+
+impl EventEmitter<ThreadHistoryEvent> for AcpThreadHistory {}
+
+impl AcpThreadHistory {
+    pub(crate) fn new(
+        history_store: Entity<agent2::HistoryStore>,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) -> Self {
+        let search_editor = cx.new(|cx| {
+            let mut editor = Editor::single_line(window, cx);
+            editor.set_placeholder_text("Search threads...", cx);
+            editor
+        });
+
+        let search_editor_subscription =
+            cx.subscribe(&search_editor, |this, search_editor, event, cx| {
+                if let EditorEvent::BufferEdited = event {
+                    let query = search_editor.read(cx).text(cx);
+                    this.search(query.into(), cx);
+                }
+            });
+
+        let history_store_subscription = cx.observe(&history_store, |this, _, cx| {
+            this.update_all_entries(cx);
+        });
+
+        let scroll_handle = UniformListScrollHandle::default();
+        let scrollbar_state = ScrollbarState::new(scroll_handle.clone());
+
+        let mut this = Self {
+            history_store,
+            scroll_handle,
+            selected_index: 0,
+            hovered_index: None,
+            search_state: SearchState::Empty,
+            all_entries: Default::default(),
+            separated_items: Default::default(),
+            separated_item_indexes: Default::default(),
+            search_editor,
+            scrollbar_visibility: true,
+            scrollbar_state,
+            local_timezone: UtcOffset::from_whole_seconds(
+                chrono::Local::now().offset().local_minus_utc(),
+            )
+            .unwrap(),
+            _subscriptions: vec![search_editor_subscription, history_store_subscription],
+            _separated_items_task: None,
+        };
+        this.update_all_entries(cx);
+        this
+    }
+
+    fn update_all_entries(&mut self, cx: &mut Context<Self>) {
+        let new_entries: Arc<Vec<HistoryEntry>> = self
+            .history_store
+            .update(cx, |store, cx| store.entries(cx))
+            .into();
+
+        self._separated_items_task.take();
+
+        let mut items = Vec::with_capacity(new_entries.len() + 1);
+        let mut indexes = Vec::with_capacity(new_entries.len() + 1);
+
+        let bg_task = cx.background_spawn(async move {
+            let mut bucket = None;
+            let today = Local::now().naive_local().date();
+
+            for (index, entry) in new_entries.iter().enumerate() {
+                let entry_date = entry
+                    .updated_at()
+                    .with_timezone(&Local)
+                    .naive_local()
+                    .date();
+                let entry_bucket = TimeBucket::from_dates(today, entry_date);
+
+                if Some(entry_bucket) != bucket {
+                    bucket = Some(entry_bucket);
+                    items.push(ListItemType::BucketSeparator(entry_bucket));
+                }
+
+                indexes.push(items.len() as u32);
+                items.push(ListItemType::Entry {
+                    index,
+                    format: entry_bucket.into(),
+                });
+            }
+            (new_entries, items, indexes)
+        });
+
+        let task = cx.spawn(async move |this, cx| {
+            let (new_entries, items, indexes) = bg_task.await;
+            this.update(cx, |this, cx| {
+                let previously_selected_entry =
+                    this.all_entries.get(this.selected_index).map(|e| e.id());
+
+                this.all_entries = new_entries;
+                this.separated_items = items;
+                this.separated_item_indexes = indexes;
+
+                match &this.search_state {
+                    SearchState::Empty => {
+                        if this.selected_index >= this.all_entries.len() {
+                            this.set_selected_entry_index(
+                                this.all_entries.len().saturating_sub(1),
+                                cx,
+                            );
+                        } else if let Some(prev_id) = previously_selected_entry
+                            && let Some(new_ix) = this
+                                .all_entries
+                                .iter()
+                                .position(|probe| probe.id() == prev_id)
+                        {
+                            this.set_selected_entry_index(new_ix, cx);
+                        }
+                    }
+                    SearchState::Searching { query, .. } | SearchState::Searched { query, .. } => {
+                        this.search(query.clone(), cx);
+                    }
+                }
+
+                cx.notify();
+            })
+            .log_err();
+        });
+        self._separated_items_task = Some(task);
+    }
+
+    fn search(&mut self, query: SharedString, cx: &mut Context<Self>) {
+        if query.is_empty() {
+            self.search_state = SearchState::Empty;
+            cx.notify();
+            return;
+        }
+
+        let all_entries = self.all_entries.clone();
+
+        let fuzzy_search_task = cx.background_spawn({
+            let query = query.clone();
+            let executor = cx.background_executor().clone();
+            async move {
+                let mut candidates = Vec::with_capacity(all_entries.len());
+
+                for (idx, entry) in all_entries.iter().enumerate() {
+                    candidates.push(StringMatchCandidate::new(idx, entry.title()));
+                }
+
+                const MAX_MATCHES: usize = 100;
+
+                fuzzy::match_strings(
+                    &candidates,
+                    &query,
+                    false,
+                    true,
+                    MAX_MATCHES,
+                    &Default::default(),
+                    executor,
+                )
+                .await
+            }
+        });
+
+        let task = cx.spawn({
+            let query = query.clone();
+            async move |this, cx| {
+                let matches = fuzzy_search_task.await;
+
+                this.update(cx, |this, cx| {
+                    let SearchState::Searching {
+                        query: current_query,
+                        _task,
+                    } = &this.search_state
+                    else {
+                        return;
+                    };
+
+                    if &query == current_query {
+                        this.search_state = SearchState::Searched {
+                            query: query.clone(),
+                            matches,
+                        };
+
+                        this.set_selected_entry_index(0, cx);
+                        cx.notify();
+                    };
+                })
+                .log_err();
+            }
+        });
+
+        self.search_state = SearchState::Searching { query, _task: task };
+        cx.notify();
+    }
+
+    fn matched_count(&self) -> usize {
+        match &self.search_state {
+            SearchState::Empty => self.all_entries.len(),
+            SearchState::Searching { .. } => 0,
+            SearchState::Searched { matches, .. } => matches.len(),
+        }
+    }
+
+    fn list_item_count(&self) -> usize {
+        match &self.search_state {
+            SearchState::Empty => self.separated_items.len(),
+            SearchState::Searching { .. } => 0,
+            SearchState::Searched { matches, .. } => matches.len(),
+        }
+    }
+
+    fn search_produced_no_matches(&self) -> bool {
+        match &self.search_state {
+            SearchState::Empty => false,
+            SearchState::Searching { .. } => false,
+            SearchState::Searched { matches, .. } => matches.is_empty(),
+        }
+    }
+
+    fn get_match(&self, ix: usize) -> Option<&HistoryEntry> {
+        match &self.search_state {
+            SearchState::Empty => self.all_entries.get(ix),
+            SearchState::Searching { .. } => None,
+            SearchState::Searched { matches, .. } => matches
+                .get(ix)
+                .and_then(|m| self.all_entries.get(m.candidate_id)),
+        }
+    }
+
+    pub fn select_previous(
+        &mut self,
+        _: &menu::SelectPrevious,
+        _window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        let count = self.matched_count();
+        if count > 0 {
+            if self.selected_index == 0 {
+                self.set_selected_entry_index(count - 1, cx);
+            } else {
+                self.set_selected_entry_index(self.selected_index - 1, cx);
+            }
+        }
+    }
+
+    pub fn select_next(
+        &mut self,
+        _: &menu::SelectNext,
+        _window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        let count = self.matched_count();
+        if count > 0 {
+            if self.selected_index == count - 1 {
+                self.set_selected_entry_index(0, cx);
+            } else {
+                self.set_selected_entry_index(self.selected_index + 1, cx);
+            }
+        }
+    }
+
+    fn select_first(
+        &mut self,
+        _: &menu::SelectFirst,
+        _window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        let count = self.matched_count();
+        if count > 0 {
+            self.set_selected_entry_index(0, cx);
+        }
+    }
+
+    fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
+        let count = self.matched_count();
+        if count > 0 {
+            self.set_selected_entry_index(count - 1, cx);
+        }
+    }
+
+    fn set_selected_entry_index(&mut self, entry_index: usize, cx: &mut Context<Self>) {
+        self.selected_index = entry_index;
+
+        let scroll_ix = match self.search_state {
+            SearchState::Empty | SearchState::Searching { .. } => self
+                .separated_item_indexes
+                .get(entry_index)
+                .map(|ix| *ix as usize)
+                .unwrap_or(entry_index + 1),
+            SearchState::Searched { .. } => entry_index,
+        };
+
+        self.scroll_handle
+            .scroll_to_item(scroll_ix, ScrollStrategy::Top);
+
+        cx.notify();
+    }
+
+    fn render_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
+        if !(self.scrollbar_visibility || self.scrollbar_state.is_dragging()) {
+            return None;
+        }
+
+        Some(
+            div()
+                .occlude()
+                .id("thread-history-scroll")
+                .h_full()
+                .bg(cx.theme().colors().panel_background.opacity(0.8))
+                .border_l_1()
+                .border_color(cx.theme().colors().border_variant)
+                .absolute()
+                .right_1()
+                .top_0()
+                .bottom_0()
+                .w_4()
+                .pl_1()
+                .cursor_default()
+                .on_mouse_move(cx.listener(|_, _, _window, cx| {
+                    cx.notify();
+                    cx.stop_propagation()
+                }))
+                .on_hover(|_, _window, cx| {
+                    cx.stop_propagation();
+                })
+                .on_any_mouse_down(|_, _window, cx| {
+                    cx.stop_propagation();
+                })
+                .on_scroll_wheel(cx.listener(|_, _, _window, cx| {
+                    cx.notify();
+                }))
+                .children(Scrollbar::vertical(self.scrollbar_state.clone())),
+        )
+    }
+
+    fn confirm(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context<Self>) {
+        self.confirm_entry(self.selected_index, cx);
+    }
+
+    fn confirm_entry(&mut self, ix: usize, cx: &mut Context<Self>) {
+        let Some(entry) = self.get_match(ix) else {
+            return;
+        };
+        cx.emit(ThreadHistoryEvent::Open(entry.clone()));
+    }
+
+    fn remove_selected_thread(
+        &mut self,
+        _: &RemoveSelectedThread,
+        _window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        self.remove_thread(self.selected_index, cx)
+    }
+
+    fn remove_thread(&mut self, ix: usize, cx: &mut Context<Self>) {
+        let Some(entry) = self.get_match(ix) else {
+            return;
+        };
+
+        let task = match entry {
+            HistoryEntry::AcpThread(thread) => self
+                .history_store
+                .update(cx, |this, cx| this.delete_thread(thread.id.clone(), cx)),
+            HistoryEntry::TextThread(context) => self.history_store.update(cx, |this, cx| {
+                this.delete_text_thread(context.path.clone(), cx)
+            }),
+        };
+        task.detach_and_log_err(cx);
+    }
+
+    fn list_items(
+        &mut self,
+        range: Range<usize>,
+        _window: &mut Window,
+        cx: &mut Context<Self>,
+    ) -> Vec<AnyElement> {
+        match &self.search_state {
+            SearchState::Empty => self
+                .separated_items
+                .get(range)
+                .iter()
+                .flat_map(|items| {
+                    items
+                        .iter()
+                        .map(|item| self.render_list_item(item, vec![], cx))
+                })
+                .collect(),
+            SearchState::Searched { matches, .. } => matches[range]
+                .iter()
+                .filter_map(|m| {
+                    let entry = self.all_entries.get(m.candidate_id)?;
+                    Some(self.render_history_entry(
+                        entry,
+                        EntryTimeFormat::DateAndTime,
+                        m.candidate_id,
+                        m.positions.clone(),
+                        cx,
+                    ))
+                })
+                .collect(),
+            SearchState::Searching { .. } => {
+                vec![]
+            }
+        }
+    }
+
+    fn render_list_item(
+        &self,
+        item: &ListItemType,
+        highlight_positions: Vec<usize>,
+        cx: &Context<Self>,
+    ) -> AnyElement {
+        match item {
+            ListItemType::Entry { index, format } => match self.all_entries.get(*index) {
+                Some(entry) => self
+                    .render_history_entry(entry, *format, *index, highlight_positions, cx)
+                    .into_any(),
+                None => Empty.into_any_element(),
+            },
+            ListItemType::BucketSeparator(bucket) => div()
+                .px(DynamicSpacing::Base06.rems(cx))
+                .pt_2()
+                .pb_1()
+                .child(
+                    Label::new(bucket.to_string())
+                        .size(LabelSize::XSmall)
+                        .color(Color::Muted),
+                )
+                .into_any_element(),
+        }
+    }
+
+    fn render_history_entry(
+        &self,
+        entry: &HistoryEntry,
+        format: EntryTimeFormat,
+        list_entry_ix: usize,
+        highlight_positions: Vec<usize>,
+        cx: &Context<Self>,
+    ) -> AnyElement {
+        let selected = list_entry_ix == self.selected_index;
+        let hovered = Some(list_entry_ix) == self.hovered_index;
+        let timestamp = entry.updated_at().timestamp();
+        let thread_timestamp = format.format_timestamp(timestamp, self.local_timezone);
+
+        h_flex()
+            .w_full()
+            .pb_1()
+            .child(
+                ListItem::new(list_entry_ix)
+                    .rounded()
+                    .toggle_state(selected)
+                    .spacing(ListItemSpacing::Sparse)
+                    .start_slot(
+                        h_flex()
+                            .w_full()
+                            .gap_2()
+                            .justify_between()
+                            .child(
+                                HighlightedLabel::new(entry.title(), highlight_positions)
+                                    .size(LabelSize::Small)
+                                    .truncate(),
+                            )
+                            .child(
+                                Label::new(thread_timestamp)
+                                    .color(Color::Muted)
+                                    .size(LabelSize::XSmall),
+                            ),
+                    )
+                    .on_hover(cx.listener(move |this, is_hovered, _window, cx| {
+                        if *is_hovered {
+                            this.hovered_index = Some(list_entry_ix);
+                        } else if this.hovered_index == Some(list_entry_ix) {
+                            this.hovered_index = None;
+                        }
+
+                        cx.notify();
+                    }))
+                    .end_slot::<IconButton>(if hovered || selected {
+                        Some(
+                            IconButton::new("delete", IconName::Trash)
+                                .shape(IconButtonShape::Square)
+                                .icon_size(IconSize::XSmall)
+                                .icon_color(Color::Muted)
+                                .tooltip(move |window, cx| {
+                                    Tooltip::for_action("Delete", &RemoveSelectedThread, window, cx)
+                                })
+                                .on_click(cx.listener(move |this, _, _, cx| {
+                                    this.remove_thread(list_entry_ix, cx)
+                                })),
+                        )
+                    } else {
+                        None
+                    })
+                    .on_click(
+                        cx.listener(move |this, _, _, cx| this.confirm_entry(list_entry_ix, cx)),
+                    ),
+            )
+            .into_any_element()
+    }
+}
+
+impl Focusable for AcpThreadHistory {
+    fn focus_handle(&self, cx: &App) -> FocusHandle {
+        self.search_editor.focus_handle(cx)
+    }
+}
+
+impl Render for AcpThreadHistory {
+    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+        v_flex()
+            .key_context("ThreadHistory")
+            .size_full()
+            .on_action(cx.listener(Self::select_previous))
+            .on_action(cx.listener(Self::select_next))
+            .on_action(cx.listener(Self::select_first))
+            .on_action(cx.listener(Self::select_last))
+            .on_action(cx.listener(Self::confirm))
+            .on_action(cx.listener(Self::remove_selected_thread))
+            .when(!self.all_entries.is_empty(), |parent| {
+                parent.child(
+                    h_flex()
+                        .h(px(41.)) // Match the toolbar perfectly
+                        .w_full()
+                        .py_1()
+                        .px_2()
+                        .gap_2()
+                        .justify_between()
+                        .border_b_1()
+                        .border_color(cx.theme().colors().border)
+                        .child(
+                            Icon::new(IconName::MagnifyingGlass)
+                                .color(Color::Muted)
+                                .size(IconSize::Small),
+                        )
+                        .child(self.search_editor.clone()),
+                )
+            })
+            .child({
+                let view = v_flex()
+                    .id("list-container")
+                    .relative()
+                    .overflow_hidden()
+                    .flex_grow();
+
+                if self.all_entries.is_empty() {
+                    view.justify_center()
+                        .child(
+                            h_flex().w_full().justify_center().child(
+                                Label::new("You don't have any past threads yet.")
+                                    .size(LabelSize::Small),
+                            ),
+                        )
+                } else if self.search_produced_no_matches() {
+                    view.justify_center().child(
+                        h_flex().w_full().justify_center().child(
+                            Label::new("No threads match your search.").size(LabelSize::Small),
+                        ),
+                    )
+                } else {
+                    view.pr_5()
+                        .child(
+                            uniform_list(
+                                "thread-history",
+                                self.list_item_count(),
+                                cx.processor(|this, range: Range<usize>, window, cx| {
+                                    this.list_items(range, window, cx)
+                                }),
+                            )
+                            .p_1()
+                            .track_scroll(self.scroll_handle.clone())
+                            .flex_grow(),
+                        )
+                        .when_some(self.render_scrollbar(cx), |div, scrollbar| {
+                            div.child(scrollbar)
+                        })
+                }
+            })
+    }
+}
+
+#[derive(IntoElement)]
+pub struct AcpHistoryEntryElement {
+    entry: HistoryEntry,
+    thread_view: WeakEntity<AcpThreadView>,
+    selected: bool,
+    hovered: bool,
+    on_hover: Box<dyn Fn(&bool, &mut Window, &mut App) + 'static>,
+}
+
+impl AcpHistoryEntryElement {
+    pub fn new(entry: HistoryEntry, thread_view: WeakEntity<AcpThreadView>) -> Self {
+        Self {
+            entry,
+            thread_view,
+            selected: false,
+            hovered: false,
+            on_hover: Box::new(|_, _, _| {}),
+        }
+    }
+
+    pub fn hovered(mut self, hovered: bool) -> Self {
+        self.hovered = hovered;
+        self
+    }
+
+    pub fn on_hover(mut self, on_hover: impl Fn(&bool, &mut Window, &mut App) + 'static) -> Self {
+        self.on_hover = Box::new(on_hover);
+        self
+    }
+}
+
+impl RenderOnce for AcpHistoryEntryElement {
+    fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
+        let id = self.entry.id();
+        let title = self.entry.title();
+        let timestamp = self.entry.updated_at();
+
+        let formatted_time = {
+            let now = chrono::Utc::now();
+            let duration = now.signed_duration_since(timestamp);
+
+            if duration.num_days() > 0 {
+                format!("{}d", duration.num_days())
+            } else if duration.num_hours() > 0 {
+                format!("{}h ago", duration.num_hours())
+            } else if duration.num_minutes() > 0 {
+                format!("{}m ago", duration.num_minutes())
+            } else {
+                "Just now".to_string()
+            }
+        };
+
+        ListItem::new(id)
+            .rounded()
+            .toggle_state(self.selected)
+            .spacing(ListItemSpacing::Sparse)
+            .start_slot(
+                h_flex()
+                    .w_full()
+                    .gap_2()
+                    .justify_between()
+                    .child(Label::new(title).size(LabelSize::Small).truncate())
+                    .child(
+                        Label::new(formatted_time)
+                            .color(Color::Muted)
+                            .size(LabelSize::XSmall),
+                    ),
+            )
+            .on_hover(self.on_hover)
+            .end_slot::<IconButton>(if self.hovered || self.selected {
+                Some(
+                    IconButton::new("delete", IconName::Trash)
+                        .shape(IconButtonShape::Square)
+                        .icon_size(IconSize::XSmall)
+                        .icon_color(Color::Muted)
+                        .tooltip(move |window, cx| {
+                            Tooltip::for_action("Delete", &RemoveSelectedThread, window, cx)
+                        })
+                        .on_click({
+                            let thread_view = self.thread_view.clone();
+                            let entry = self.entry.clone();
+
+                            move |_event, _window, cx| {
+                                if let Some(thread_view) = thread_view.upgrade() {
+                                    thread_view.update(cx, |thread_view, cx| {
+                                        thread_view.delete_history_entry(entry.clone(), cx);
+                                    });
+                                }
+                            }
+                        }),
+                )
+            } else {
+                None
+            })
+            .on_click({
+                let thread_view = self.thread_view.clone();
+                let entry = self.entry;
+
+                move |_event, window, cx| {
+                    if let Some(workspace) = thread_view
+                        .upgrade()
+                        .and_then(|view| view.read(cx).workspace().upgrade())
+                    {
+                        match &entry {
+                            HistoryEntry::AcpThread(thread_metadata) => {
+                                if let Some(panel) = workspace.read(cx).panel::<AgentPanel>(cx) {
+                                    panel.update(cx, |panel, cx| {
+                                        panel.load_agent_thread(
+                                            thread_metadata.clone(),
+                                            window,
+                                            cx,
+                                        );
+                                    });
+                                }
+                            }
+                            HistoryEntry::TextThread(context) => {
+                                if let Some(panel) = workspace.read(cx).panel::<AgentPanel>(cx) {
+                                    panel.update(cx, |panel, cx| {
+                                        panel
+                                            .open_saved_prompt_editor(
+                                                context.path.clone(),
+                                                window,
+                                                cx,
+                                            )
+                                            .detach_and_log_err(cx);
+                                    });
+                                }
+                            }
+                        }
+                    }
+                }
+            })
+    }
+}
+
+#[derive(Clone, Copy)]
+pub enum EntryTimeFormat {
+    DateAndTime,
+    TimeOnly,
+}
+
+impl EntryTimeFormat {
+    fn format_timestamp(&self, timestamp: i64, timezone: UtcOffset) -> String {
+        let timestamp = OffsetDateTime::from_unix_timestamp(timestamp).unwrap();
+
+        match self {
+            EntryTimeFormat::DateAndTime => time_format::format_localized_timestamp(
+                timestamp,
+                OffsetDateTime::now_utc(),
+                timezone,
+                time_format::TimestampFormat::EnhancedAbsolute,
+            ),
+            EntryTimeFormat::TimeOnly => time_format::format_time(timestamp),
+        }
+    }
+}
+
+impl From<TimeBucket> for EntryTimeFormat {
+    fn from(bucket: TimeBucket) -> Self {
+        match bucket {
+            TimeBucket::Today => EntryTimeFormat::TimeOnly,
+            TimeBucket::Yesterday => EntryTimeFormat::TimeOnly,
+            TimeBucket::ThisWeek => EntryTimeFormat::DateAndTime,
+            TimeBucket::PastWeek => EntryTimeFormat::DateAndTime,
+            TimeBucket::All => EntryTimeFormat::DateAndTime,
+        }
+    }
+}
+
+#[derive(PartialEq, Eq, Clone, Copy, Debug)]
+enum TimeBucket {
+    Today,
+    Yesterday,
+    ThisWeek,
+    PastWeek,
+    All,
+}
+
+impl TimeBucket {
+    fn from_dates(reference: NaiveDate, date: NaiveDate) -> Self {
+        if date == reference {
+            return TimeBucket::Today;
+        }
+
+        if date == reference - TimeDelta::days(1) {
+            return TimeBucket::Yesterday;
+        }
+
+        let week = date.iso_week();
+
+        if reference.iso_week() == week {
+            return TimeBucket::ThisWeek;
+        }
+
+        let last_week = (reference - TimeDelta::days(7)).iso_week();
+
+        if week == last_week {
+            return TimeBucket::PastWeek;
+        }
+
+        TimeBucket::All
+    }
+}
+
+impl Display for TimeBucket {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            TimeBucket::Today => write!(f, "Today"),
+            TimeBucket::Yesterday => write!(f, "Yesterday"),
+            TimeBucket::ThisWeek => write!(f, "This Week"),
+            TimeBucket::PastWeek => write!(f, "Past Week"),
+            TimeBucket::All => write!(f, "All"),
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use chrono::NaiveDate;
+
+    #[test]
+    fn test_time_bucket_from_dates() {
+        let today = NaiveDate::from_ymd_opt(2023, 1, 15).unwrap();
+
+        let date = today;
+        assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::Today);
+
+        let date = NaiveDate::from_ymd_opt(2023, 1, 14).unwrap();
+        assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::Yesterday);
+
+        let date = NaiveDate::from_ymd_opt(2023, 1, 13).unwrap();
+        assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::ThisWeek);
+
+        let date = NaiveDate::from_ymd_opt(2023, 1, 11).unwrap();
+        assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::ThisWeek);
+
+        let date = NaiveDate::from_ymd_opt(2023, 1, 8).unwrap();
+        assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::PastWeek);
+
+        let date = NaiveDate::from_ymd_opt(2023, 1, 5).unwrap();
+        assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::PastWeek);
+
+        // All: not in this week or last week
+        let date = NaiveDate::from_ymd_opt(2023, 1, 1).unwrap();
+        assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::All);
+
+        // Test year boundary cases
+        let new_year = NaiveDate::from_ymd_opt(2023, 1, 1).unwrap();
+
+        let date = NaiveDate::from_ymd_opt(2022, 12, 31).unwrap();
+        assert_eq!(
+            TimeBucket::from_dates(new_year, date),
+            TimeBucket::Yesterday
+        );
+
+        let date = NaiveDate::from_ymd_opt(2022, 12, 28).unwrap();
+        assert_eq!(TimeBucket::from_dates(new_year, date), TimeBucket::ThisWeek);
+    }
+}

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

@@ -1,75 +1,267 @@
+use acp_thread::{
+    AcpThread, AcpThreadEvent, AgentThreadEntry, AssistantMessage, AssistantMessageChunk,
+    AuthRequired, LoadError, MentionUri, RetryStatus, ThreadStatus, ToolCall, ToolCallContent,
+    ToolCallStatus, UserMessageId,
+};
 use acp_thread::{AgentConnection, Plan};
-use agent_servers::AgentServer;
-use agent_settings::{AgentSettings, NotifyWhenAgentWaiting};
+use action_log::ActionLog;
+use agent_client_protocol::{self as acp};
+use agent_servers::{AgentServer, ClaudeCode};
+use agent_settings::{AgentProfileId, AgentSettings, CompletionMode, NotifyWhenAgentWaiting};
+use agent2::{DbThreadMetadata, HistoryEntry, HistoryEntryId, HistoryStore};
+use anyhow::bail;
 use audio::{Audio, Sound};
-use std::cell::RefCell;
-use std::collections::BTreeMap;
-use std::path::Path;
-use std::process::ExitStatus;
-use std::rc::Rc;
-use std::sync::Arc;
-use std::time::Duration;
-
-use agent_client_protocol as acp;
-use assistant_tool::ActionLog;
 use buffer_diff::BufferDiff;
+use client::zed_urls;
 use collections::{HashMap, HashSet};
-use editor::{
-    AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorMode,
-    EditorStyle, MinimapVisibility, MultiBuffer, PathKey,
-};
+use editor::scroll::Autoscroll;
+use editor::{Editor, EditorMode, MultiBuffer, PathKey, SelectionEffects};
 use file_icons::FileIcons;
+use fs::Fs;
 use gpui::{
-    Action, Animation, AnimationExt, App, BorderStyle, EdgesRefinement, Empty, Entity, EntityId,
-    FocusHandle, Focusable, Hsla, Length, ListOffset, ListState, MouseButton, PlatformDisplay,
-    SharedString, Stateful, StyleRefinement, Subscription, Task, TextStyle, TextStyleRefinement,
-    Transformation, UnderlineStyle, WeakEntity, Window, WindowHandle, div, linear_color_stop,
-    linear_gradient, list, percentage, point, prelude::*, pulsating_between,
+    Action, Animation, AnimationExt, AnyView, App, BorderStyle, ClickEvent, ClipboardItem,
+    EdgesRefinement, ElementId, Empty, Entity, FocusHandle, Focusable, Hsla, Length, ListOffset,
+    ListState, MouseButton, PlatformDisplay, SharedString, Stateful, StyleRefinement, Subscription,
+    Task, TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, Window,
+    WindowHandle, div, ease_in_out, linear_color_stop, linear_gradient, list, percentage, point,
+    prelude::*, pulsating_between,
 };
-use language::language_settings::SoftWrap;
-use language::{Buffer, Language};
+use language::Buffer;
+
+use language_model::LanguageModelRegistry;
 use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle};
-use parking_lot::Mutex;
-use project::Project;
+use project::{Project, ProjectEntryId};
+use prompt_store::{PromptId, PromptStore};
+use rope::Point;
 use settings::{Settings as _, SettingsStore};
-use text::{Anchor, BufferSnapshot};
+use std::sync::Arc;
+use std::time::Instant;
+use std::{collections::BTreeMap, rc::Rc, time::Duration};
+use text::Anchor;
 use theme::ThemeSettings;
 use ui::{
-    Disclosure, Divider, DividerColor, KeyBinding, Scrollbar, ScrollbarState, Tooltip, prelude::*,
+    Callout, Disclosure, Divider, DividerColor, ElevationIndex, KeyBinding, PopoverMenuHandle,
+    Scrollbar, ScrollbarState, Tooltip, prelude::*,
 };
-use util::ResultExt;
+use util::{ResultExt, size::format_file_size, time::duration_alt_display};
 use workspace::{CollaboratorId, Workspace};
-use zed_actions::agent::{Chat, NextHistoryMessage, PreviousHistoryMessage};
-
-use ::acp_thread::{
-    AcpThread, AcpThreadEvent, AgentThreadEntry, AssistantMessage, AssistantMessageChunk, Diff,
-    LoadError, MentionPath, ThreadStatus, ToolCall, ToolCallContent, ToolCallStatus,
-};
+use zed_actions::agent::{Chat, ToggleModelSelector};
+use zed_actions::assistant::OpenRulesLibrary;
 
-use crate::acp::completion_provider::{ContextPickerCompletionProvider, MentionSet};
-use crate::acp::message_history::MessageHistory;
+use super::entry_view_state::EntryViewState;
+use crate::acp::AcpModelSelectorPopover;
+use crate::acp::entry_view_state::{EntryViewEvent, ViewEvent};
+use crate::acp::message_editor::{MessageEditor, MessageEditorEvent};
 use crate::agent_diff::AgentDiff;
-use crate::message_editor::{MAX_EDITOR_LINES, MIN_EDITOR_LINES};
-use crate::ui::{AgentNotification, AgentNotificationEvent};
+use crate::profile_selector::{ProfileProvider, ProfileSelector};
+
+use crate::ui::preview::UsageCallout;
+use crate::ui::{AgentNotification, AgentNotificationEvent, BurnModeTooltip};
 use crate::{
-    AgentDiffPane, AgentPanel, ExpandMessageEditor, Follow, KeepAll, OpenAgentDiff, RejectAll,
+    AgentDiffPane, AgentPanel, ContinueThread, ContinueWithBurnMode, ExpandMessageEditor, Follow,
+    KeepAll, OpenAgentDiff, OpenHistory, RejectAll, ToggleBurnMode, ToggleProfileSelector,
 };
 
 const RESPONSE_PADDING_X: Pixels = px(19.);
+pub const MIN_EDITOR_LINES: usize = 4;
+pub const MAX_EDITOR_LINES: usize = 8;
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+enum ThreadFeedback {
+    Positive,
+    Negative,
+}
+
+enum ThreadError {
+    PaymentRequired,
+    ModelRequestLimitReached(cloud_llm_client::Plan),
+    ToolUseLimitReached,
+    AuthenticationRequired(SharedString),
+    Other(SharedString),
+}
+
+impl ThreadError {
+    fn from_err(error: anyhow::Error, agent: &Rc<dyn AgentServer>) -> Self {
+        if error.is::<language_model::PaymentRequiredError>() {
+            Self::PaymentRequired
+        } else if error.is::<language_model::ToolUseLimitReachedError>() {
+            Self::ToolUseLimitReached
+        } else if let Some(error) =
+            error.downcast_ref::<language_model::ModelRequestLimitReachedError>()
+        {
+            Self::ModelRequestLimitReached(error.plan)
+        } else {
+            let string = error.to_string();
+            // TODO: we should have Gemini return better errors here.
+            if agent.clone().downcast::<agent_servers::Gemini>().is_some()
+                && string.contains("Could not load the default credentials")
+                || string.contains("API key not valid")
+                || string.contains("Request had invalid authentication credentials")
+            {
+                Self::AuthenticationRequired(string.into())
+            } else {
+                Self::Other(error.to_string().into())
+            }
+        }
+    }
+}
+
+impl ProfileProvider for Entity<agent2::Thread> {
+    fn profile_id(&self, cx: &App) -> AgentProfileId {
+        self.read(cx).profile().clone()
+    }
+
+    fn set_profile(&self, profile_id: AgentProfileId, cx: &mut App) {
+        self.update(cx, |thread, _cx| {
+            thread.set_profile(profile_id);
+        });
+    }
+
+    fn profiles_supported(&self, cx: &App) -> bool {
+        self.read(cx)
+            .model()
+            .is_some_and(|model| model.supports_tools())
+    }
+}
+
+#[derive(Default)]
+struct ThreadFeedbackState {
+    feedback: Option<ThreadFeedback>,
+    comments_editor: Option<Entity<Editor>>,
+}
+
+impl ThreadFeedbackState {
+    pub fn submit(
+        &mut self,
+        thread: Entity<AcpThread>,
+        feedback: ThreadFeedback,
+        window: &mut Window,
+        cx: &mut App,
+    ) {
+        let Some(telemetry) = thread.read(cx).connection().telemetry() else {
+            return;
+        };
+
+        if self.feedback == Some(feedback) {
+            return;
+        }
+
+        self.feedback = Some(feedback);
+        match feedback {
+            ThreadFeedback::Positive => {
+                self.comments_editor = None;
+            }
+            ThreadFeedback::Negative => {
+                self.comments_editor = Some(Self::build_feedback_comments_editor(window, cx));
+            }
+        }
+        let session_id = thread.read(cx).session_id().clone();
+        let agent_name = telemetry.agent_name();
+        let task = telemetry.thread_data(&session_id, cx);
+        let rating = match feedback {
+            ThreadFeedback::Positive => "positive",
+            ThreadFeedback::Negative => "negative",
+        };
+        cx.background_spawn(async move {
+            let thread = task.await?;
+            telemetry::event!(
+                "Agent Thread Rated",
+                session_id = session_id,
+                rating = rating,
+                agent = agent_name,
+                thread = thread
+            );
+            anyhow::Ok(())
+        })
+        .detach_and_log_err(cx);
+    }
+
+    pub fn submit_comments(&mut self, thread: Entity<AcpThread>, cx: &mut App) {
+        let Some(telemetry) = thread.read(cx).connection().telemetry() else {
+            return;
+        };
+
+        let Some(comments) = self
+            .comments_editor
+            .as_ref()
+            .map(|editor| editor.read(cx).text(cx))
+            .filter(|text| !text.trim().is_empty())
+        else {
+            return;
+        };
+
+        self.comments_editor.take();
+
+        let session_id = thread.read(cx).session_id().clone();
+        let agent_name = telemetry.agent_name();
+        let task = telemetry.thread_data(&session_id, cx);
+        cx.background_spawn(async move {
+            let thread = task.await?;
+            telemetry::event!(
+                "Agent Thread Feedback Comments",
+                session_id = session_id,
+                comments = comments,
+                agent = agent_name,
+                thread = thread
+            );
+            anyhow::Ok(())
+        })
+        .detach_and_log_err(cx);
+    }
+
+    pub fn clear(&mut self) {
+        *self = Self::default()
+    }
+
+    pub fn dismiss_comments(&mut self) {
+        self.comments_editor.take();
+    }
+
+    fn build_feedback_comments_editor(window: &mut Window, cx: &mut App) -> Entity<Editor> {
+        let buffer = cx.new(|cx| {
+            let empty_string = String::new();
+            MultiBuffer::singleton(cx.new(|cx| Buffer::local(empty_string, cx)), cx)
+        });
+
+        let editor = cx.new(|cx| {
+            let mut editor = Editor::new(
+                editor::EditorMode::AutoHeight {
+                    min_lines: 1,
+                    max_lines: Some(4),
+                },
+                buffer,
+                None,
+                window,
+                cx,
+            );
+            editor.set_placeholder_text(
+                "What went wrong? Share your feedback so we can improve.",
+                cx,
+            );
+            editor
+        });
+
+        editor.read(cx).focus_handle(cx).focus(window);
+        editor
+    }
+}
 
 pub struct AcpThreadView {
     agent: Rc<dyn AgentServer>,
     workspace: WeakEntity<Workspace>,
     project: Entity<Project>,
     thread_state: ThreadState,
-    diff_editors: HashMap<EntityId, Entity<Editor>>,
-    message_editor: Entity<Editor>,
-    message_set_from_history: Option<BufferSnapshot>,
-    _message_editor_subscription: Subscription,
-    mention_set: Arc<Mutex<MentionSet>>,
+    history_store: Entity<HistoryStore>,
+    hovered_recent_history_item: Option<usize>,
+    entry_view_state: Entity<EntryViewState>,
+    message_editor: Entity<MessageEditor>,
+    model_selector: Option<Entity<AcpModelSelectorPopover>>,
+    profile_selector: Option<Entity<ProfileSelector>>,
     notifications: Vec<WindowHandle<AgentNotification>>,
     notification_subscriptions: HashMap<WindowHandle<AgentNotification>, Vec<Subscription>>,
-    last_error: Option<Entity<Markdown>>,
+    thread_retry_status: Option<RetryStatus>,
+    thread_error: Option<ThreadError>,
+    thread_feedback: ThreadFeedbackState,
     list_state: ListState,
     scrollbar_state: ScrollbarState,
     auth_task: Option<Task<()>>,
@@ -78,9 +270,9 @@ pub struct AcpThreadView {
     edits_expanded: bool,
     plan_expanded: bool,
     editor_expanded: bool,
-    message_history: Rc<RefCell<MessageHistory<Vec<acp::ContentBlock>>>>,
+    editing_message: Option<usize>,
     _cancel_task: Option<Task<()>>,
-    _subscriptions: [Subscription; 1],
+    _subscriptions: [Subscription; 3],
 }
 
 enum ThreadState {
@@ -94,122 +286,98 @@ enum ThreadState {
     LoadError(LoadError),
     Unauthenticated {
         connection: Rc<dyn AgentConnection>,
-    },
-    ServerExited {
-        status: ExitStatus,
+        description: Option<Entity<Markdown>>,
+        configuration_view: Option<AnyView>,
+        pending_auth_method: Option<acp::AuthMethodId>,
+        _subscription: Option<Subscription>,
     },
 }
 
 impl AcpThreadView {
     pub fn new(
         agent: Rc<dyn AgentServer>,
+        resume_thread: Option<DbThreadMetadata>,
+        summarize_thread: Option<DbThreadMetadata>,
         workspace: WeakEntity<Workspace>,
         project: Entity<Project>,
-        message_history: Rc<RefCell<MessageHistory<Vec<acp::ContentBlock>>>>,
-        min_lines: usize,
-        max_lines: Option<usize>,
+        history_store: Entity<HistoryStore>,
+        prompt_store: Option<Entity<PromptStore>>,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) -> Self {
-        let language = Language::new(
-            language::LanguageConfig {
-                completion_query_characters: HashSet::from_iter(['.', '-', '_', '@']),
-                ..Default::default()
-            },
-            None,
-        );
-
-        let mention_set = Arc::new(Mutex::new(MentionSet::default()));
-
+        let prevent_slash_commands = agent.clone().downcast::<ClaudeCode>().is_some();
         let message_editor = cx.new(|cx| {
-            let buffer = cx.new(|cx| Buffer::local("", cx).with_language(Arc::new(language), cx));
-            let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
-
-            let mut editor = Editor::new(
+            let mut editor = MessageEditor::new(
+                workspace.clone(),
+                project.clone(),
+                history_store.clone(),
+                prompt_store.clone(),
+                "Message the agent — @ to include context",
+                prevent_slash_commands,
                 editor::EditorMode::AutoHeight {
-                    min_lines,
-                    max_lines: max_lines,
+                    min_lines: MIN_EDITOR_LINES,
+                    max_lines: Some(MAX_EDITOR_LINES),
                 },
-                buffer,
-                None,
                 window,
                 cx,
             );
-            editor.set_placeholder_text("Message the agent - @ to include files", cx);
-            editor.set_show_indent_guides(false, cx);
-            editor.set_soft_wrap();
-            editor.set_use_modal_editing(true);
-            editor.set_completion_provider(Some(Rc::new(ContextPickerCompletionProvider::new(
-                mention_set.clone(),
-                workspace.clone(),
-                cx.weak_entity(),
-            ))));
-            editor.set_context_menu_options(ContextMenuOptions {
-                min_entries_visible: 12,
-                max_entries_visible: 12,
-                placement: Some(ContextMenuPlacement::Above),
-            });
+            if let Some(entry) = summarize_thread {
+                editor.insert_thread_summary(entry, window, cx);
+            }
             editor
         });
 
-        let message_editor_subscription =
-            cx.subscribe(&message_editor, |this, editor, event, cx| {
-                if let editor::EditorEvent::BufferEdited = &event {
-                    let buffer = editor
-                        .read(cx)
-                        .buffer()
-                        .read(cx)
-                        .as_singleton()
-                        .unwrap()
-                        .read(cx)
-                        .snapshot();
-                    if let Some(message) = this.message_set_from_history.clone()
-                        && message.version() != buffer.version()
-                    {
-                        this.message_set_from_history = None;
-                    }
-
-                    if this.message_set_from_history.is_none() {
-                        this.message_history.borrow_mut().reset_position();
-                    }
-                }
-            });
-
-        let mention_set = mention_set.clone();
-
         let list_state = ListState::new(0, gpui::ListAlignment::Bottom, px(2048.0));
 
-        let subscription = cx.observe_global_in::<SettingsStore>(window, Self::settings_changed);
+        let entry_view_state = cx.new(|_| {
+            EntryViewState::new(
+                workspace.clone(),
+                project.clone(),
+                history_store.clone(),
+                prompt_store.clone(),
+                prevent_slash_commands,
+            )
+        });
+
+        let subscriptions = [
+            cx.observe_global_in::<SettingsStore>(window, Self::settings_changed),
+            cx.subscribe_in(&message_editor, window, Self::handle_message_editor_event),
+            cx.subscribe_in(&entry_view_state, window, Self::handle_entry_view_event),
+        ];
 
         Self {
             agent: agent.clone(),
             workspace: workspace.clone(),
             project: project.clone(),
-            thread_state: Self::initial_state(agent, workspace, project, window, cx),
+            entry_view_state,
+            thread_state: Self::initial_state(agent, resume_thread, workspace, project, window, cx),
             message_editor,
-            message_set_from_history: None,
-            _message_editor_subscription: message_editor_subscription,
-            mention_set,
+            model_selector: None,
+            profile_selector: None,
             notifications: Vec::new(),
             notification_subscriptions: HashMap::default(),
-            diff_editors: Default::default(),
             list_state: list_state.clone(),
             scrollbar_state: ScrollbarState::new(list_state).parent_entity(&cx.entity()),
-            last_error: None,
+            thread_retry_status: None,
+            thread_error: None,
+            thread_feedback: Default::default(),
             auth_task: None,
             expanded_tool_calls: HashSet::default(),
             expanded_thinking_blocks: HashSet::default(),
+            editing_message: None,
             edits_expanded: false,
             plan_expanded: false,
             editor_expanded: false,
-            message_history,
-            _subscriptions: [subscription],
+            history_store,
+            hovered_recent_history_item: None,
+            _subscriptions: subscriptions,
             _cancel_task: None,
         }
     }
 
     fn initial_state(
         agent: Rc<dyn AgentServer>,
+        resume_thread: Option<DbThreadMetadata>,
         workspace: WeakEntity<Workspace>,
         project: Entity<Project>,
         window: &mut Window,
@@ -236,39 +404,42 @@ impl AcpThreadView {
                 }
             };
 
-            // this.update_in(cx, |_this, _window, cx| {
-            //     let status = connection.exit_status(cx);
-            //     cx.spawn(async move |this, cx| {
-            //         let status = status.await.ok();
-            //         this.update(cx, |this, cx| {
-            //             this.thread_state = ThreadState::ServerExited { status };
-            //             cx.notify();
-            //         })
-            //         .ok();
-            //     })
-            //     .detach();
-            // })
-            // .ok();
-
-            let result = match connection
+            let result = if let Some(native_agent) = connection
                 .clone()
-                .new_thread(project.clone(), &root_dir, cx)
-                .await
+                .downcast::<agent2::NativeAgentConnection>()
+                && let Some(resume) = resume_thread.clone()
             {
-                Err(e) => {
-                    let mut cx = cx.clone();
-                    if e.is::<acp_thread::AuthRequired>() {
-                        this.update(&mut cx, |this, cx| {
-                            this.thread_state = ThreadState::Unauthenticated { connection };
-                            cx.notify();
+                cx.update(|_, cx| {
+                    native_agent
+                        .0
+                        .update(cx, |agent, cx| agent.open_thread(resume.id, cx))
+                })
+                .log_err()
+            } else {
+                cx.update(|_, cx| {
+                    connection
+                        .clone()
+                        .new_thread(project.clone(), &root_dir, cx)
+                })
+                .log_err()
+            };
+
+            let Some(result) = result else {
+                return;
+            };
+
+            let result = match result.await {
+                Err(e) => match e.downcast::<acp_thread::AuthRequired>() {
+                    Ok(err) => {
+                        cx.update(|window, cx| {
+                            Self::handle_auth_required(this, err, agent, connection, window, cx)
                         })
-                        .ok();
+                        .log_err();
                         return;
-                    } else {
-                        Err(e)
                     }
-                }
-                Ok(session_id) => Ok(session_id),
+                    Err(err) => Err(err),
+                },
+                Ok(thread) => Ok(thread),
             };
 
             this.update_in(cx, |this, window, cx| {
@@ -281,16 +452,64 @@ impl AcpThreadView {
                         let action_log_subscription =
                             cx.observe(&action_log, |_, _, cx| cx.notify());
 
-                        this.list_state
-                            .splice(0..0, thread.read(cx).entries().len());
+                        let count = thread.read(cx).entries().len();
+                        this.list_state.splice(0..0, count);
+                        this.entry_view_state.update(cx, |view_state, cx| {
+                            for ix in 0..count {
+                                view_state.sync_entry(ix, &thread, window, cx);
+                            }
+                        });
+
+                        if let Some(resume) = resume_thread {
+                            this.history_store.update(cx, |history, cx| {
+                                history.push_recently_opened_entry(
+                                    HistoryEntryId::AcpThread(resume.id),
+                                    cx,
+                                );
+                            });
+                        }
 
                         AgentDiff::set_active_thread(&workspace, thread.clone(), window, cx);
 
+                        this.model_selector =
+                            thread
+                                .read(cx)
+                                .connection()
+                                .model_selector()
+                                .map(|selector| {
+                                    cx.new(|cx| {
+                                        AcpModelSelectorPopover::new(
+                                            thread.read(cx).session_id().clone(),
+                                            selector,
+                                            PopoverMenuHandle::default(),
+                                            this.focus_handle(cx),
+                                            window,
+                                            cx,
+                                        )
+                                    })
+                                });
+
                         this.thread_state = ThreadState::Ready {
                             thread,
                             _subscription: [thread_subscription, action_log_subscription],
                         };
 
+                        this.profile_selector = this.as_native_thread(cx).map(|thread| {
+                            cx.new(|cx| {
+                                ProfileSelector::new(
+                                    <dyn Fs>::global(cx),
+                                    Arc::new(thread.clone()),
+                                    this.focus_handle(cx),
+                                    cx,
+                                )
+                            })
+                        });
+
+                        this.message_editor.update(cx, |message_editor, _cx| {
+                            message_editor
+                                .set_prompt_capabilities(connection.prompt_capabilities());
+                        });
+
                         cx.notify();
                     }
                     Err(err) => {
@@ -304,6 +523,70 @@ impl AcpThreadView {
         ThreadState::Loading { _task: load_task }
     }
 
+    fn handle_auth_required(
+        this: WeakEntity<Self>,
+        err: AuthRequired,
+        agent: Rc<dyn AgentServer>,
+        connection: Rc<dyn AgentConnection>,
+        window: &mut Window,
+        cx: &mut App,
+    ) {
+        let agent_name = agent.name();
+        let (configuration_view, subscription) = if let Some(provider_id) = err.provider_id {
+            let registry = LanguageModelRegistry::global(cx);
+
+            let sub = window.subscribe(&registry, cx, {
+                let provider_id = provider_id.clone();
+                let this = this.clone();
+                move |_, ev, window, cx| {
+                    if let language_model::Event::ProviderStateChanged(updated_provider_id) = &ev
+                        && &provider_id == updated_provider_id
+                    {
+                        this.update(cx, |this, cx| {
+                            this.thread_state = Self::initial_state(
+                                agent.clone(),
+                                None,
+                                this.workspace.clone(),
+                                this.project.clone(),
+                                window,
+                                cx,
+                            );
+                            cx.notify();
+                        })
+                        .ok();
+                    }
+                }
+            });
+
+            let view = registry.read(cx).provider(&provider_id).map(|provider| {
+                provider.configuration_view(
+                    language_model::ConfigurationViewTargetAgent::Other(agent_name),
+                    window,
+                    cx,
+                )
+            });
+
+            (view, Some(sub))
+        } else {
+            (None, None)
+        };
+
+        this.update(cx, |this, cx| {
+            this.thread_state = ThreadState::Unauthenticated {
+                pending_auth_method: None,
+                connection,
+                configuration_view,
+                description: err
+                    .description
+                    .clone()
+                    .map(|desc| cx.new(|cx| Markdown::new(desc.into(), None, None, cx))),
+                _subscription: subscription,
+            };
+            cx.notify();
+        })
+        .ok();
+    }
+
     fn handle_load_error(&mut self, err: anyhow::Error, cx: &mut Context<Self>) {
         if let Some(load_err) = err.downcast_ref::<LoadError>() {
             self.thread_state = ThreadState::LoadError(load_err.clone());
@@ -313,13 +596,16 @@ impl AcpThreadView {
         cx.notify();
     }
 
+    pub fn workspace(&self) -> &WeakEntity<Workspace> {
+        &self.workspace
+    }
+
     pub fn thread(&self) -> Option<&Entity<AcpThread>> {
         match &self.thread_state {
             ThreadState::Ready { thread, .. } => Some(thread),
             ThreadState::Unauthenticated { .. }
             | ThreadState::Loading { .. }
-            | ThreadState::LoadError(..)
-            | ThreadState::ServerExited { .. } => None,
+            | ThreadState::LoadError { .. } => None,
         }
     }
 
@@ -328,13 +614,13 @@ impl AcpThreadView {
             ThreadState::Ready { thread, .. } => thread.read(cx).title(),
             ThreadState::Loading { .. } => "Loading…".into(),
             ThreadState::LoadError(_) => "Failed to load".into(),
-            ThreadState::Unauthenticated { .. } => "Not authenticated".into(),
-            ThreadState::ServerExited { .. } => "Server exited unexpectedly".into(),
+            ThreadState::Unauthenticated { .. } => "Authentication Required".into(),
         }
     }
 
-    pub fn cancel(&mut self, cx: &mut Context<Self>) {
-        self.last_error.take();
+    pub fn cancel_generation(&mut self, cx: &mut Context<Self>) {
+        self.thread_error.take();
+        self.thread_retry_status.take();
 
         if let Some(thread) = self.thread() {
             self._cancel_task = Some(thread.update(cx, |thread, cx| thread.cancel(cx)));
@@ -353,167 +639,246 @@ impl AcpThreadView {
 
     fn set_editor_is_expanded(&mut self, is_expanded: bool, cx: &mut Context<Self>) {
         self.editor_expanded = is_expanded;
-        self.message_editor.update(cx, |editor, _| {
-            if self.editor_expanded {
-                editor.set_mode(EditorMode::Full {
-                    scale_ui_elements_with_buffer_font_size: false,
-                    show_active_line_background: false,
-                    sized_by_content: false,
-                })
+        self.message_editor.update(cx, |editor, cx| {
+            if is_expanded {
+                editor.set_mode(
+                    EditorMode::Full {
+                        scale_ui_elements_with_buffer_font_size: false,
+                        show_active_line_background: false,
+                        sized_by_content: false,
+                    },
+                    cx,
+                )
             } else {
-                editor.set_mode(EditorMode::AutoHeight {
-                    min_lines: MIN_EDITOR_LINES,
-                    max_lines: Some(MAX_EDITOR_LINES),
-                })
+                editor.set_mode(
+                    EditorMode::AutoHeight {
+                        min_lines: MIN_EDITOR_LINES,
+                        max_lines: Some(MAX_EDITOR_LINES),
+                    },
+                    cx,
+                )
             }
         });
         cx.notify();
     }
 
-    fn chat(&mut self, _: &Chat, window: &mut Window, cx: &mut Context<Self>) {
-        self.last_error.take();
-
-        let mut ix = 0;
-        let mut chunks: Vec<acp::ContentBlock> = Vec::new();
-        let project = self.project.clone();
-        self.message_editor.update(cx, |editor, cx| {
-            let text = editor.text(cx);
-            editor.display_map.update(cx, |map, cx| {
-                let snapshot = map.snapshot(cx);
-                for (crease_id, crease) in snapshot.crease_snapshot.creases() {
-                    // Skip creases that have been edited out of the message buffer.
-                    if !crease.range().start.is_valid(&snapshot.buffer_snapshot) {
-                        continue;
-                    }
+    pub fn handle_message_editor_event(
+        &mut self,
+        _: &Entity<MessageEditor>,
+        event: &MessageEditorEvent,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        match event {
+            MessageEditorEvent::Send => self.send(window, cx),
+            MessageEditorEvent::Cancel => self.cancel_generation(cx),
+            MessageEditorEvent::Focus => {
+                self.cancel_editing(&Default::default(), window, cx);
+            }
+        }
+    }
 
-                    if let Some(project_path) =
-                        self.mention_set.lock().path_for_crease_id(crease_id)
-                    {
-                        let crease_range = crease.range().to_offset(&snapshot.buffer_snapshot);
-                        if crease_range.start > ix {
-                            chunks.push(text[ix..crease_range.start].into());
-                        }
-                        if let Some(abs_path) = project.read(cx).absolute_path(&project_path, cx) {
-                            let path_str = abs_path.display().to_string();
-                            chunks.push(acp::ContentBlock::ResourceLink(acp::ResourceLink {
-                                uri: path_str.clone(),
-                                name: path_str,
-                                annotations: None,
-                                description: None,
-                                mime_type: None,
-                                size: None,
-                                title: None,
-                            }));
-                        }
-                        ix = crease_range.end;
-                    }
+    pub fn handle_entry_view_event(
+        &mut self,
+        _: &Entity<EntryViewState>,
+        event: &EntryViewEvent,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        match &event.view_event {
+            ViewEvent::NewDiff(tool_call_id) => {
+                if AgentSettings::get_global(cx).expand_edit_card {
+                    self.expanded_tool_calls.insert(tool_call_id.clone());
                 }
-
-                if ix < text.len() {
-                    let last_chunk = text[ix..].trim();
-                    if !last_chunk.is_empty() {
-                        chunks.push(last_chunk.into());
-                    }
+            }
+            ViewEvent::NewTerminal(tool_call_id) => {
+                if AgentSettings::get_global(cx).expand_terminal_card {
+                    self.expanded_tool_calls.insert(tool_call_id.clone());
                 }
-            })
-        });
-
-        if chunks.is_empty() {
-            return;
+            }
+            ViewEvent::MessageEditorEvent(_editor, MessageEditorEvent::Focus) => {
+                if let Some(thread) = self.thread()
+                    && let Some(AgentThreadEntry::UserMessage(user_message)) =
+                        thread.read(cx).entries().get(event.entry_index)
+                    && user_message.id.is_some()
+                {
+                    self.editing_message = Some(event.entry_index);
+                    cx.notify();
+                }
+            }
+            ViewEvent::MessageEditorEvent(editor, MessageEditorEvent::Send) => {
+                self.regenerate(event.entry_index, editor, window, cx);
+            }
+            ViewEvent::MessageEditorEvent(_editor, MessageEditorEvent::Cancel) => {
+                self.cancel_editing(&Default::default(), window, cx);
+            }
         }
+    }
 
+    fn resume_chat(&mut self, cx: &mut Context<Self>) {
+        self.thread_error.take();
         let Some(thread) = self.thread() else {
             return;
         };
-        let task = thread.update(cx, |thread, cx| thread.send(chunks.clone(), cx));
 
+        let task = thread.update(cx, |thread, cx| thread.resume(cx));
         cx.spawn(async move |this, cx| {
             let result = task.await;
 
             this.update(cx, |this, cx| {
                 if let Err(err) = result {
-                    this.last_error =
-                        Some(cx.new(|cx| Markdown::new(err.to_string().into(), None, None, cx)))
+                    this.handle_thread_error(err, cx);
                 }
             })
         })
         .detach();
+    }
 
-        let mention_set = self.mention_set.clone();
-
-        self.set_editor_is_expanded(false, cx);
-
-        self.message_editor.update(cx, |editor, cx| {
-            editor.clear(window, cx);
-            editor.remove_creases(mention_set.lock().drain(), cx)
+    fn send(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+        let Some(thread) = self.thread() else { return };
+        self.history_store.update(cx, |history, cx| {
+            history.push_recently_opened_entry(
+                HistoryEntryId::AcpThread(thread.read(cx).session_id().clone()),
+                cx,
+            );
         });
 
-        self.scroll_to_bottom(cx);
+        if thread.read(cx).status() != ThreadStatus::Idle {
+            self.stop_current_and_send_new_message(window, cx);
+            return;
+        }
 
-        self.message_history.borrow_mut().push(chunks);
+        let contents = self
+            .message_editor
+            .update(cx, |message_editor, cx| message_editor.contents(window, cx));
+        self.send_impl(contents, window, cx)
     }
 
-    fn previous_history_message(
-        &mut self,
-        _: &PreviousHistoryMessage,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        if self.message_set_from_history.is_none() && !self.message_editor.read(cx).is_empty(cx) {
-            self.message_editor.update(cx, |editor, cx| {
-                editor.move_up(&Default::default(), window, cx);
-            });
+    fn stop_current_and_send_new_message(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+        let Some(thread) = self.thread().cloned() else {
             return;
-        }
+        };
 
-        self.message_set_from_history = Self::set_draft_message(
-            self.message_editor.clone(),
-            self.mention_set.clone(),
-            self.project.clone(),
-            self.message_history
-                .borrow_mut()
-                .prev()
-                .map(|blocks| blocks.as_slice()),
-            window,
-            cx,
-        );
+        let cancelled = thread.update(cx, |thread, cx| thread.cancel(cx));
+
+        let contents = self
+            .message_editor
+            .update(cx, |message_editor, cx| message_editor.contents(window, cx));
+
+        cx.spawn_in(window, async move |this, cx| {
+            cancelled.await;
+
+            this.update_in(cx, |this, window, cx| {
+                this.send_impl(contents, window, cx);
+            })
+            .ok();
+        })
+        .detach();
     }
 
-    fn next_history_message(
+    fn send_impl(
         &mut self,
-        _: &NextHistoryMessage,
+        contents: Task<anyhow::Result<(Vec<acp::ContentBlock>, Vec<Entity<Buffer>>)>>,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        if self.message_set_from_history.is_none() {
-            self.message_editor.update(cx, |editor, cx| {
-                editor.move_down(&Default::default(), window, cx);
-            });
+        self.thread_error.take();
+        self.editing_message.take();
+        self.thread_feedback.clear();
+
+        let Some(thread) = self.thread().cloned() else {
             return;
-        }
+        };
+        let task = cx.spawn_in(window, async move |this, cx| {
+            let (contents, tracked_buffers) = contents.await?;
 
-        let mut message_history = self.message_history.borrow_mut();
-        let next_history = message_history.next();
+            if contents.is_empty() {
+                return Ok(());
+            }
 
-        let set_draft_message = Self::set_draft_message(
-            self.message_editor.clone(),
-            self.mention_set.clone(),
-            self.project.clone(),
-            Some(
-                next_history
-                    .map(|blocks| blocks.as_slice())
-                    .unwrap_or_else(|| &[]),
-            ),
-            window,
-            cx,
-        );
-        // If we reset the text to an empty string because we ran out of history,
-        // we don't want to mark it as coming from the history
-        self.message_set_from_history = if next_history.is_some() {
-            set_draft_message
-        } else {
-            None
+            this.update_in(cx, |this, window, cx| {
+                this.set_editor_is_expanded(false, cx);
+                this.scroll_to_bottom(cx);
+                this.message_editor.update(cx, |message_editor, cx| {
+                    message_editor.clear(window, cx);
+                });
+            })?;
+            let send = thread.update(cx, |thread, cx| {
+                thread.action_log().update(cx, |action_log, cx| {
+                    for buffer in tracked_buffers {
+                        action_log.buffer_read(buffer, cx)
+                    }
+                });
+                thread.send(contents, cx)
+            })?;
+            send.await
+        });
+
+        cx.spawn(async move |this, cx| {
+            if let Err(err) = task.await {
+                this.update(cx, |this, cx| {
+                    this.handle_thread_error(err, cx);
+                })
+                .ok();
+            }
+        })
+        .detach();
+    }
+
+    fn cancel_editing(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
+        let Some(thread) = self.thread().cloned() else {
+            return;
+        };
+
+        if let Some(index) = self.editing_message.take()
+            && let Some(editor) = self
+                .entry_view_state
+                .read(cx)
+                .entry(index)
+                .and_then(|e| e.message_editor())
+                .cloned()
+        {
+            editor.update(cx, |editor, cx| {
+                if let Some(user_message) = thread
+                    .read(cx)
+                    .entries()
+                    .get(index)
+                    .and_then(|e| e.user_message())
+                {
+                    editor.set_message(user_message.chunks.clone(), window, cx);
+                }
+            })
+        };
+        self.focus_handle(cx).focus(window);
+        cx.notify();
+    }
+
+    fn regenerate(
+        &mut self,
+        entry_ix: usize,
+        message_editor: &Entity<MessageEditor>,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        let Some(thread) = self.thread().cloned() else {
+            return;
+        };
+
+        let Some(rewind) = thread.update(cx, |thread, cx| {
+            let user_message_id = thread.entries().get(entry_ix)?.user_message()?.id.clone()?;
+            Some(thread.rewind(user_message_id, cx))
+        }) else {
+            return;
         };
+
+        let contents =
+            message_editor.update(cx, |message_editor, cx| message_editor.contents(window, cx));
+
+        let task = cx.foreground_executor().spawn(async move {
+            rewind.await?;
+            contents.await
+        });
+        self.send_impl(task, window, cx);
     }
 
     fn open_agent_diff(&mut self, _: &OpenAgentDiff, window: &mut Window, cx: &mut Context<Self>) {

crates/agent_ui/src/active_thread.rs 🔗

@@ -434,7 +434,7 @@ fn render_markdown_code_block(
                             .child(content)
                             .child(
                                 Icon::new(IconName::ArrowUpRight)
-                                    .size(IconSize::XSmall)
+                                    .size(IconSize::Small)
                                     .color(Color::Ignored),
                             ),
                     )
@@ -491,7 +491,7 @@ fn render_markdown_code_block(
             .on_click({
                 let active_thread = active_thread.clone();
                 let parsed_markdown = parsed_markdown.clone();
-                let code_block_range = metadata.content_range.clone();
+                let code_block_range = metadata.content_range;
                 move |_event, _window, cx| {
                     active_thread.update(cx, |this, cx| {
                         this.copied_code_block_ids.insert((message_id, ix));
@@ -532,7 +532,6 @@ fn render_markdown_code_block(
                 "Expand Code"
             }))
             .on_click({
-                let active_thread = active_thread.clone();
                 move |_event, _window, cx| {
                     active_thread.update(cx, |this, cx| {
                         this.toggle_codeblock_expanded(message_id, ix);
@@ -780,13 +779,11 @@ impl ActiveThread {
 
         let list_state = ListState::new(0, ListAlignment::Bottom, px(2048.));
 
-        let workspace_subscription = if let Some(workspace) = workspace.upgrade() {
-            Some(cx.observe_release(&workspace, |this, _, cx| {
+        let workspace_subscription = workspace.upgrade().map(|workspace| {
+            cx.observe_release(&workspace, |this, _, cx| {
                 this.dismiss_notifications(cx);
-            }))
-        } else {
-            None
-        };
+            })
+        });
 
         let mut this = Self {
             language_registry,
@@ -916,7 +913,7 @@ impl ActiveThread {
     ) {
         let rendered = self
             .rendered_tool_uses
-            .entry(tool_use_id.clone())
+            .entry(tool_use_id)
             .or_insert_with(|| RenderedToolUse {
                 label: cx.new(|cx| {
                     Markdown::new("".into(), Some(self.language_registry.clone()), None, cx)
@@ -1044,12 +1041,12 @@ impl ActiveThread {
                 );
             }
             ThreadEvent::StreamedAssistantText(message_id, text) => {
-                if let Some(rendered_message) = self.rendered_messages_by_id.get_mut(&message_id) {
+                if let Some(rendered_message) = self.rendered_messages_by_id.get_mut(message_id) {
                     rendered_message.append_text(text, cx);
                 }
             }
             ThreadEvent::StreamedAssistantThinking(message_id, text) => {
-                if let Some(rendered_message) = self.rendered_messages_by_id.get_mut(&message_id) {
+                if let Some(rendered_message) = self.rendered_messages_by_id.get_mut(message_id) {
                     rendered_message.append_thinking(text, cx);
                 }
             }
@@ -1072,8 +1069,8 @@ impl ActiveThread {
             }
             ThreadEvent::MessageEdited(message_id) => {
                 self.clear_last_error();
-                if let Some(index) = self.messages.iter().position(|id| id == message_id) {
-                    if let Some(rendered_message) = self.thread.update(cx, |thread, cx| {
+                if let Some(index) = self.messages.iter().position(|id| id == message_id)
+                    && let Some(rendered_message) = self.thread.update(cx, |thread, cx| {
                         thread.message(*message_id).map(|message| {
                             let mut rendered_message = RenderedMessage {
                                 language_registry: self.language_registry.clone(),
@@ -1084,14 +1081,14 @@ impl ActiveThread {
                             }
                             rendered_message
                         })
-                    }) {
-                        self.list_state.splice(index..index + 1, 1);
-                        self.rendered_messages_by_id
-                            .insert(*message_id, rendered_message);
-                        self.scroll_to_bottom(cx);
-                        self.save_thread(cx);
-                        cx.notify();
-                    }
+                    })
+                {
+                    self.list_state.splice(index..index + 1, 1);
+                    self.rendered_messages_by_id
+                        .insert(*message_id, rendered_message);
+                    self.scroll_to_bottom(cx);
+                    self.save_thread(cx);
+                    cx.notify();
                 }
             }
             ThreadEvent::MessageDeleted(message_id) => {
@@ -1218,7 +1215,7 @@ impl ActiveThread {
         match AgentSettings::get_global(cx).notify_when_agent_waiting {
             NotifyWhenAgentWaiting::PrimaryScreen => {
                 if let Some(primary) = cx.primary_display() {
-                    self.pop_up(icon, caption.into(), title.clone(), window, primary, cx);
+                    self.pop_up(icon, caption.into(), title, window, primary, cx);
                 }
             }
             NotifyWhenAgentWaiting::AllScreens => {
@@ -1272,62 +1269,61 @@ impl ActiveThread {
                 })
             })
             .log_err()
+            && let Some(pop_up) = screen_window.entity(cx).log_err()
         {
-            if let Some(pop_up) = screen_window.entity(cx).log_err() {
-                self.notification_subscriptions
-                    .entry(screen_window)
-                    .or_insert_with(Vec::new)
-                    .push(cx.subscribe_in(&pop_up, window, {
-                        |this, _, event, window, cx| match event {
-                            AgentNotificationEvent::Accepted => {
-                                let handle = window.window_handle();
-                                cx.activate(true);
-
-                                let workspace_handle = this.workspace.clone();
-
-                                // If there are multiple Zed windows, activate the correct one.
-                                cx.defer(move |cx| {
-                                    handle
-                                        .update(cx, |_view, window, _cx| {
-                                            window.activate_window();
-
-                                            if let Some(workspace) = workspace_handle.upgrade() {
-                                                workspace.update(_cx, |workspace, cx| {
-                                                    workspace.focus_panel::<AgentPanel>(window, cx);
-                                                });
-                                            }
-                                        })
-                                        .log_err();
-                                });
+            self.notification_subscriptions
+                .entry(screen_window)
+                .or_insert_with(Vec::new)
+                .push(cx.subscribe_in(&pop_up, window, {
+                    |this, _, event, window, cx| match event {
+                        AgentNotificationEvent::Accepted => {
+                            let handle = window.window_handle();
+                            cx.activate(true);
+
+                            let workspace_handle = this.workspace.clone();
+
+                            // If there are multiple Zed windows, activate the correct one.
+                            cx.defer(move |cx| {
+                                handle
+                                    .update(cx, |_view, window, _cx| {
+                                        window.activate_window();
+
+                                        if let Some(workspace) = workspace_handle.upgrade() {
+                                            workspace.update(_cx, |workspace, cx| {
+                                                workspace.focus_panel::<AgentPanel>(window, cx);
+                                            });
+                                        }
+                                    })
+                                    .log_err();
+                            });
 
-                                this.dismiss_notifications(cx);
-                            }
-                            AgentNotificationEvent::Dismissed => {
-                                this.dismiss_notifications(cx);
-                            }
+                            this.dismiss_notifications(cx);
                         }
-                    }));
-
-                self.notifications.push(screen_window);
-
-                // If the user manually refocuses the original window, dismiss the popup.
-                self.notification_subscriptions
-                    .entry(screen_window)
-                    .or_insert_with(Vec::new)
-                    .push({
-                        let pop_up_weak = pop_up.downgrade();
-
-                        cx.observe_window_activation(window, move |_, window, cx| {
-                            if window.is_window_active() {
-                                if let Some(pop_up) = pop_up_weak.upgrade() {
-                                    pop_up.update(cx, |_, cx| {
-                                        cx.emit(AgentNotificationEvent::Dismissed);
-                                    });
-                                }
-                            }
-                        })
-                    });
-            }
+                        AgentNotificationEvent::Dismissed => {
+                            this.dismiss_notifications(cx);
+                        }
+                    }
+                }));
+
+            self.notifications.push(screen_window);
+
+            // If the user manually refocuses the original window, dismiss the popup.
+            self.notification_subscriptions
+                .entry(screen_window)
+                .or_insert_with(Vec::new)
+                .push({
+                    let pop_up_weak = pop_up.downgrade();
+
+                    cx.observe_window_activation(window, move |_, window, cx| {
+                        if window.is_window_active()
+                            && let Some(pop_up) = pop_up_weak.upgrade()
+                        {
+                            pop_up.update(cx, |_, cx| {
+                                cx.emit(AgentNotificationEvent::Dismissed);
+                            });
+                        }
+                    })
+                });
         }
     }
 
@@ -1374,12 +1370,12 @@ impl ActiveThread {
             editor.focus_handle(cx).focus(window);
             editor.move_to_end(&editor::actions::MoveToEnd, window, cx);
         });
-        let buffer_edited_subscription = cx.subscribe(&editor, |this, _, event, cx| match event {
-            EditorEvent::BufferEdited => {
-                this.update_editing_message_token_count(true, cx);
-            }
-            _ => {}
-        });
+        let buffer_edited_subscription =
+            cx.subscribe(&editor, |this, _, event: &EditorEvent, cx| {
+                if event == &EditorEvent::BufferEdited {
+                    this.update_editing_message_token_count(true, cx);
+                }
+            });
 
         let context_picker_menu_handle = PopoverMenuHandle::default();
         let context_strip = cx.new(|cx| {
@@ -1766,7 +1762,7 @@ impl ActiveThread {
                 .thread
                 .read(cx)
                 .message(message_id)
-                .map(|msg| msg.to_string())
+                .map(|msg| msg.to_message_content())
                 .unwrap_or_default();
 
             telemetry::event!(
@@ -1896,8 +1892,9 @@ impl ActiveThread {
             (colors.editor_background, colors.panel_background)
         };
 
-        let open_as_markdown = IconButton::new(("open-as-markdown", ix), IconName::DocumentText)
-            .icon_size(IconSize::XSmall)
+        let open_as_markdown = IconButton::new(("open-as-markdown", ix), IconName::FileMarkdown)
+            .shape(ui::IconButtonShape::Square)
+            .icon_size(IconSize::Small)
             .icon_color(Color::Ignored)
             .tooltip(Tooltip::text("Open Thread as Markdown"))
             .on_click({
@@ -1911,8 +1908,9 @@ impl ActiveThread {
                 }
             });
 
-        let scroll_to_top = IconButton::new(("scroll_to_top", ix), IconName::ArrowUpAlt)
-            .icon_size(IconSize::XSmall)
+        let scroll_to_top = IconButton::new(("scroll_to_top", ix), IconName::ArrowUp)
+            .shape(ui::IconButtonShape::Square)
+            .icon_size(IconSize::Small)
             .icon_color(Color::Ignored)
             .tooltip(Tooltip::text("Scroll To Top"))
             .on_click(cx.listener(move |this, _, _, cx| {
@@ -1926,6 +1924,7 @@ impl ActiveThread {
             .py_2()
             .px(RESPONSE_PADDING_X)
             .mr_1()
+            .gap_1()
             .opacity(0.4)
             .hover(|style| style.opacity(1.))
             .gap_1p5()
@@ -1949,7 +1948,8 @@ impl ActiveThread {
                     h_flex()
                         .child(
                             IconButton::new(("feedback-thumbs-up", ix), IconName::ThumbsUp)
-                                .icon_size(IconSize::XSmall)
+                                .shape(ui::IconButtonShape::Square)
+                                .icon_size(IconSize::Small)
                                 .icon_color(match feedback {
                                     ThreadFeedback::Positive => Color::Accent,
                                     ThreadFeedback::Negative => Color::Ignored,
@@ -1966,7 +1966,8 @@ impl ActiveThread {
                         )
                         .child(
                             IconButton::new(("feedback-thumbs-down", ix), IconName::ThumbsDown)
-                                .icon_size(IconSize::XSmall)
+                                .shape(ui::IconButtonShape::Square)
+                                .icon_size(IconSize::Small)
                                 .icon_color(match feedback {
                                     ThreadFeedback::Positive => Color::Ignored,
                                     ThreadFeedback::Negative => Color::Accent,
@@ -1999,7 +2000,8 @@ impl ActiveThread {
                     h_flex()
                         .child(
                             IconButton::new(("feedback-thumbs-up", ix), IconName::ThumbsUp)
-                                .icon_size(IconSize::XSmall)
+                                .shape(ui::IconButtonShape::Square)
+                                .icon_size(IconSize::Small)
                                 .icon_color(Color::Ignored)
                                 .tooltip(Tooltip::text("Helpful Response"))
                                 .on_click(cx.listener(move |this, _, window, cx| {
@@ -2013,7 +2015,8 @@ impl ActiveThread {
                         )
                         .child(
                             IconButton::new(("feedback-thumbs-down", ix), IconName::ThumbsDown)
-                                .icon_size(IconSize::XSmall)
+                                .shape(ui::IconButtonShape::Square)
+                                .icon_size(IconSize::Small)
                                 .icon_color(Color::Ignored)
                                 .tooltip(Tooltip::text("Not Helpful"))
                                 .on_click(cx.listener(move |this, _, window, cx| {
@@ -2106,7 +2109,7 @@ impl ActiveThread {
                                         .gap_1()
                                         .children(message_content)
                                         .when_some(editing_message_state, |this, state| {
-                                            let focus_handle = state.editor.focus_handle(cx).clone();
+                                            let focus_handle = state.editor.focus_handle(cx);
 
                                             this.child(
                                                 h_flex()
@@ -2167,7 +2170,6 @@ impl ActiveThread {
                                                                 .icon_color(Color::Muted)
                                                                 .icon_size(IconSize::Small)
                                                                 .tooltip({
-                                                                    let focus_handle = focus_handle.clone();
                                                                     move |window, cx| {
                                                                         Tooltip::for_action_in(
                                                                             "Regenerate",
@@ -2240,9 +2242,7 @@ impl ActiveThread {
         let after_editing_message = self
             .editing_message
             .as_ref()
-            .map_or(false, |(editing_message_id, _)| {
-                message_id > *editing_message_id
-            });
+            .is_some_and(|(editing_message_id, _)| message_id > *editing_message_id);
 
         let backdrop = div()
             .id(("backdrop", ix))
@@ -2262,13 +2262,12 @@ impl ActiveThread {
                     let mut error = None;
                     if let Some(last_restore_checkpoint) =
                         self.thread.read(cx).last_restore_checkpoint()
+                        && last_restore_checkpoint.message_id() == message_id
                     {
-                        if last_restore_checkpoint.message_id() == message_id {
-                            match last_restore_checkpoint {
-                                LastRestoreCheckpoint::Pending { .. } => is_pending = true,
-                                LastRestoreCheckpoint::Error { error: err, .. } => {
-                                    error = Some(err.clone());
-                                }
+                        match last_restore_checkpoint {
+                            LastRestoreCheckpoint::Pending { .. } => is_pending = true,
+                            LastRestoreCheckpoint::Error { error: err, .. } => {
+                                error = Some(err.clone());
                             }
                         }
                     }
@@ -2309,7 +2308,7 @@ impl ActiveThread {
                             .into_any_element()
                     } else if let Some(error) = error {
                         restore_checkpoint_button
-                            .tooltip(Tooltip::text(error.to_string()))
+                            .tooltip(Tooltip::text(error))
                             .into_any_element()
                     } else {
                         restore_checkpoint_button.into_any_element()
@@ -2350,7 +2349,6 @@ impl ActiveThread {
                                     this.submit_feedback_message(message_id, cx);
                                     cx.notify();
                                 }))
-                                .on_action(cx.listener(Self::confirm_editing_message))
                                 .mb_2()
                                 .mx_4()
                                 .p_2()
@@ -2466,7 +2464,7 @@ impl ActiveThread {
                                 message_id,
                                 index,
                                 content.clone(),
-                                &scroll_handle,
+                                scroll_handle,
                                 Some(index) == pending_thinking_segment_index,
                                 window,
                                 cx,
@@ -2590,7 +2588,7 @@ impl ActiveThread {
             .id(("message-container", ix))
             .py_1()
             .px_2p5()
-            .child(Banner::new().severity(ui::Severity::Warning).child(message))
+            .child(Banner::new().severity(Severity::Warning).child(message))
     }
 
     fn render_message_thinking_segment(
@@ -2750,7 +2748,7 @@ impl ActiveThread {
                                 h_flex()
                                     .gap_1p5()
                                     .child(
-                                        Icon::new(IconName::LightBulb)
+                                        Icon::new(IconName::ToolThink)
                                             .size(IconSize::XSmall)
                                             .color(Color::Muted),
                                     )
@@ -3362,7 +3360,7 @@ impl ActiveThread {
                                 .mr_0p5(),
                         )
                         .child(
-                            IconButton::new("open-prompt-library", IconName::ArrowUpRightAlt)
+                            IconButton::new("open-prompt-library", IconName::ArrowUpRight)
                                 .shape(ui::IconButtonShape::Square)
                                 .icon_size(IconSize::XSmall)
                                 .icon_color(Color::Ignored)
@@ -3397,7 +3395,7 @@ impl ActiveThread {
                                 .mr_0p5(),
                         )
                         .child(
-                            IconButton::new("open-rule", IconName::ArrowUpRightAlt)
+                            IconButton::new("open-rule", IconName::ArrowUpRight)
                                 .shape(ui::IconButtonShape::Square)
                                 .icon_size(IconSize::XSmall)
                                 .icon_color(Color::Ignored)
@@ -4013,7 +4011,7 @@ mod tests {
 
         cx.run_until_parked();
 
-        // Verify that the previous completion was cancelled
+        // Verify that the previous completion was canceled
         assert_eq!(cancellation_events.lock().unwrap().len(), 1);
 
         // Verify that a new request was started after cancellation

crates/agent_ui/src/agent_configuration.rs 🔗

@@ -96,7 +96,7 @@ impl AgentConfiguration {
         let mut expanded_provider_configurations = HashMap::default();
         if LanguageModelRegistry::read_global(cx)
             .provider(&ZED_CLOUD_PROVIDER_ID)
-            .map_or(false, |cloud_provider| cloud_provider.must_accept_terms(cx))
+            .is_some_and(|cloud_provider| cloud_provider.must_accept_terms(cx))
         {
             expanded_provider_configurations.insert(ZED_CLOUD_PROVIDER_ID, true);
         }
@@ -137,7 +137,11 @@ impl AgentConfiguration {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        let configuration_view = provider.configuration_view(window, cx);
+        let configuration_view = provider.configuration_view(
+            language_model::ConfigurationViewTargetAgent::ZedAgent,
+            window,
+            cx,
+        );
         self.configuration_views_by_provider
             .insert(provider.id(), configuration_view);
     }
@@ -161,8 +165,8 @@ impl AgentConfiguration {
         provider: &Arc<dyn LanguageModelProvider>,
         cx: &mut Context<Self>,
     ) -> impl IntoElement + use<> {
-        let provider_id = provider.id().0.clone();
-        let provider_name = provider.name().0.clone();
+        let provider_id = provider.id().0;
+        let provider_name = provider.name().0;
         let provider_id_string = SharedString::from(format!("provider-disclosure-{provider_id}"));
 
         let configuration_view = self
@@ -188,7 +192,7 @@ impl AgentConfiguration {
         let is_signed_in = self
             .workspace
             .read_with(cx, |workspace, _| {
-                workspace.client().status().borrow().is_connected()
+                !workspace.client().status().borrow().is_signed_out()
             })
             .unwrap_or(false);
 
@@ -265,7 +269,7 @@ impl AgentConfiguration {
                                     .closed_icon(IconName::ChevronDown),
                             )
                             .on_click(cx.listener({
-                                let provider_id = provider.id().clone();
+                                let provider_id = provider.id();
                                 move |this, _event, _window, _cx| {
                                     let is_expanded = this
                                         .expanded_provider_configurations
@@ -300,6 +304,7 @@ impl AgentConfiguration {
             )
             .child(
                 div()
+                    .w_full()
                     .px_2()
                     .when(is_expanded, |parent| match configuration_view {
                         Some(configuration_view) => parent.child(configuration_view),
@@ -465,7 +470,7 @@ impl AgentConfiguration {
             "modifier-send",
             "Use modifier to submit a message",
             Some(
-                "Make a modifier (cmd-enter on macOS, ctrl-enter on Linux) required to send messages.".into(),
+                "Make a modifier (cmd-enter on macOS, ctrl-enter on Linux or Windows) required to send messages.".into(),
             ),
             use_modifier_to_send,
             move |state, _window, cx| {
@@ -573,7 +578,7 @@ impl AgentConfiguration {
                             .style(ButtonStyle::Filled)
                             .layer(ElevationIndex::ModalSurface)
                             .full_width()
-                            .icon(IconName::Hammer)
+                            .icon(IconName::ToolHammer)
                             .icon_size(IconSize::Small)
                             .icon_position(IconPosition::Start)
                             .on_click(|_event, window, cx| {
@@ -660,7 +665,7 @@ impl AgentConfiguration {
                     .size(IconSize::XSmall)
                     .color(Color::Accent)
                     .with_animation(
-                        SharedString::from(format!("{}-starting", context_server_id.0.clone(),)),
+                        SharedString::from(format!("{}-starting", context_server_id.0,)),
                         Animation::new(Duration::from_secs(3)).repeat(),
                         |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
                     )
@@ -860,7 +865,6 @@ impl AgentConfiguration {
                                     .on_click({
                                         let context_server_manager =
                                             self.context_server_store.clone();
-                                        let context_server_id = context_server_id.clone();
                                         let fs = self.fs.clone();
 
                                         move |state, _window, cx| {
@@ -953,7 +957,7 @@ impl AgentConfiguration {
                 }
 
                 parent.child(v_flex().py_1p5().px_1().gap_1().children(
-                    tools.into_iter().enumerate().map(|(ix, tool)| {
+                    tools.iter().enumerate().map(|(ix, tool)| {
                         h_flex()
                             .id(("tool-item", ix))
                             .px_1()
@@ -1035,7 +1039,6 @@ fn extension_only_provides_context_server(manifest: &ExtensionManifest) -> bool
         && manifest.grammars.is_empty()
         && manifest.language_servers.is_empty()
         && manifest.slash_commands.is_empty()
-        && manifest.indexed_docs_providers.is_empty()
         && manifest.snippets.is_none()
         && manifest.debug_locators.is_empty()
 }
@@ -1071,7 +1074,6 @@ fn show_unable_to_uninstall_extension_with_context_server(
         cx,
         move |this, _cx| {
             let workspace_handle = workspace_handle.clone();
-            let context_server_id = context_server_id.clone();
 
             this.icon(ToastIcon::new(IconName::Warning).color(Color::Warning))
                 .dismiss_button(true)

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

@@ -7,10 +7,12 @@ use gpui::{DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Render, T
 use language_model::LanguageModelRegistry;
 use language_models::{
     AllLanguageModelSettings, OpenAiCompatibleSettingsContent,
-    provider::open_ai_compatible::AvailableModel,
+    provider::open_ai_compatible::{AvailableModel, ModelCapabilities},
 };
 use settings::update_settings_file;
-use ui::{Banner, KeyBinding, Modal, ModalFooter, ModalHeader, Section, prelude::*};
+use ui::{
+    Banner, Checkbox, KeyBinding, Modal, ModalFooter, ModalHeader, Section, ToggleState, prelude::*,
+};
 use ui_input::SingleLineInput;
 use workspace::{ModalView, Workspace};
 
@@ -69,11 +71,19 @@ impl AddLlmProviderInput {
     }
 }
 
+struct ModelCapabilityToggles {
+    pub supports_tools: ToggleState,
+    pub supports_images: ToggleState,
+    pub supports_parallel_tool_calls: ToggleState,
+    pub supports_prompt_cache_key: ToggleState,
+}
+
 struct ModelInput {
     name: Entity<SingleLineInput>,
     max_completion_tokens: Entity<SingleLineInput>,
     max_output_tokens: Entity<SingleLineInput>,
     max_tokens: Entity<SingleLineInput>,
+    capabilities: ModelCapabilityToggles,
 }
 
 impl ModelInput {
@@ -100,11 +110,23 @@ impl ModelInput {
             cx,
         );
         let max_tokens = single_line_input("Max Tokens", "Max Tokens", Some("200000"), window, cx);
+        let ModelCapabilities {
+            tools,
+            images,
+            parallel_tool_calls,
+            prompt_cache_key,
+        } = ModelCapabilities::default();
         Self {
             name: model_name,
             max_completion_tokens,
             max_output_tokens,
             max_tokens,
+            capabilities: ModelCapabilityToggles {
+                supports_tools: tools.into(),
+                supports_images: images.into(),
+                supports_parallel_tool_calls: parallel_tool_calls.into(),
+                supports_prompt_cache_key: prompt_cache_key.into(),
+            },
         }
     }
 
@@ -136,6 +158,12 @@ impl ModelInput {
                 .text(cx)
                 .parse::<u64>()
                 .map_err(|_| SharedString::from("Max Tokens must be a number"))?,
+            capabilities: ModelCapabilities {
+                tools: self.capabilities.supports_tools.selected(),
+                images: self.capabilities.supports_images.selected(),
+                parallel_tool_calls: self.capabilities.supports_parallel_tool_calls.selected(),
+                prompt_cache_key: self.capabilities.supports_prompt_cache_key.selected(),
+            },
         })
     }
 }
@@ -322,6 +350,55 @@ impl AddLlmProviderModal {
                     .child(model.max_output_tokens.clone()),
             )
             .child(model.max_tokens.clone())
+            .child(
+                v_flex()
+                    .gap_1()
+                    .child(
+                        Checkbox::new(("supports-tools", ix), model.capabilities.supports_tools)
+                            .label("Supports tools")
+                            .on_click(cx.listener(move |this, checked, _window, cx| {
+                                this.input.models[ix].capabilities.supports_tools = *checked;
+                                cx.notify();
+                            })),
+                    )
+                    .child(
+                        Checkbox::new(("supports-images", ix), model.capabilities.supports_images)
+                            .label("Supports images")
+                            .on_click(cx.listener(move |this, checked, _window, cx| {
+                                this.input.models[ix].capabilities.supports_images = *checked;
+                                cx.notify();
+                            })),
+                    )
+                    .child(
+                        Checkbox::new(
+                            ("supports-parallel-tool-calls", ix),
+                            model.capabilities.supports_parallel_tool_calls,
+                        )
+                        .label("Supports parallel_tool_calls")
+                        .on_click(cx.listener(
+                            move |this, checked, _window, cx| {
+                                this.input.models[ix]
+                                    .capabilities
+                                    .supports_parallel_tool_calls = *checked;
+                                cx.notify();
+                            },
+                        )),
+                    )
+                    .child(
+                        Checkbox::new(
+                            ("supports-prompt-cache-key", ix),
+                            model.capabilities.supports_prompt_cache_key,
+                        )
+                        .label("Supports prompt_cache_key")
+                        .on_click(cx.listener(
+                            move |this, checked, _window, cx| {
+                                this.input.models[ix].capabilities.supports_prompt_cache_key =
+                                    *checked;
+                                cx.notify();
+                            },
+                        )),
+                    ),
+            )
             .when(has_more_than_one_model, |this| {
                 this.child(
                     Button::new(("remove-model", ix), "Remove Model")
@@ -377,7 +454,7 @@ impl Render for AddLlmProviderModal {
                         this.section(
                             Section::new().child(
                                 Banner::new()
-                                    .severity(ui::Severity::Warning)
+                                    .severity(Severity::Warning)
                                     .child(div().text_xs().child(error)),
                             ),
                         )
@@ -562,6 +639,93 @@ mod tests {
         );
     }
 
+    #[gpui::test]
+    async fn test_model_input_default_capabilities(cx: &mut TestAppContext) {
+        let cx = setup_test(cx).await;
+
+        cx.update(|window, cx| {
+            let model_input = ModelInput::new(window, cx);
+            model_input.name.update(cx, |input, cx| {
+                input.editor().update(cx, |editor, cx| {
+                    editor.set_text("somemodel", window, cx);
+                });
+            });
+            assert_eq!(
+                model_input.capabilities.supports_tools,
+                ToggleState::Selected
+            );
+            assert_eq!(
+                model_input.capabilities.supports_images,
+                ToggleState::Unselected
+            );
+            assert_eq!(
+                model_input.capabilities.supports_parallel_tool_calls,
+                ToggleState::Unselected
+            );
+            assert_eq!(
+                model_input.capabilities.supports_prompt_cache_key,
+                ToggleState::Unselected
+            );
+
+            let parsed_model = model_input.parse(cx).unwrap();
+            assert!(parsed_model.capabilities.tools);
+            assert!(!parsed_model.capabilities.images);
+            assert!(!parsed_model.capabilities.parallel_tool_calls);
+            assert!(!parsed_model.capabilities.prompt_cache_key);
+        });
+    }
+
+    #[gpui::test]
+    async fn test_model_input_deselected_capabilities(cx: &mut TestAppContext) {
+        let cx = setup_test(cx).await;
+
+        cx.update(|window, cx| {
+            let mut model_input = ModelInput::new(window, cx);
+            model_input.name.update(cx, |input, cx| {
+                input.editor().update(cx, |editor, cx| {
+                    editor.set_text("somemodel", window, cx);
+                });
+            });
+
+            model_input.capabilities.supports_tools = ToggleState::Unselected;
+            model_input.capabilities.supports_images = ToggleState::Unselected;
+            model_input.capabilities.supports_parallel_tool_calls = ToggleState::Unselected;
+            model_input.capabilities.supports_prompt_cache_key = ToggleState::Unselected;
+
+            let parsed_model = model_input.parse(cx).unwrap();
+            assert!(!parsed_model.capabilities.tools);
+            assert!(!parsed_model.capabilities.images);
+            assert!(!parsed_model.capabilities.parallel_tool_calls);
+            assert!(!parsed_model.capabilities.prompt_cache_key);
+        });
+    }
+
+    #[gpui::test]
+    async fn test_model_input_with_name_and_capabilities(cx: &mut TestAppContext) {
+        let cx = setup_test(cx).await;
+
+        cx.update(|window, cx| {
+            let mut model_input = ModelInput::new(window, cx);
+            model_input.name.update(cx, |input, cx| {
+                input.editor().update(cx, |editor, cx| {
+                    editor.set_text("somemodel", window, cx);
+                });
+            });
+
+            model_input.capabilities.supports_tools = ToggleState::Selected;
+            model_input.capabilities.supports_images = ToggleState::Unselected;
+            model_input.capabilities.supports_parallel_tool_calls = ToggleState::Selected;
+            model_input.capabilities.supports_prompt_cache_key = ToggleState::Unselected;
+
+            let parsed_model = model_input.parse(cx).unwrap();
+            assert_eq!(parsed_model.name, "somemodel");
+            assert!(parsed_model.capabilities.tools);
+            assert!(!parsed_model.capabilities.images);
+            assert!(parsed_model.capabilities.parallel_tool_calls);
+            assert!(!parsed_model.capabilities.prompt_cache_key);
+        });
+    }
+
     async fn setup_test(cx: &mut TestAppContext) -> &mut VisualTestContext {
         cx.update(|cx| {
             let store = SettingsStore::test(cx);

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

@@ -163,10 +163,10 @@ impl ConfigurationSource {
                     .read(cx)
                     .text(cx);
                 let settings = serde_json_lenient::from_str::<serde_json::Value>(&text)?;
-                if let Some(settings_validator) = settings_validator {
-                    if let Err(error) = settings_validator.validate(&settings) {
-                        return Err(anyhow::anyhow!(error.to_string()));
-                    }
+                if let Some(settings_validator) = settings_validator
+                    && let Err(error) = settings_validator.validate(&settings)
+                {
+                    return Err(anyhow::anyhow!(error.to_string()));
                 }
                 Ok((
                     id.clone(),
@@ -261,7 +261,6 @@ impl ConfigureContextServerModal {
         _cx: &mut Context<Workspace>,
     ) {
         workspace.register_action({
-            let language_registry = language_registry.clone();
             move |_workspace, _: &AddContextServer, window, cx| {
                 let workspace_handle = cx.weak_entity();
                 let language_registry = language_registry.clone();
@@ -438,7 +437,7 @@ impl ConfigureContextServerModal {
                         format!("{} configured successfully.", id.0),
                         cx,
                         |this, _cx| {
-                            this.icon(ToastIcon::new(IconName::Hammer).color(Color::Muted))
+                            this.icon(ToastIcon::new(IconName::ToolHammer).color(Color::Muted))
                                 .action("Dismiss", |_, _| {})
                         },
                     );
@@ -487,7 +486,7 @@ impl ConfigureContextServerModal {
     }
 
     fn render_modal_description(&self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
-        const MODAL_DESCRIPTION: &'static str = "Visit the MCP server configuration docs to find all necessary arguments and environment variables.";
+        const MODAL_DESCRIPTION: &str = "Visit the MCP server configuration docs to find all necessary arguments and environment variables.";
 
         if let ConfigurationSource::Extension {
             installation_instructions: Some(installation_instructions),
@@ -567,7 +566,7 @@ impl ConfigureContextServerModal {
                         Button::new("open-repository", "Open Repository")
                             .icon(IconName::ArrowUpRight)
                             .icon_color(Color::Muted)
-                            .icon_size(IconSize::XSmall)
+                            .icon_size(IconSize::Small)
                             .tooltip({
                                 let repository_url = repository_url.clone();
                                 move |window, cx| {
@@ -716,24 +715,24 @@ fn wait_for_context_server(
         project::context_server_store::Event::ServerStatusChanged { server_id, status } => {
             match status {
                 ContextServerStatus::Running => {
-                    if server_id == &context_server_id {
-                        if let Some(tx) = tx.lock().unwrap().take() {
-                            let _ = tx.send(Ok(()));
-                        }
+                    if server_id == &context_server_id
+                        && let Some(tx) = tx.lock().unwrap().take()
+                    {
+                        let _ = tx.send(Ok(()));
                     }
                 }
                 ContextServerStatus::Stopped => {
-                    if server_id == &context_server_id {
-                        if let Some(tx) = tx.lock().unwrap().take() {
-                            let _ = tx.send(Err("Context server stopped running".into()));
-                        }
+                    if server_id == &context_server_id
+                        && let Some(tx) = tx.lock().unwrap().take()
+                    {
+                        let _ = tx.send(Err("Context server stopped running".into()));
                     }
                 }
                 ContextServerStatus::Error(error) => {
-                    if server_id == &context_server_id {
-                        if let Some(tx) = tx.lock().unwrap().take() {
-                            let _ = tx.send(Err(error.clone()));
-                        }
+                    if server_id == &context_server_id
+                        && let Some(tx) = tx.lock().unwrap().take()
+                    {
+                        let _ = tx.send(Err(error.clone()));
                     }
                 }
                 _ => {}

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

@@ -464,7 +464,7 @@ impl ManageProfilesModal {
                 },
             ))
             .child(ListSeparator)
-            .child(h_flex().p_2().child(mode.name_editor.clone()))
+            .child(h_flex().p_2().child(mode.name_editor))
     }
 
     fn render_view_profile(
@@ -594,7 +594,7 @@ impl ManageProfilesModal {
                                         .inset(true)
                                         .spacing(ListItemSpacing::Sparse)
                                         .start_slot(
-                                            Icon::new(IconName::Hammer)
+                                            Icon::new(IconName::ToolHammer)
                                                 .size(IconSize::Small)
                                                 .color(Color::Muted),
                                         )
@@ -763,7 +763,7 @@ impl Render for ManageProfilesModal {
                         .pb_1()
                         .child(ProfileModalHeader::new(
                             format!("{profile_name} — Configure MCP Tools"),
-                            Some(IconName::Hammer),
+                            Some(IconName::ToolHammer),
                         ))
                         .child(ListSeparator)
                         .child(tool_picker.clone())

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

@@ -191,10 +191,10 @@ impl PickerDelegate for ToolPickerDelegate {
                         BTreeMap::default();
 
                     for item in all_items.iter() {
-                        if let PickerItem::Tool { server_id, name } = item.clone() {
-                            if name.contains(&query) {
-                                tools_by_provider.entry(server_id).or_default().push(name);
-                            }
+                        if let PickerItem::Tool { server_id, name } = item.clone()
+                            && name.contains(&query)
+                        {
+                            tools_by_provider.entry(server_id).or_default().push(name);
                         }
                     }
 

crates/agent_ui/src/agent_diff.rs 🔗

@@ -1,9 +1,9 @@
 use crate::{Keep, KeepAll, OpenAgentDiff, Reject, RejectAll};
 use acp_thread::{AcpThread, AcpThreadEvent};
+use action_log::ActionLog;
 use agent::{Thread, ThreadEvent, ThreadSummary};
 use agent_settings::AgentSettings;
 use anyhow::Result;
-use assistant_tool::ActionLog;
 use buffer_diff::DiffHunkStatus;
 use collections::{HashMap, HashSet};
 use editor::{
@@ -185,7 +185,7 @@ impl AgentDiffPane {
         let focus_handle = cx.focus_handle();
         let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
 
-        let project = thread.project(cx).clone();
+        let project = thread.project(cx);
         let editor = cx.new(|cx| {
             let mut editor =
                 Editor::for_multibuffer(multibuffer.clone(), Some(project.clone()), window, cx);
@@ -196,27 +196,24 @@ impl AgentDiffPane {
             editor
         });
 
-        let action_log = thread.action_log(cx).clone();
+        let action_log = thread.action_log(cx);
 
         let mut this = Self {
-            _subscriptions: [
-                Some(
-                    cx.observe_in(&action_log, window, |this, _action_log, window, cx| {
-                        this.update_excerpts(window, cx)
-                    }),
-                ),
+            _subscriptions: vec![
+                cx.observe_in(&action_log, window, |this, _action_log, window, cx| {
+                    this.update_excerpts(window, cx)
+                }),
                 match &thread {
-                    AgentDiffThread::Native(thread) => {
-                        Some(cx.subscribe(&thread, |this, _thread, event, cx| {
-                            this.handle_thread_event(event, cx)
-                        }))
-                    }
-                    AgentDiffThread::AcpThread(_) => None,
+                    AgentDiffThread::Native(thread) => cx
+                        .subscribe(thread, |this, _thread, event, cx| {
+                            this.handle_native_thread_event(event, cx)
+                        }),
+                    AgentDiffThread::AcpThread(thread) => cx
+                        .subscribe(thread, |this, _thread, event, cx| {
+                            this.handle_acp_thread_event(event, cx)
+                        }),
                 },
-            ]
-            .into_iter()
-            .flatten()
-            .collect(),
+            ],
             title: SharedString::default(),
             multibuffer,
             editor,
@@ -288,7 +285,7 @@ impl AgentDiffPane {
                     && buffer
                         .read(cx)
                         .file()
-                        .map_or(false, |file| file.disk_state() == DiskState::Deleted)
+                        .is_some_and(|file| file.disk_state() == DiskState::Deleted)
                 {
                     editor.fold_buffer(snapshot.text.remote_id(), cx)
                 }
@@ -324,10 +321,15 @@ impl AgentDiffPane {
         }
     }
 
-    fn handle_thread_event(&mut self, event: &ThreadEvent, cx: &mut Context<Self>) {
-        match event {
-            ThreadEvent::SummaryGenerated => self.update_title(cx),
-            _ => {}
+    fn handle_native_thread_event(&mut self, event: &ThreadEvent, cx: &mut Context<Self>) {
+        if let ThreadEvent::SummaryGenerated = event {
+            self.update_title(cx)
+        }
+    }
+
+    fn handle_acp_thread_event(&mut self, event: &AcpThreadEvent, cx: &mut Context<Self>) {
+        if let AcpThreadEvent::TitleUpdated = event {
+            self.update_title(cx)
         }
     }
 
@@ -398,7 +400,7 @@ fn keep_edits_in_selection(
         .disjoint_anchor_ranges()
         .collect::<Vec<_>>();
 
-    keep_edits_in_ranges(editor, buffer_snapshot, &thread, ranges, window, cx)
+    keep_edits_in_ranges(editor, buffer_snapshot, thread, ranges, window, cx)
 }
 
 fn reject_edits_in_selection(
@@ -412,7 +414,7 @@ fn reject_edits_in_selection(
         .selections
         .disjoint_anchor_ranges()
         .collect::<Vec<_>>();
-    reject_edits_in_ranges(editor, buffer_snapshot, &thread, ranges, window, cx)
+    reject_edits_in_ranges(editor, buffer_snapshot, thread, ranges, window, cx)
 }
 
 fn keep_edits_in_ranges(
@@ -503,8 +505,7 @@ fn update_editor_selection(
                         &[last_kept_hunk_end..editor::Anchor::max()],
                         buffer_snapshot,
                     )
-                    .skip(1)
-                    .next()
+                    .nth(1)
             })
             .or_else(|| {
                 let first_kept_hunk = diff_hunks.first()?;
@@ -1001,7 +1002,7 @@ impl AgentDiffToolbar {
             return;
         };
 
-        *state = agent_diff.read(cx).editor_state(&editor);
+        *state = agent_diff.read(cx).editor_state(editor);
         self.update_location(cx);
         cx.notify();
     }
@@ -1044,23 +1045,23 @@ impl ToolbarItemView for AgentDiffToolbar {
                 return self.location(cx);
             }
 
-            if let Some(editor) = item.act_as::<Editor>(cx) {
-                if editor.read(cx).mode().is_full() {
-                    let agent_diff = AgentDiff::global(cx);
+            if let Some(editor) = item.act_as::<Editor>(cx)
+                && editor.read(cx).mode().is_full()
+            {
+                let agent_diff = AgentDiff::global(cx);
 
-                    self.active_item = Some(AgentDiffToolbarItem::Editor {
-                        editor: editor.downgrade(),
-                        state: agent_diff.read(cx).editor_state(&editor.downgrade()),
-                        _diff_subscription: cx.observe(&agent_diff, Self::handle_diff_notify),
-                    });
+                self.active_item = Some(AgentDiffToolbarItem::Editor {
+                    editor: editor.downgrade(),
+                    state: agent_diff.read(cx).editor_state(&editor.downgrade()),
+                    _diff_subscription: cx.observe(&agent_diff, Self::handle_diff_notify),
+                });
 
-                    return self.location(cx);
-                }
+                return self.location(cx);
             }
         }
 
         self.active_item = None;
-        return self.location(cx);
+        self.location(cx)
     }
 
     fn pane_focus_update(
@@ -1311,7 +1312,7 @@ impl AgentDiff {
                 let entity = cx.new(|_cx| Self::default());
                 let global = AgentDiffGlobal(entity.clone());
                 cx.set_global(global);
-                entity.clone()
+                entity
             })
     }
 
@@ -1333,7 +1334,7 @@ impl AgentDiff {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        let action_log = thread.action_log(cx).clone();
+        let action_log = thread.action_log(cx);
 
         let action_log_subscription = cx.observe_in(&action_log, window, {
             let workspace = workspace.clone();
@@ -1343,13 +1344,13 @@ impl AgentDiff {
         });
 
         let thread_subscription = match &thread {
-            AgentDiffThread::Native(thread) => cx.subscribe_in(&thread, window, {
+            AgentDiffThread::Native(thread) => cx.subscribe_in(thread, window, {
                 let workspace = workspace.clone();
                 move |this, _thread, event, window, cx| {
                     this.handle_native_thread_event(&workspace, event, window, cx)
                 }
             }),
-            AgentDiffThread::AcpThread(thread) => cx.subscribe_in(&thread, window, {
+            AgentDiffThread::AcpThread(thread) => cx.subscribe_in(thread, window, {
                 let workspace = workspace.clone();
                 move |this, thread, event, window, cx| {
                     this.handle_acp_thread_event(&workspace, thread, event, window, cx)
@@ -1357,11 +1358,11 @@ impl AgentDiff {
             }),
         };
 
-        if let Some(workspace_thread) = self.workspace_threads.get_mut(&workspace) {
+        if let Some(workspace_thread) = self.workspace_threads.get_mut(workspace) {
             // replace thread and action log subscription, but keep editors
             workspace_thread.thread = thread.downgrade();
             workspace_thread._thread_subscriptions = (action_log_subscription, thread_subscription);
-            self.update_reviewing_editors(&workspace, window, cx);
+            self.update_reviewing_editors(workspace, window, cx);
             return;
         }
 
@@ -1506,7 +1507,7 @@ impl AgentDiff {
                     .read(cx)
                     .entries()
                     .last()
-                    .map_or(false, |entry| entry.diffs().next().is_some())
+                    .is_some_and(|entry| entry.diffs().next().is_some())
                 {
                     self.update_reviewing_editors(workspace, window, cx);
                 }
@@ -1516,15 +1517,19 @@ impl AgentDiff {
                     .read(cx)
                     .entries()
                     .get(*ix)
-                    .map_or(false, |entry| entry.diffs().next().is_some())
+                    .is_some_and(|entry| entry.diffs().next().is_some())
                 {
                     self.update_reviewing_editors(workspace, window, cx);
                 }
             }
-            AcpThreadEvent::Stopped
+            AcpThreadEvent::Stopped | AcpThreadEvent::Error | AcpThreadEvent::LoadError(_) => {
+                self.update_reviewing_editors(workspace, window, cx);
+            }
+            AcpThreadEvent::TitleUpdated
+            | AcpThreadEvent::TokenUsageUpdated
+            | AcpThreadEvent::EntriesRemoved(_)
             | AcpThreadEvent::ToolAuthorizationRequired
-            | AcpThreadEvent::Error
-            | AcpThreadEvent::ServerExited(_) => {}
+            | AcpThreadEvent::Retry(_) => {}
         }
     }
 
@@ -1535,21 +1540,11 @@ impl AgentDiff {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        match event {
-            workspace::Event::ItemAdded { item } => {
-                if let Some(editor) = item.downcast::<Editor>() {
-                    if let Some(buffer) = Self::full_editor_buffer(editor.read(cx), cx) {
-                        self.register_editor(
-                            workspace.downgrade(),
-                            buffer.clone(),
-                            editor,
-                            window,
-                            cx,
-                        );
-                    }
-                }
-            }
-            _ => {}
+        if let workspace::Event::ItemAdded { item } = event
+            && let Some(editor) = item.downcast::<Editor>()
+            && let Some(buffer) = Self::full_editor_buffer(editor.read(cx), cx)
+        {
+            self.register_editor(workspace.downgrade(), buffer, editor, window, cx);
         }
     }
 
@@ -1648,7 +1643,7 @@ impl AgentDiff {
                 continue;
             };
 
-            for (weak_editor, _) in buffer_editors {
+            for weak_editor in buffer_editors.keys() {
                 let Some(editor) = weak_editor.upgrade() else {
                     continue;
                 };
@@ -1676,7 +1671,7 @@ impl AgentDiff {
                         editor.register_addon(EditorAgentDiffAddon);
                     });
                 } else {
-                    unaffected.remove(&weak_editor);
+                    unaffected.remove(weak_editor);
                 }
 
                 if new_state == EditorState::Reviewing && previous_state != Some(new_state) {
@@ -1709,7 +1704,7 @@ impl AgentDiff {
                 .read_with(cx, |editor, _cx| editor.workspace())
                 .ok()
                 .flatten()
-                .map_or(false, |editor_workspace| {
+                .is_some_and(|editor_workspace| {
                     editor_workspace.entity_id() == workspace.entity_id()
                 });
 
@@ -1729,7 +1724,7 @@ impl AgentDiff {
 
     fn editor_state(&self, editor: &WeakEntity<Editor>) -> EditorState {
         self.reviewing_editors
-            .get(&editor)
+            .get(editor)
             .cloned()
             .unwrap_or(EditorState::Idle)
     }
@@ -1849,26 +1844,26 @@ impl AgentDiff {
 
         let thread = thread.upgrade()?;
 
-        if let PostReviewState::AllReviewed = review(&editor, &thread, window, cx) {
-            if let Some(curr_buffer) = editor.read(cx).buffer().read(cx).as_singleton() {
-                let changed_buffers = thread.action_log(cx).read(cx).changed_buffers(cx);
-
-                let mut keys = changed_buffers.keys().cycle();
-                keys.find(|k| *k == &curr_buffer);
-                let next_project_path = keys
-                    .next()
-                    .filter(|k| *k != &curr_buffer)
-                    .and_then(|after| after.read(cx).project_path(cx));
-
-                if let Some(path) = next_project_path {
-                    let task = workspace.open_path(path, None, true, window, cx);
-                    let task = cx.spawn(async move |_, _cx| task.await.map(|_| ()));
-                    return Some(task);
-                }
+        if let PostReviewState::AllReviewed = review(&editor, &thread, window, cx)
+            && let Some(curr_buffer) = editor.read(cx).buffer().read(cx).as_singleton()
+        {
+            let changed_buffers = thread.action_log(cx).read(cx).changed_buffers(cx);
+
+            let mut keys = changed_buffers.keys().cycle();
+            keys.find(|k| *k == &curr_buffer);
+            let next_project_path = keys
+                .next()
+                .filter(|k| *k != &curr_buffer)
+                .and_then(|after| after.read(cx).project_path(cx));
+
+            if let Some(path) = next_project_path {
+                let task = workspace.open_path(path, None, true, window, cx);
+                let task = cx.spawn(async move |_, _cx| task.await.map(|_| ()));
+                return Some(task);
             }
         }
 
-        return Some(Task::ready(Ok(())));
+        Some(Task::ready(Ok(())))
     }
 }
 

crates/agent_ui/src/agent_model_selector.rs 🔗

@@ -66,10 +66,8 @@ impl AgentModelSelector {
                                     fs.clone(),
                                     cx,
                                     move |settings, _cx| {
-                                        settings.set_inline_assistant_model(
-                                            provider.clone(),
-                                            model_id.clone(),
-                                        );
+                                        settings
+                                            .set_inline_assistant_model(provider.clone(), model_id);
                                     },
                                 );
                             }

crates/agent_ui/src/agent_panel.rs 🔗

@@ -1,23 +1,22 @@
-use std::cell::RefCell;
 use std::ops::{Not, Range};
 use std::path::Path;
 use std::rc::Rc;
 use std::sync::Arc;
 use std::time::Duration;
 
-use agent_servers::AgentServer;
+use acp_thread::AcpThread;
+use agent2::{DbThreadMetadata, HistoryEntry};
 use db::kvp::{Dismissable, KEY_VALUE_STORE};
 use serde::{Deserialize, Serialize};
 
-use crate::NewExternalAgentThread;
+use crate::acp::{AcpThreadHistory, ThreadHistoryEvent};
 use crate::agent_diff::AgentDiffThread;
-use crate::message_editor::{MAX_EDITOR_LINES, MIN_EDITOR_LINES};
-use crate::ui::NewThreadButton;
 use crate::{
     AddContextServer, AgentDiffPane, ContinueThread, ContinueWithBurnMode,
     DeleteRecentlyOpenThread, ExpandMessageEditor, Follow, InlineAssistant, NewTextThread,
     NewThread, OpenActiveThreadAsMarkdown, OpenAgentDiff, OpenHistory, ResetTrialEndUpsell,
-    ResetTrialUpsell, ToggleBurnMode, ToggleContextPicker, ToggleNavigationMenu, ToggleOptionsMenu,
+    ResetTrialUpsell, ToggleBurnMode, ToggleContextPicker, ToggleNavigationMenu,
+    ToggleNewThreadMenu, ToggleOptionsMenu,
     acp::AcpThreadView,
     active_thread::{self, ActiveThread, ActiveThreadEvent},
     agent_configuration::{AgentConfiguration, AssistantConfigurationEvent},
@@ -31,6 +30,7 @@ use crate::{
     thread_history::{HistoryEntryElement, ThreadHistory},
     ui::{AgentOnboardingModal, EndTrialUpsell},
 };
+use crate::{ExternalAgent, NewExternalAgentThread, NewNativeAgentThreadFromSummary};
 use agent::{
     Thread, ThreadError, ThreadEvent, ThreadId, ThreadSummary, TokenUsageRatio,
     context_store::ContextStore,
@@ -46,13 +46,12 @@ use assistant_tool::ToolWorkingSet;
 use client::{UserStore, zed_urls};
 use cloud_llm_client::{CompletionIntent, Plan, UsageLimit};
 use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer};
-use feature_flags::{self, FeatureFlagAppExt};
+use feature_flags::{self, ClaudeCodeFeatureFlag, FeatureFlagAppExt, GeminiAndNativeFeatureFlag};
 use fs::Fs;
 use gpui::{
     Action, Animation, AnimationExt as _, AnyElement, App, AsyncWindowContext, ClipboardItem,
-    Corner, DismissEvent, Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable, Hsla,
-    KeyContext, Pixels, Subscription, Task, UpdateGlobal, WeakEntity, prelude::*,
-    pulsating_between,
+    Corner, DismissEvent, Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable, KeyContext,
+    Pixels, Subscription, Task, UpdateGlobal, WeakEntity, prelude::*, pulsating_between,
 };
 use language::LanguageRegistry;
 use language_model::{
@@ -86,6 +85,7 @@ const AGENT_PANEL_KEY: &str = "agent_panel";
 #[derive(Serialize, Deserialize)]
 struct SerializedAgentPanel {
     width: Option<Pixels>,
+    selected_agent: Option<AgentType>,
 }
 
 pub fn init(cx: &mut App) {
@@ -98,6 +98,16 @@ pub fn init(cx: &mut App) {
                         workspace.focus_panel::<AgentPanel>(window, cx);
                     }
                 })
+                .register_action(
+                    |workspace, action: &NewNativeAgentThreadFromSummary, window, cx| {
+                        if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
+                            panel.update(cx, |panel, cx| {
+                                panel.new_native_agent_thread_from_summary(action, window, cx)
+                            });
+                            workspace.focus_panel::<AgentPanel>(window, cx);
+                        }
+                    },
+                )
                 .register_action(|workspace, _: &OpenHistory, window, cx| {
                     if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
                         workspace.focus_panel::<AgentPanel>(window, cx);
@@ -120,7 +130,7 @@ pub fn init(cx: &mut App) {
                     if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
                         workspace.focus_panel::<AgentPanel>(window, cx);
                         panel.update(cx, |panel, cx| {
-                            panel.new_external_thread(action.agent, window, cx)
+                            panel.external_thread(action.agent, None, None, window, cx)
                         });
                     }
                 })
@@ -179,6 +189,14 @@ pub fn init(cx: &mut App) {
                         });
                     }
                 })
+                .register_action(|workspace, _: &ToggleNewThreadMenu, window, cx| {
+                    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
+                        workspace.focus_panel::<AgentPanel>(window, cx);
+                        panel.update(cx, |panel, cx| {
+                            panel.toggle_new_thread_menu(&ToggleNewThreadMenu, window, cx);
+                        });
+                    }
+                })
                 .register_action(|workspace, _: &OpenOnboardingModal, window, cx| {
                     AgentOnboardingModal::toggle(workspace, window, cx)
                 })
@@ -223,6 +241,35 @@ enum WhichFontSize {
     None,
 }
 
+#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
+pub enum AgentType {
+    #[default]
+    Zed,
+    TextThread,
+    Gemini,
+    ClaudeCode,
+    NativeAgent,
+}
+
+impl AgentType {
+    fn label(self) -> impl Into<SharedString> {
+        match self {
+            Self::Zed | Self::TextThread => "Zed Agent",
+            Self::NativeAgent => "Agent 2",
+            Self::Gemini => "Gemini CLI",
+            Self::ClaudeCode => "Claude Code",
+        }
+    }
+
+    fn icon(self) -> Option<IconName> {
+        match self {
+            Self::Zed | Self::NativeAgent | Self::TextThread => None,
+            Self::Gemini => Some(IconName::AiGemini),
+            Self::ClaudeCode => Some(IconName::AiClaude),
+        }
+    }
+}
+
 impl ActiveView {
     pub fn which_font_size_used(&self) -> WhichFontSize {
         match self {
@@ -317,7 +364,7 @@ impl ActiveView {
         Self::Thread {
             change_title_editor: editor,
             thread: active_thread,
-            message_editor: message_editor,
+            message_editor,
             _subscriptions: subscriptions,
         }
     }
@@ -325,6 +372,7 @@ impl ActiveView {
     pub fn prompt_editor(
         context_editor: Entity<TextThreadEditor>,
         history_store: Entity<HistoryStore>,
+        acp_history_store: Entity<agent2::HistoryStore>,
         language_registry: Arc<LanguageRegistry>,
         window: &mut Window,
         cx: &mut App,
@@ -402,6 +450,18 @@ impl ActiveView {
                                 );
                             }
                         });
+
+                        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()),
+                                    cx,
+                                );
+                            }
+                        });
                     }
                     _ => {}
                 }
@@ -430,6 +490,8 @@ pub struct AgentPanel {
     fs: Arc<dyn Fs>,
     language_registry: Arc<LanguageRegistry>,
     thread_store: Entity<ThreadStore>,
+    acp_history: Entity<AcpThreadHistory>,
+    acp_history_store: Entity<agent2::HistoryStore>,
     _default_model_subscription: Subscription,
     context_store: Entity<TextThreadStore>,
     prompt_store: Option<Entity<PromptStore>>,
@@ -438,8 +500,6 @@ pub struct AgentPanel {
     configuration_subscription: Option<Subscription>,
     local_timezone: UtcOffset,
     active_view: ActiveView,
-    acp_message_history:
-        Rc<RefCell<crate::acp::MessageHistory<Vec<agent_client_protocol::ContentBlock>>>>,
     previous_view: Option<ActiveView>,
     history_store: Entity<HistoryStore>,
     history: Entity<ThreadHistory>,
@@ -453,21 +513,27 @@ pub struct AgentPanel {
     zoomed: bool,
     pending_serialization: Option<Task<Result<()>>>,
     onboarding: Entity<AgentPanelOnboarding>,
+    selected_agent: AgentType,
 }
 
 impl AgentPanel {
     fn serialize(&mut self, cx: &mut Context<Self>) {
         let width = self.width;
+        let selected_agent = self.selected_agent;
         self.pending_serialization = Some(cx.background_spawn(async move {
             KEY_VALUE_STORE
                 .write_kvp(
                     AGENT_PANEL_KEY.into(),
-                    serde_json::to_string(&SerializedAgentPanel { width })?,
+                    serde_json::to_string(&SerializedAgentPanel {
+                        width,
+                        selected_agent: Some(selected_agent),
+                    })?,
                 )
                 .await?;
             anyhow::Ok(())
         }));
     }
+
     pub fn load(
         workspace: WeakEntity<Workspace>,
         prompt_builder: Arc<PromptBuilder>,
@@ -517,6 +583,17 @@ impl AgentPanel {
                 None
             };
 
+            // Wait for the Gemini/Native feature flag to be available.
+            let client = workspace.read_with(cx, |workspace, _| workspace.client().clone())?;
+            if !client.status().borrow().is_signed_out() {
+                cx.update(|_, cx| {
+                    cx.wait_for_flag_or_timeout::<feature_flags::GeminiAndNativeFeatureFlag>(
+                        Duration::from_secs(2),
+                    )
+                })?
+                .await;
+            }
+
             let panel = workspace.update_in(cx, |workspace, window, cx| {
                 let panel = cx.new(|cx| {
                     Self::new(
@@ -531,6 +608,10 @@ impl AgentPanel {
                 if let Some(serialized_panel) = serialized_panel {
                     panel.update(cx, |panel, cx| {
                         panel.width = serialized_panel.width.map(|w| w.round());
+                        if let Some(selected_agent) = serialized_panel.selected_agent {
+                            panel.selected_agent = selected_agent;
+                            panel.new_agent_thread(selected_agent, window, cx);
+                        }
                         cx.notify();
                     });
                 }
@@ -589,6 +670,29 @@ impl AgentPanel {
             )
         });
 
+        let acp_history_store = cx.new(|cx| agent2::HistoryStore::new(context_store.clone(), cx));
+        let acp_history = cx.new(|cx| AcpThreadHistory::new(acp_history_store.clone(), window, cx));
+        cx.subscribe_in(
+            &acp_history,
+            window,
+            |this, _, event, window, cx| match event {
+                ThreadHistoryEvent::Open(HistoryEntry::AcpThread(thread)) => {
+                    this.external_thread(
+                        Some(crate::ExternalAgent::NativeAgent),
+                        Some(thread.clone()),
+                        None,
+                        window,
+                        cx,
+                    );
+                }
+                ThreadHistoryEvent::Open(HistoryEntry::TextThread(thread)) => {
+                    this.open_saved_prompt_editor(thread.path.clone(), window, cx)
+                        .detach_and_log_err(cx);
+                }
+            },
+        )
+        .detach();
+
         cx.observe(&history_store, |_, _, cx| cx.notify()).detach();
 
         let active_thread = cx.new(|cx| {
@@ -627,6 +731,7 @@ impl AgentPanel {
                 ActiveView::prompt_editor(
                     context_editor,
                     history_store.clone(),
+                    acp_history_store.clone(),
                     language_registry.clone(),
                     window,
                     cx,
@@ -643,7 +748,11 @@ impl AgentPanel {
             let assistant_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);
+                        if cx.has_flag::<GeminiAndNativeFeatureFlag>() {
+                            menu = Self::populate_recently_opened_menu_section_new(menu, panel, cx);
+                        } else {
+                            menu = Self::populate_recently_opened_menu_section_old(menu, panel, cx);
+                        }
                     }
                     menu.action("View All", Box::new(OpenHistory))
                         .end_slot_action(DeleteRecentlyOpenThread.boxed_clone())
@@ -669,25 +778,25 @@ impl AgentPanel {
                 .ok();
         });
 
-        let _default_model_subscription = cx.subscribe(
-            &LanguageModelRegistry::global(cx),
-            |this, _, event: &language_model::Event, cx| match event {
-                language_model::Event::DefaultModelChanged => match &this.active_view {
-                    ActiveView::Thread { thread, .. } => {
-                        thread
-                            .read(cx)
-                            .thread()
-                            .clone()
-                            .update(cx, |thread, cx| thread.get_or_init_configured_model(cx));
+        let _default_model_subscription =
+            cx.subscribe(
+                &LanguageModelRegistry::global(cx),
+                |this, _, event: &language_model::Event, cx| {
+                    if let language_model::Event::DefaultModelChanged = event {
+                        match &this.active_view {
+                            ActiveView::Thread { thread, .. } => {
+                                thread.read(cx).thread().clone().update(cx, |thread, cx| {
+                                    thread.get_or_init_configured_model(cx)
+                                });
+                            }
+                            ActiveView::ExternalAgentThread { .. }
+                            | ActiveView::TextThread { .. }
+                            | ActiveView::History
+                            | ActiveView::Configuration => {}
+                        }
                     }
-                    ActiveView::ExternalAgentThread { .. }
-                    | ActiveView::TextThread { .. }
-                    | ActiveView::History
-                    | ActiveView::Configuration => {}
                 },
-                _ => {}
-            },
-        );
+            );
 
         let onboarding = cx.new(|cx| {
             AgentPanelOnboarding::new(
@@ -719,7 +828,6 @@ impl AgentPanel {
             .unwrap(),
             inline_assist_context_store,
             previous_view: None,
-            acp_message_history: Default::default(),
             history_store: history_store.clone(),
             history: cx.new(|cx| ThreadHistory::new(weak_self, history_store, window, cx)),
             hovered_recent_history_item: None,
@@ -732,6 +840,9 @@ impl AgentPanel {
             zoomed: false,
             pending_serialization: None,
             onboarding,
+            acp_history,
+            acp_history_store,
+            selected_agent: AgentType::default(),
         }
     }
 
@@ -775,10 +886,10 @@ impl AgentPanel {
             ActiveView::Thread { thread, .. } => {
                 thread.update(cx, |thread, cx| thread.cancel_last_completion(window, cx));
             }
-            ActiveView::ExternalAgentThread { thread_view, .. } => {
-                thread_view.update(cx, |thread_element, cx| thread_element.cancel(cx));
-            }
-            ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {}
+            ActiveView::ExternalAgentThread { .. }
+            | ActiveView::TextThread { .. }
+            | ActiveView::History
+            | ActiveView::Configuration => {}
         }
     }
 
@@ -792,7 +903,20 @@ impl AgentPanel {
         }
     }
 
+    fn active_thread_view(&self) -> Option<&Entity<AcpThreadView>> {
+        match &self.active_view {
+            ActiveView::ExternalAgentThread { thread_view } => Some(thread_view),
+            ActiveView::Thread { .. }
+            | ActiveView::TextThread { .. }
+            | ActiveView::History
+            | ActiveView::Configuration => None,
+        }
+    }
+
     fn new_thread(&mut self, action: &NewThread, window: &mut Window, cx: &mut Context<Self>) {
+        if cx.has_flag::<GeminiAndNativeFeatureFlag>() {
+            return self.new_agent_thread(AgentType::NativeAgent, window, cx);
+        }
         // Preserve chat box text when using creating new thread
         let preserved_text = self
             .active_message_editor()
@@ -865,12 +989,35 @@ impl AgentPanel {
 
         message_editor.focus_handle(cx).focus(window);
 
-        let thread_view = ActiveView::thread(active_thread.clone(), message_editor, window, cx);
+        let thread_view = ActiveView::thread(active_thread, message_editor, window, cx);
         self.set_active_view(thread_view, window, cx);
 
         AgentDiff::set_active_thread(&self.workspace, thread.clone(), window, cx);
     }
 
+    fn new_native_agent_thread_from_summary(
+        &mut self,
+        action: &NewNativeAgentThreadFromSummary,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        let Some(thread) = self
+            .acp_history_store
+            .read(cx)
+            .thread_from_session_id(&action.from_session_id)
+        else {
+            return;
+        };
+
+        self.external_thread(
+            Some(ExternalAgent::NativeAgent),
+            None,
+            Some(thread.clone()),
+            window,
+            cx,
+        );
+    }
+
     fn new_prompt_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) {
         let context = self
             .context_store
@@ -897,6 +1044,7 @@ impl AgentPanel {
             ActiveView::prompt_editor(
                 context_editor.clone(),
                 self.history_store.clone(),
+                self.acp_history_store.clone(),
                 self.language_registry.clone(),
                 window,
                 cx,
@@ -907,15 +1055,17 @@ impl AgentPanel {
         context_editor.focus_handle(cx).focus(window);
     }
 
-    fn new_external_thread(
+    fn external_thread(
         &mut self,
         agent_choice: Option<crate::ExternalAgent>,
+        resume_thread: Option<DbThreadMetadata>,
+        summarize_thread: Option<DbThreadMetadata>,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
         let workspace = self.workspace.clone();
         let project = self.project.clone();
-        let message_history = self.acp_message_history.clone();
+        let fs = self.fs.clone();
 
         const LAST_USED_EXTERNAL_AGENT_KEY: &str = "agent_panel__last_used_external_agent";
 
@@ -924,8 +1074,10 @@ impl AgentPanel {
             agent: crate::ExternalAgent,
         }
 
+        let history = self.acp_history_store.clone();
+
         cx.spawn_in(window, async move |this, cx| {
-            let server: Rc<dyn AgentServer> = match agent_choice {
+            let ext_agent = match agent_choice {
                 Some(agent) => {
                     cx.background_spawn(async move {
                         if let Some(serialized) =
@@ -939,10 +1091,10 @@ impl AgentPanel {
                     })
                     .detach();
 
-                    agent.server()
+                    agent
                 }
-                None => cx
-                    .background_spawn(async move {
+                None => {
+                    cx.background_spawn(async move {
                         KEY_VALUE_STORE.read_kvp(LAST_USED_EXTERNAL_AGENT_KEY)
                     })
                     .await
@@ -953,18 +1105,34 @@ impl AgentPanel {
                     })
                     .unwrap_or_default()
                     .agent
-                    .server(),
+                }
             };
 
+            let server = ext_agent.server(fs, history);
+
             this.update_in(cx, |this, window, cx| {
+                match ext_agent {
+                    crate::ExternalAgent::Gemini | crate::ExternalAgent::NativeAgent => {
+                        if !cx.has_flag::<GeminiAndNativeFeatureFlag>() {
+                            return;
+                        }
+                    }
+                    crate::ExternalAgent::ClaudeCode => {
+                        if !cx.has_flag::<ClaudeCodeFeatureFlag>() {
+                            return;
+                        }
+                    }
+                }
+
                 let thread_view = cx.new(|cx| {
                     crate::acp::AcpThreadView::new(
                         server,
+                        resume_thread,
+                        summarize_thread,
                         workspace.clone(),
                         project,
-                        message_history,
-                        MIN_EDITOR_LINES,
-                        Some(MAX_EDITOR_LINES),
+                        this.acp_history_store.clone(),
+                        this.prompt_store.clone(),
                         window,
                         cx,
                     )
@@ -1053,8 +1221,9 @@ impl AgentPanel {
         });
         self.set_active_view(
             ActiveView::prompt_editor(
-                editor.clone(),
+                editor,
                 self.history_store.clone(),
+                self.acp_history_store.clone(),
                 self.language_registry.clone(),
                 window,
                 cx,
@@ -1125,7 +1294,7 @@ impl AgentPanel {
         });
         message_editor.focus_handle(cx).focus(window);
 
-        let thread_view = ActiveView::thread(active_thread.clone(), message_editor, window, cx);
+        let thread_view = ActiveView::thread(active_thread, message_editor, window, cx);
         self.set_active_view(thread_view, window, cx);
         AgentDiff::set_active_thread(&self.workspace, thread.clone(), window, cx);
     }
@@ -1173,6 +1342,15 @@ impl AgentPanel {
         self.agent_panel_menu_handle.toggle(window, cx);
     }
 
+    pub fn toggle_new_thread_menu(
+        &mut self,
+        _: &ToggleNewThreadMenu,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        self.new_thread_menu_handle.toggle(window, cx);
+    }
+
     pub fn increase_font_size(
         &mut self,
         action: &IncreaseBufferFontSize,
@@ -1203,13 +1381,11 @@ impl AgentPanel {
                                 ThemeSettings::get_global(cx).agent_font_size(cx) + delta;
                             let _ = settings
                                 .agent_font_size
-                                .insert(theme::clamp_font_size(agent_font_size).0);
+                                .insert(Some(theme::clamp_font_size(agent_font_size).into()));
                         },
                     );
                 } else {
-                    theme::adjust_agent_font_size(cx, |size| {
-                        *size += delta;
-                    });
+                    theme::adjust_agent_font_size(cx, |size| size + delta);
                 }
             }
             WhichFontSize::BufferFont => {
@@ -1345,15 +1521,14 @@ impl AgentPanel {
             AssistantConfigurationEvent::NewThread(provider) => {
                 if LanguageModelRegistry::read_global(cx)
                     .default_model()
-                    .map_or(true, |model| model.provider.id() != provider.id())
+                    .is_none_or(|model| model.provider.id() != provider.id())
+                    && let Some(model) = provider.default_model(cx)
                 {
-                    if let Some(model) = provider.default_model(cx) {
-                        update_settings_file::<AgentSettings>(
-                            self.fs.clone(),
-                            cx,
-                            move |settings, _| settings.set_model(model),
-                        );
-                    }
+                    update_settings_file::<AgentSettings>(
+                        self.fs.clone(),
+                        cx,
+                        move |settings, _| settings.set_model(model),
+                    );
                 }
 
                 self.new_thread(&NewThread::default(), window, cx);
@@ -1380,6 +1555,14 @@ impl AgentPanel {
             _ => None,
         }
     }
+    pub(crate) fn active_agent_thread(&self, cx: &App) -> Option<Entity<AcpThread>> {
+        match &self.active_view {
+            ActiveView::ExternalAgentThread { thread_view, .. } => {
+                thread_view.read(cx).thread().cloned()
+            }
+            _ => None,
+        }
+    }
 
     pub(crate) fn delete_thread(
         &mut self,
@@ -1400,7 +1583,7 @@ impl AgentPanel {
             return;
         }
 
-        let model = thread_state.configured_model().map(|cm| cm.model.clone());
+        let model = thread_state.configured_model().map(|cm| cm.model);
         if let Some(model) = model {
             thread.update(cx, |active_thread, cx| {
                 active_thread.thread().update(cx, |thread, cx| {
@@ -1472,17 +1655,14 @@ impl AgentPanel {
         let current_is_special = current_is_history || current_is_config;
         let new_is_special = new_is_history || new_is_config;
 
-        match &self.active_view {
-            ActiveView::Thread { thread, .. } => {
-                let thread = thread.read(cx);
-                if thread.is_empty() {
-                    let id = thread.thread().read(cx).id().clone();
-                    self.history_store.update(cx, |store, cx| {
-                        store.remove_recently_opened_thread(id, cx);
-                    });
-                }
+        if let ActiveView::Thread { thread, .. } = &self.active_view {
+            let thread = thread.read(cx);
+            if thread.is_empty() {
+                let id = thread.thread().read(cx).id().clone();
+                self.history_store.update(cx, |store, cx| {
+                    store.remove_recently_opened_thread(id, cx);
+                });
             }
-            _ => {}
         }
 
         match &new_view {
@@ -1495,6 +1675,14 @@ impl AgentPanel {
                     if let Some(path) = context_editor.read(cx).context().read(cx).path() {
                         store.push_recently_opened_entry(HistoryEntryId::Context(path.clone()), cx)
                     }
+                });
+                self.acp_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::ExternalAgentThread { .. } => {}
@@ -1512,12 +1700,10 @@ impl AgentPanel {
             self.active_view = new_view;
         }
 
-        self.acp_message_history.borrow_mut().reset_position();
-
         self.focus_handle(cx).focus(window);
     }
 
-    fn populate_recently_opened_menu_section(
+    fn populate_recently_opened_menu_section_old(
         mut menu: ContextMenu,
         panel: Entity<Self>,
         cx: &mut Context<ContextMenu>,
@@ -1552,7 +1738,7 @@ impl AgentPanel {
                                     .open_thread_by_id(&id, window, cx)
                                     .detach_and_log_err(cx),
                                 HistoryEntryId::Context(path) => this
-                                    .open_saved_prompt_editor(path.clone(), window, cx)
+                                    .open_saved_prompt_editor(path, window, cx)
                                     .detach_and_log_err(cx),
                             })
                             .ok();
@@ -1580,6 +1766,144 @@ impl AgentPanel {
 
         menu
     }
+
+    fn populate_recently_opened_menu_section_new(
+        mut menu: ContextMenu,
+        panel: Entity<Self>,
+        cx: &mut Context<ContextMenu>,
+    ) -> ContextMenu {
+        let entries = panel
+            .read(cx)
+            .acp_history_store
+            .read(cx)
+            .recently_opened_entries(cx);
+
+        if entries.is_empty() {
+            return menu;
+        }
+
+        menu = menu.header("Recently Opened");
+
+        for entry in entries {
+            let title = entry.title().clone();
+
+            menu = menu.entry_with_end_slot_on_hover(
+                title,
+                None,
+                {
+                    let panel = panel.downgrade();
+                    let entry = entry.clone();
+                    move |window, cx| {
+                        let entry = entry.clone();
+                        panel
+                            .update(cx, move |this, cx| match &entry {
+                                agent2::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)
+                                    .detach_and_log_err(cx),
+                            })
+                            .ok();
+                    }
+                },
+                IconName::Close,
+                "Close Entry".into(),
+                {
+                    let panel = panel.downgrade();
+                    let id = entry.id();
+                    move |_window, cx| {
+                        panel
+                            .update(cx, |this, cx| {
+                                this.acp_history_store.update(cx, |history_store, cx| {
+                                    history_store.remove_recently_opened_entry(&id, cx);
+                                });
+                            })
+                            .ok();
+                    }
+                },
+            );
+        }
+
+        menu = menu.separator();
+
+        menu
+    }
+
+    pub fn set_selected_agent(
+        &mut self,
+        agent: AgentType,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        if self.selected_agent != agent {
+            self.selected_agent = agent;
+            self.serialize(cx);
+        }
+        self.new_agent_thread(agent, window, cx);
+    }
+
+    pub fn selected_agent(&self) -> AgentType {
+        self.selected_agent
+    }
+
+    pub fn new_agent_thread(
+        &mut self,
+        agent: AgentType,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        match agent {
+            AgentType::Zed => {
+                window.dispatch_action(
+                    NewThread {
+                        from_thread_id: None,
+                    }
+                    .boxed_clone(),
+                    cx,
+                );
+            }
+            AgentType::TextThread => {
+                window.dispatch_action(NewTextThread.boxed_clone(), cx);
+            }
+            AgentType::NativeAgent => self.external_thread(
+                Some(crate::ExternalAgent::NativeAgent),
+                None,
+                None,
+                window,
+                cx,
+            ),
+            AgentType::Gemini => {
+                self.external_thread(Some(crate::ExternalAgent::Gemini), None, None, window, cx)
+            }
+            AgentType::ClaudeCode => self.external_thread(
+                Some(crate::ExternalAgent::ClaudeCode),
+                None,
+                None,
+                window,
+                cx,
+            ),
+        }
+    }
+
+    pub fn load_agent_thread(
+        &mut self,
+        thread: DbThreadMetadata,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        self.external_thread(
+            Some(ExternalAgent::NativeAgent),
+            Some(thread),
+            None,
+            window,
+            cx,
+        );
+    }
 }
 
 impl Focusable for AgentPanel {
@@ -1587,7 +1911,13 @@ impl Focusable for AgentPanel {
         match &self.active_view {
             ActiveView::Thread { message_editor, .. } => message_editor.focus_handle(cx),
             ActiveView::ExternalAgentThread { thread_view, .. } => thread_view.focus_handle(cx),
-            ActiveView::History => self.history.focus_handle(cx),
+            ActiveView::History => {
+                if cx.has_flag::<feature_flags::GeminiAndNativeFeatureFlag>() {
+                    self.acp_history.focus_handle(cx)
+                } else {
+                    self.history.focus_handle(cx)
+                }
+            }
             ActiveView::TextThread { context_editor, .. } => context_editor.focus_handle(cx),
             ActiveView::Configuration => {
                 if let Some(configuration) = self.configuration.as_ref() {
@@ -1709,7 +2039,7 @@ impl AgentPanel {
                 };
 
                 match state {
-                    ThreadSummary::Pending => Label::new(ThreadSummary::DEFAULT.clone())
+                    ThreadSummary::Pending => Label::new(ThreadSummary::DEFAULT)
                         .truncate()
                         .into_any_element(),
                     ThreadSummary::Generating => Label::new(LOADING_SUMMARY_PLACEHOLDER)
@@ -1723,7 +2053,8 @@ impl AgentPanel {
                         .w_full()
                         .child(change_title_editor.clone())
                         .child(
-                            ui::IconButton::new("retry-summary-generation", IconName::RotateCcw)
+                            IconButton::new("retry-summary-generation", IconName::RotateCcw)
+                                .icon_size(IconSize::Small)
                                 .on_click({
                                     let active_thread = active_thread.clone();
                                     move |_, _window, cx| {
@@ -1775,7 +2106,8 @@ impl AgentPanel {
                         .w_full()
                         .child(title_editor.clone())
                         .child(
-                            ui::IconButton::new("retry-summary-generation", IconName::RotateCcw)
+                            IconButton::new("retry-summary-generation", IconName::RotateCcw)
+                                .icon_size(IconSize::Small)
                                 .on_click({
                                     let context_editor = context_editor.clone();
                                     move |_, _window, cx| {
@@ -1810,75 +2142,165 @@ impl AgentPanel {
             .into_any()
     }
 
-    fn render_toolbar(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+    fn render_panel_options_menu(
+        &self,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) -> impl IntoElement {
         let user_store = self.user_store.read(cx);
         let usage = user_store.model_request_usage();
-
         let account_url = zed_urls::account_url(cx);
 
         let focus_handle = self.focus_handle(cx);
 
-        let go_back_button = div().child(
-            IconButton::new("go-back", IconName::ArrowLeft)
-                .icon_size(IconSize::Small)
-                .on_click(cx.listener(|this, _, window, cx| {
-                    this.go_back(&workspace::GoBack, window, cx);
-                }))
-                .tooltip({
+        let full_screen_label = if self.is_zoomed(window, cx) {
+            "Disable Full Screen"
+        } else {
+            "Enable Full Screen"
+        };
+
+        PopoverMenu::new("agent-options-menu")
+            .trigger_with_tooltip(
+                IconButton::new("agent-options-menu", IconName::Ellipsis)
+                    .icon_size(IconSize::Small),
+                {
                     let focus_handle = focus_handle.clone();
                     move |window, cx| {
                         Tooltip::for_action_in(
-                            "Go Back",
-                            &workspace::GoBack,
+                            "Toggle Agent Menu",
+                            &ToggleOptionsMenu,
                             &focus_handle,
                             window,
                             cx,
                         )
                     }
-                }),
-        );
-
-        let recent_entries_menu = div().child(
-            PopoverMenu::new("agent-nav-menu")
-                .trigger_with_tooltip(
-                    IconButton::new("agent-nav-menu", IconName::MenuAlt)
-                        .icon_size(IconSize::Small)
-                        .style(ui::ButtonStyle::Subtle),
-                    {
-                        let focus_handle = focus_handle.clone();
-                        move |window, cx| {
-                            Tooltip::for_action_in(
-                                "Toggle Panel Menu",
-                                &ToggleNavigationMenu,
-                                &focus_handle,
-                                window,
-                                cx,
-                            )
-                        }
-                    },
-                )
-                .anchor(Corner::TopLeft)
-                .with_handle(self.assistant_navigation_menu_handle.clone())
-                .menu({
-                    let menu = self.assistant_navigation_menu.clone();
-                    move |window, cx| {
-                        if let Some(menu) = menu.as_ref() {
-                            menu.update(cx, |_, cx| {
-                                cx.defer_in(window, |menu, window, cx| {
-                                    menu.rebuild(window, cx);
-                                });
-                            })
+                },
+            )
+            .anchor(Corner::TopRight)
+            .with_handle(self.agent_panel_menu_handle.clone())
+            .menu({
+                move |window, cx| {
+                    Some(ContextMenu::build(window, cx, |mut menu, _window, _| {
+                        menu = menu.context(focus_handle.clone());
+                        if let Some(usage) = usage {
+                            menu = menu
+                                .header_with_link("Prompt Usage", "Manage", account_url.clone())
+                                .custom_entry(
+                                    move |_window, cx| {
+                                        let used_percentage = match usage.limit {
+                                            UsageLimit::Limited(limit) => {
+                                                Some((usage.amount as f32 / limit as f32) * 100.)
+                                            }
+                                            UsageLimit::Unlimited => None,
+                                        };
+
+                                        h_flex()
+                                            .flex_1()
+                                            .gap_1p5()
+                                            .children(used_percentage.map(|percent| {
+                                                ProgressBar::new("usage", percent, 100., cx)
+                                            }))
+                                            .child(
+                                                Label::new(match usage.limit {
+                                                    UsageLimit::Limited(limit) => {
+                                                        format!("{} / {limit}", usage.amount)
+                                                    }
+                                                    UsageLimit::Unlimited => {
+                                                        format!("{} / ∞", usage.amount)
+                                                    }
+                                                })
+                                                .size(LabelSize::Small)
+                                                .color(Color::Muted),
+                                            )
+                                            .into_any_element()
+                                    },
+                                    move |_, cx| cx.open_url(&zed_urls::account_url(cx)),
+                                )
+                                .separator()
                         }
-                        menu.clone()
+
+                        menu = menu
+                            .header("MCP Servers")
+                            .action(
+                                "View Server Extensions",
+                                Box::new(zed_actions::Extensions {
+                                    category_filter: Some(
+                                        zed_actions::ExtensionCategoryFilter::ContextServers,
+                                    ),
+                                    id: None,
+                                }),
+                            )
+                            .action("Add Custom Server…", Box::new(AddContextServer))
+                            .separator();
+
+                        menu = menu
+                            .action("Rules…", Box::new(OpenRulesLibrary::default()))
+                            .action("Settings", Box::new(OpenSettings))
+                            .separator()
+                            .action(full_screen_label, Box::new(ToggleZoom));
+                        menu
+                    }))
+                }
+            })
+    }
+
+    fn render_recent_entries_menu(
+        &self,
+        icon: IconName,
+        corner: Corner,
+        cx: &mut Context<Self>,
+    ) -> impl IntoElement {
+        let focus_handle = self.focus_handle(cx);
+
+        PopoverMenu::new("agent-nav-menu")
+            .trigger_with_tooltip(
+                IconButton::new("agent-nav-menu", icon).icon_size(IconSize::Small),
+                {
+                    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())
+            .menu({
+                let menu = self.assistant_navigation_menu.clone();
+                move |window, cx| {
+                    if let Some(menu) = menu.as_ref() {
+                        menu.update(cx, |_, cx| {
+                            cx.defer_in(window, |menu, window, cx| {
+                                menu.rebuild(window, cx);
+                            });
+                        })
+                    }
+                    menu.clone()
+                }
+            })
+    }
 
-        let full_screen_label = if self.is_zoomed(window, cx) {
-            "Disable Full Screen"
-        } else {
-            "Enable Full Screen"
-        };
+    fn render_toolbar_back_button(&self, cx: &mut Context<Self>) -> impl IntoElement {
+        let focus_handle = self.focus_handle(cx);
+
+        IconButton::new("go-back", IconName::ArrowLeft)
+            .icon_size(IconSize::Small)
+            .on_click(cx.listener(|this, _, window, cx| {
+                this.go_back(&workspace::GoBack, window, cx);
+            }))
+            .tooltip({
+                move |window, cx| {
+                    Tooltip::for_action_in("Go Back", &workspace::GoBack, &focus_handle, window, cx)
+                }
+            })
+    }
+
+    fn render_toolbar_old(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+        let focus_handle = self.focus_handle(cx);
 
         let active_thread = match &self.active_view {
             ActiveView::Thread { thread, .. } => Some(thread.read(cx).thread().clone()),

crates/agent_ui/src/agent_ui.rs 🔗

@@ -5,7 +5,6 @@ mod agent_diff;
 mod agent_model_selector;
 mod agent_panel;
 mod buffer_codegen;
-mod burn_mode_tooltip;
 mod context_picker;
 mod context_server_configuration;
 mod context_strip;
@@ -64,6 +63,8 @@ actions!(
         NewTextThread,
         /// Toggles the context picker interface for adding files, symbols, or other context.
         ToggleContextPicker,
+        /// Toggles the menu to create new agent threads.
+        ToggleNewThreadMenu,
         /// Toggles the navigation menu for switching between threads and views.
         ToggleNavigationMenu,
         /// Toggles the options menu for agent settings and preferences.
@@ -127,6 +128,12 @@ actions!(
     ]
 );
 
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Action)]
+#[action(namespace = agent)]
+#[action(deprecated_aliases = ["assistant::QuoteSelection"])]
+/// Quotes the current selection in the agent panel's message editor.
+pub struct QuoteSelection;
+
 /// Creates a new conversation thread, optionally based on an existing thread.
 #[derive(Default, Clone, PartialEq, Deserialize, JsonSchema, Action)]
 #[action(namespace = agent)]
@@ -145,7 +152,14 @@ pub struct NewExternalAgentThread {
     agent: Option<ExternalAgent>,
 }
 
-#[derive(Default, Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)]
+#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)]
+#[action(namespace = agent)]
+#[serde(deny_unknown_fields)]
+pub struct NewNativeAgentThreadFromSummary {
+    from_session_id: agent_client_protocol::SessionId,
+}
+
+#[derive(Default, Debug, Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)]
 #[serde(rename_all = "snake_case")]
 enum ExternalAgent {
     #[default]
@@ -155,11 +169,15 @@ enum ExternalAgent {
 }
 
 impl ExternalAgent {
-    pub fn server(&self) -> Rc<dyn agent_servers::AgentServer> {
+    pub fn server(
+        &self,
+        fs: Arc<dyn fs::Fs>,
+        history: Entity<agent2::HistoryStore>,
+    ) -> Rc<dyn agent_servers::AgentServer> {
         match self {
             ExternalAgent::Gemini => Rc::new(agent_servers::Gemini),
             ExternalAgent::ClaudeCode => Rc::new(agent_servers::ClaudeCode),
-            ExternalAgent::NativeAgent => Rc::new(agent2::NativeAgentServer),
+            ExternalAgent::NativeAgent => Rc::new(agent2::NativeAgentServer::new(fs, history)),
         }
     }
 }
@@ -235,13 +253,7 @@ pub fn init(
         client.telemetry().clone(),
         cx,
     );
-    terminal_inline_assistant::init(
-        fs.clone(),
-        prompt_builder.clone(),
-        client.telemetry().clone(),
-        cx,
-    );
-    indexed_docs::init(cx);
+    terminal_inline_assistant::init(fs.clone(), prompt_builder, client.telemetry().clone(), cx);
     cx.observe_new(move |workspace, window, cx| {
         ConfigureContextServerModal::register(workspace, language_registry.clone(), window, cx)
     })
@@ -320,7 +332,7 @@ fn init_language_model_settings(cx: &mut App) {
     cx.subscribe(
         &LanguageModelRegistry::global(cx),
         |_, event: &language_model::Event, cx| match event {
-            language_model::Event::ProviderStateChanged
+            language_model::Event::ProviderStateChanged(_)
             | language_model::Event::AddedProvider(_)
             | language_model::Event::RemovedProvider(_) => {
                 update_active_language_model_from_settings(cx);
@@ -387,7 +399,6 @@ fn register_slash_commands(cx: &mut App) {
     slash_command_registry.register_command(assistant_slash_commands::FetchSlashCommand, true);
 
     cx.observe_flag::<assistant_slash_commands::StreamingExampleSlashCommandFeatureFlag, _>({
-        let slash_command_registry = slash_command_registry.clone();
         move |is_enabled, _cx| {
             if is_enabled {
                 slash_command_registry.register_command(
@@ -408,12 +419,6 @@ fn update_slash_commands_from_settings(cx: &mut App) {
     let slash_command_registry = SlashCommandRegistry::global(cx);
     let settings = SlashCommandSettings::get_global(cx);
 
-    if settings.docs.enabled {
-        slash_command_registry.register_command(assistant_slash_commands::DocsSlashCommand, true);
-    } else {
-        slash_command_registry.unregister_command(assistant_slash_commands::DocsSlashCommand);
-    }
-
     if settings.cargo_workspace.enabled {
         slash_command_registry
             .register_command(assistant_slash_commands::CargoWorkspaceSlashCommand, true);

crates/agent_ui/src/buffer_codegen.rs 🔗

@@ -352,12 +352,12 @@ impl CodegenAlternative {
         event: &multi_buffer::Event,
         cx: &mut Context<Self>,
     ) {
-        if let multi_buffer::Event::TransactionUndone { transaction_id } = event {
-            if self.transformation_transaction_id == Some(*transaction_id) {
-                self.transformation_transaction_id = None;
-                self.generation = Task::ready(());
-                cx.emit(CodegenEvent::Undone);
-            }
+        if let multi_buffer::Event::TransactionUndone { transaction_id } = event
+            && self.transformation_transaction_id == Some(*transaction_id)
+        {
+            self.transformation_transaction_id = None;
+            self.generation = Task::ready(());
+            cx.emit(CodegenEvent::Undone);
         }
     }
 
@@ -388,7 +388,7 @@ impl CodegenAlternative {
             } else {
                 let request = self.build_request(&model, user_prompt, cx)?;
                 cx.spawn(async move |_, cx| {
-                    Ok(model.stream_completion_text(request.await, &cx).await?)
+                    Ok(model.stream_completion_text(request.await, cx).await?)
                 })
                 .boxed_local()
             };
@@ -447,7 +447,7 @@ impl CodegenAlternative {
             }
         });
 
-        let temperature = AgentSettings::temperature_for_model(&model, cx);
+        let temperature = AgentSettings::temperature_for_model(model, cx);
 
         Ok(cx.spawn(async move |_cx| {
             let mut request_message = LanguageModelRequestMessage {
@@ -576,38 +576,34 @@ impl CodegenAlternative {
                                 let mut lines = chunk.split('\n').peekable();
                                 while let Some(line) = lines.next() {
                                     new_text.push_str(line);
-                                    if line_indent.is_none() {
-                                        if let Some(non_whitespace_ch_ix) =
+                                    if line_indent.is_none()
+                                        && let Some(non_whitespace_ch_ix) =
                                             new_text.find(|ch: char| !ch.is_whitespace())
-                                        {
-                                            line_indent = Some(non_whitespace_ch_ix);
-                                            base_indent = base_indent.or(line_indent);
-
-                                            let line_indent = line_indent.unwrap();
-                                            let base_indent = base_indent.unwrap();
-                                            let indent_delta =
-                                                line_indent as i32 - base_indent as i32;
-                                            let mut corrected_indent_len = cmp::max(
-                                                0,
-                                                suggested_line_indent.len as i32 + indent_delta,
-                                            )
-                                                as usize;
-                                            if first_line {
-                                                corrected_indent_len = corrected_indent_len
-                                                    .saturating_sub(
-                                                        selection_start.column as usize,
-                                                    );
-                                            }
-
-                                            let indent_char = suggested_line_indent.char();
-                                            let mut indent_buffer = [0; 4];
-                                            let indent_str =
-                                                indent_char.encode_utf8(&mut indent_buffer);
-                                            new_text.replace_range(
-                                                ..line_indent,
-                                                &indent_str.repeat(corrected_indent_len),
-                                            );
+                                    {
+                                        line_indent = Some(non_whitespace_ch_ix);
+                                        base_indent = base_indent.or(line_indent);
+
+                                        let line_indent = line_indent.unwrap();
+                                        let base_indent = base_indent.unwrap();
+                                        let indent_delta = line_indent as i32 - base_indent as i32;
+                                        let mut corrected_indent_len = cmp::max(
+                                            0,
+                                            suggested_line_indent.len as i32 + indent_delta,
+                                        )
+                                            as usize;
+                                        if first_line {
+                                            corrected_indent_len = corrected_indent_len
+                                                .saturating_sub(selection_start.column as usize);
                                         }
+
+                                        let indent_char = suggested_line_indent.char();
+                                        let mut indent_buffer = [0; 4];
+                                        let indent_str =
+                                            indent_char.encode_utf8(&mut indent_buffer);
+                                        new_text.replace_range(
+                                            ..line_indent,
+                                            &indent_str.repeat(corrected_indent_len),
+                                        );
                                     }
 
                                     if line_indent.is_some() {
@@ -1028,7 +1024,7 @@ where
                                 chunk.push('\n');
                             }
 
-                            chunk.push_str(&line);
+                            chunk.push_str(line);
                         }
 
                         consumed += line.len();
@@ -1133,7 +1129,7 @@ mod tests {
             )
         });
 
-        let chunks_tx = simulate_response_stream(codegen.clone(), cx);
+        let chunks_tx = simulate_response_stream(&codegen, cx);
 
         let mut new_text = concat!(
             "       let mut x = 0;\n",
@@ -1200,7 +1196,7 @@ mod tests {
             )
         });
 
-        let chunks_tx = simulate_response_stream(codegen.clone(), cx);
+        let chunks_tx = simulate_response_stream(&codegen, cx);
 
         cx.background_executor.run_until_parked();
 
@@ -1269,7 +1265,7 @@ mod tests {
             )
         });
 
-        let chunks_tx = simulate_response_stream(codegen.clone(), cx);
+        let chunks_tx = simulate_response_stream(&codegen, cx);
 
         cx.background_executor.run_until_parked();
 
@@ -1338,7 +1334,7 @@ mod tests {
             )
         });
 
-        let chunks_tx = simulate_response_stream(codegen.clone(), cx);
+        let chunks_tx = simulate_response_stream(&codegen, cx);
         let new_text = concat!(
             "func main() {\n",
             "\tx := 0\n",
@@ -1395,7 +1391,7 @@ mod tests {
             )
         });
 
-        let chunks_tx = simulate_response_stream(codegen.clone(), cx);
+        let chunks_tx = simulate_response_stream(&codegen, cx);
         chunks_tx
             .unbounded_send("let mut x = 0;\nx += 1;".to_string())
             .unwrap();
@@ -1477,7 +1473,7 @@ mod tests {
     }
 
     fn simulate_response_stream(
-        codegen: Entity<CodegenAlternative>,
+        codegen: &Entity<CodegenAlternative>,
         cx: &mut TestAppContext,
     ) -> mpsc::UnboundedSender<String> {
         let (chunks_tx, chunks_rx) = mpsc::unbounded();

crates/agent_ui/src/burn_mode_tooltip.rs 🔗

@@ -1,61 +0,0 @@
-use gpui::{Context, FontWeight, IntoElement, Render, Window};
-use ui::{prelude::*, tooltip_container};
-
-pub struct BurnModeTooltip {
-    selected: bool,
-}
-
-impl BurnModeTooltip {
-    pub fn new() -> Self {
-        Self { selected: false }
-    }
-
-    pub fn selected(mut self, selected: bool) -> Self {
-        self.selected = selected;
-        self
-    }
-}
-
-impl Render for BurnModeTooltip {
-    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
-        let (icon, color) = if self.selected {
-            (IconName::ZedBurnModeOn, Color::Error)
-        } else {
-            (IconName::ZedBurnMode, Color::Default)
-        };
-
-        let turned_on = h_flex()
-            .h_4()
-            .px_1()
-            .border_1()
-            .border_color(cx.theme().colors().border)
-            .bg(cx.theme().colors().text_accent.opacity(0.1))
-            .rounded_sm()
-            .child(
-                Label::new("ON")
-                    .size(LabelSize::XSmall)
-                    .weight(FontWeight::SEMIBOLD)
-                    .color(Color::Accent),
-            );
-
-        let title = h_flex()
-            .gap_1p5()
-            .child(Icon::new(icon).size(IconSize::Small).color(color))
-            .child(Label::new("Burn Mode"))
-            .when(self.selected, |title| title.child(turned_on));
-
-        tooltip_container(window, cx, |this, _, _| {
-            this
-                .child(title)
-                .child(
-                    div()
-                        .max_w_64()
-                        .child(
-                            Label::new("Enables models to use large context windows, unlimited tool calls, and other capabilities for expanded reasoning.")
-                                .size(LabelSize::Small)
-                                .color(Color::Muted)
-                        )
-                )
-        })
-    }
-}

crates/agent_ui/src/context_picker.rs 🔗

@@ -1,18 +1,19 @@
 mod completion_provider;
-mod fetch_context_picker;
+pub(crate) mod fetch_context_picker;
 pub(crate) mod file_context_picker;
-mod rules_context_picker;
-mod symbol_context_picker;
-mod thread_context_picker;
+pub(crate) mod rules_context_picker;
+pub(crate) mod symbol_context_picker;
+pub(crate) mod thread_context_picker;
 
 use std::ops::Range;
 use std::path::{Path, PathBuf};
 use std::sync::Arc;
 
 use anyhow::{Result, anyhow};
+use collections::HashSet;
 pub use completion_provider::ContextPickerCompletionProvider;
 use editor::display_map::{Crease, CreaseId, CreaseMetadata, FoldId};
-use editor::{Anchor, AnchorRangeExt as _, Editor, ExcerptId, FoldPlaceholder, ToOffset};
+use editor::{Anchor, Editor, ExcerptId, FoldPlaceholder, ToOffset};
 use fetch_context_picker::FetchContextPicker;
 use file_context_picker::FileContextPicker;
 use file_context_picker::render_file_context_entry;
@@ -45,7 +46,7 @@ use agent::{
 };
 
 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
-enum ContextPickerEntry {
+pub(crate) enum ContextPickerEntry {
     Mode(ContextPickerMode),
     Action(ContextPickerAction),
 }
@@ -74,7 +75,7 @@ impl ContextPickerEntry {
 }
 
 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
-enum ContextPickerMode {
+pub(crate) enum ContextPickerMode {
     File,
     Symbol,
     Fetch,
@@ -83,7 +84,7 @@ enum ContextPickerMode {
 }
 
 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
-enum ContextPickerAction {
+pub(crate) enum ContextPickerAction {
     AddSelections,
 }
 
@@ -102,7 +103,7 @@ impl ContextPickerAction {
 
     pub fn icon(&self) -> IconName {
         match self {
-            Self::AddSelections => IconName::Context,
+            Self::AddSelections => IconName::Reader,
         }
     }
 }
@@ -147,7 +148,7 @@ impl ContextPickerMode {
         match self {
             Self::File => IconName::File,
             Self::Symbol => IconName::Code,
-            Self::Fetch => IconName::Globe,
+            Self::Fetch => IconName::ToolWeb,
             Self::Thread => IconName::Thread,
             Self::Rules => RULES_ICON,
         }
@@ -227,7 +228,7 @@ impl ContextPicker {
     }
 
     fn build_menu(&mut self, window: &mut Window, cx: &mut Context<Self>) -> Entity<ContextMenu> {
-        let context_picker = cx.entity().clone();
+        let context_picker = cx.entity();
 
         let menu = ContextMenu::build(window, cx, move |menu, _window, cx| {
             let recent = self.recent_entries(cx);
@@ -384,12 +385,11 @@ impl ContextPicker {
     }
 
     pub fn select_first(&mut self, window: &mut Window, cx: &mut Context<Self>) {
-        match &self.mode {
-            ContextPickerState::Default(entity) => entity.update(cx, |entity, cx| {
+        // Other variants already select their first entry on open automatically
+        if let ContextPickerState::Default(entity) = &self.mode {
+            entity.update(cx, |entity, cx| {
                 entity.select_first(&Default::default(), window, cx)
-            }),
-            // Other variants already select their first entry on open automatically
-            _ => {}
+            })
         }
     }
 
@@ -531,7 +531,7 @@ impl ContextPicker {
             return vec![];
         };
 
-        recent_context_picker_entries(
+        recent_context_picker_entries_with_store(
             context_store,
             self.thread_store.clone(),
             self.text_thread_store.clone(),
@@ -585,7 +585,8 @@ impl Render for ContextPicker {
             })
     }
 }
-enum RecentEntry {
+
+pub(crate) enum RecentEntry {
     File {
         project_path: ProjectPath,
         path_prefix: Arc<str>,
@@ -593,7 +594,7 @@ enum RecentEntry {
     Thread(ThreadContextEntry),
 }
 
-fn available_context_picker_entries(
+pub(crate) fn available_context_picker_entries(
     prompt_store: &Option<Entity<PromptStore>>,
     thread_store: &Option<WeakEntity<ThreadStore>>,
     workspace: &Entity<Workspace>,
@@ -608,9 +609,7 @@ fn available_context_picker_entries(
         .read(cx)
         .active_item(cx)
         .and_then(|item| item.downcast::<Editor>())
-        .map_or(false, |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(cx)));
     if has_selection {
         entries.push(ContextPickerEntry::Action(
             ContextPickerAction::AddSelections,
@@ -630,24 +629,56 @@ fn available_context_picker_entries(
     entries
 }
 
-fn recent_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>>,
     workspace: Entity<Workspace>,
     exclude_path: Option<ProjectPath>,
     cx: &App,
+) -> Vec<RecentEntry> {
+    let project = workspace.read(cx).project();
+
+    let mut exclude_paths = context_store.read(cx).file_paths(cx);
+    exclude_paths.extend(exclude_path);
+
+    let exclude_paths = exclude_paths
+        .into_iter()
+        .filter_map(|project_path| project.read(cx).absolute_path(&project_path, cx))
+        .collect();
+
+    let exclude_threads = context_store.read(cx).thread_ids();
+
+    recent_context_picker_entries(
+        thread_store,
+        text_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>>,
+    workspace: Entity<Workspace>,
+    exclude_paths: &HashSet<PathBuf>,
+    exclude_threads: &HashSet<ThreadId>,
+    cx: &App,
 ) -> Vec<RecentEntry> {
     let mut recent = Vec::with_capacity(6);
-    let mut current_files = context_store.read(cx).file_paths(cx);
-    current_files.extend(exclude_path);
     let workspace = workspace.read(cx);
     let project = workspace.project().read(cx);
 
     recent.extend(
         workspace
             .recent_navigation_history_iter(cx)
-            .filter(|(path, _)| !current_files.contains(path))
+            .filter(|(_, abs_path)| {
+                abs_path
+                    .as_ref()
+                    .is_none_or(|path| !exclude_paths.contains(path.as_path()))
+            })
             .take(4)
             .filter_map(|(project_path, _)| {
                 project
@@ -659,8 +690,6 @@ fn recent_context_picker_entries(
             }),
     );
 
-    let current_threads = context_store.read(cx).thread_ids();
-
     let active_thread_id = workspace
         .panel::<AgentPanel>(cx)
         .and_then(|panel| Some(panel.read(cx).active_thread(cx)?.read(cx).id()));
@@ -672,7 +701,7 @@ fn recent_context_picker_entries(
         let mut threads = unordered_thread_entries(thread_store, text_thread_store, cx)
             .filter(|(_, thread)| match thread {
                 ThreadContextEntry::Thread { id, .. } => {
-                    Some(id) != active_thread_id && !current_threads.contains(id)
+                    Some(id) != active_thread_id && !exclude_threads.contains(id)
                 }
                 ThreadContextEntry::Context { .. } => true,
             })
@@ -710,7 +739,7 @@ fn add_selections_as_context(
     })
 }
 
-fn selection_ranges(
+pub(crate) fn selection_ranges(
     workspace: &Entity<Workspace>,
     cx: &mut App,
 ) -> Vec<(Entity<Buffer>, Range<text::Anchor>)> {
@@ -789,13 +818,8 @@ pub fn crease_for_mention(
 
     let render_trailer = move |_row, _unfold, _window: &mut Window, _cx: &mut App| Empty.into_any();
 
-    Crease::inline(
-        range,
-        placeholder.clone(),
-        fold_toggle("mention"),
-        render_trailer,
-    )
-    .with_metadata(CreaseMetadata { icon_path, label })
+    Crease::inline(range, placeholder, fold_toggle("mention"), render_trailer)
+        .with_metadata(CreaseMetadata { icon_path, label })
 }
 
 fn render_fold_icon_button(
@@ -805,42 +829,9 @@ fn render_fold_icon_button(
 ) -> Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut App) -> AnyElement> {
     Arc::new({
         move |fold_id, fold_range, cx| {
-            let is_in_text_selection = editor.upgrade().is_some_and(|editor| {
-                editor.update(cx, |editor, cx| {
-                    let snapshot = editor
-                        .buffer()
-                        .update(cx, |multi_buffer, cx| multi_buffer.snapshot(cx));
-
-                    let is_in_pending_selection = || {
-                        editor
-                            .selections
-                            .pending
-                            .as_ref()
-                            .is_some_and(|pending_selection| {
-                                pending_selection
-                                    .selection
-                                    .range()
-                                    .includes(&fold_range, &snapshot)
-                            })
-                    };
-
-                    let mut is_in_complete_selection = || {
-                        editor
-                            .selections
-                            .disjoint_in_range::<usize>(fold_range.clone(), cx)
-                            .into_iter()
-                            .any(|selection| {
-                                // This is needed to cover a corner case, if we just check for an existing
-                                // selection in the fold range, having a cursor at the start of the fold
-                                // marks it as selected. Non-empty selections don't cause this.
-                                let length = selection.end - selection.start;
-                                length > 0
-                            })
-                    };
-
-                    is_in_pending_selection() || is_in_complete_selection()
-                })
-            });
+            let is_in_text_selection = editor
+                .update(cx, |editor, cx| editor.is_range_selected(&fold_range, cx))
+                .unwrap_or_default();
 
             ButtonLike::new(fold_id)
                 .style(ButtonStyle::Filled)

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

@@ -35,7 +35,7 @@ use super::symbol_context_picker::search_symbols;
 use super::thread_context_picker::{ThreadContextEntry, ThreadMatch, search_threads};
 use super::{
     ContextPickerAction, ContextPickerEntry, ContextPickerMode, MentionLink, RecentEntry,
-    available_context_picker_entries, recent_context_picker_entries, selection_ranges,
+    available_context_picker_entries, recent_context_picker_entries_with_store, selection_ranges,
 };
 use crate::message_editor::ContextCreasesAddon;
 
@@ -79,8 +79,7 @@ fn search(
 ) -> Task<Vec<Match>> {
     match mode {
         Some(ContextPickerMode::File) => {
-            let search_files_task =
-                search_files(query.clone(), cancellation_flag.clone(), &workspace, cx);
+            let search_files_task = search_files(query, cancellation_flag, &workspace, cx);
             cx.background_spawn(async move {
                 search_files_task
                     .await
@@ -91,8 +90,7 @@ fn search(
         }
 
         Some(ContextPickerMode::Symbol) => {
-            let search_symbols_task =
-                search_symbols(query.clone(), cancellation_flag.clone(), &workspace, cx);
+            let search_symbols_task = search_symbols(query, cancellation_flag, &workspace, cx);
             cx.background_spawn(async move {
                 search_symbols_task
                     .await
@@ -108,13 +106,8 @@ fn search(
                 .and_then(|t| t.upgrade())
                 .zip(text_thread_context_store.as_ref().and_then(|t| t.upgrade()))
             {
-                let search_threads_task = search_threads(
-                    query.clone(),
-                    cancellation_flag.clone(),
-                    thread_store,
-                    context_store,
-                    cx,
-                );
+                let search_threads_task =
+                    search_threads(query, cancellation_flag, thread_store, context_store, cx);
                 cx.background_spawn(async move {
                     search_threads_task
                         .await
@@ -137,8 +130,7 @@ fn search(
 
         Some(ContextPickerMode::Rules) => {
             if let Some(prompt_store) = prompt_store.as_ref() {
-                let search_rules_task =
-                    search_rules(query.clone(), cancellation_flag.clone(), prompt_store, cx);
+                let search_rules_task = search_rules(query, cancellation_flag, prompt_store, cx);
                 cx.background_spawn(async move {
                     search_rules_task
                         .await
@@ -196,7 +188,7 @@ fn search(
                 let executor = cx.background_executor().clone();
 
                 let search_files_task =
-                    search_files(query.clone(), cancellation_flag.clone(), &workspace, cx);
+                    search_files(query.clone(), cancellation_flag, &workspace, cx);
 
                 let entries =
                     available_context_picker_entries(&prompt_store, &thread_store, &workspace, cx);
@@ -283,7 +275,7 @@ impl ContextPickerCompletionProvider {
     ) -> Option<Completion> {
         match entry {
             ContextPickerEntry::Mode(mode) => Some(Completion {
-                replace_range: source_range.clone(),
+                replace_range: source_range,
                 new_text: format!("@{} ", mode.keyword()),
                 label: CodeLabel::plain(mode.label().to_string(), None),
                 icon_path: Some(mode.icon().path().into()),
@@ -330,9 +322,6 @@ impl ContextPickerCompletionProvider {
                         );
 
                         let callback = Arc::new({
-                            let context_store = context_store.clone();
-                            let selections = selections.clone();
-                            let selection_infos = selection_infos.clone();
                             move |_, window: &mut Window, cx: &mut App| {
                                 context_store.update(cx, |context_store, cx| {
                                     for (buffer, range) in &selections {
@@ -371,7 +360,7 @@ impl ContextPickerCompletionProvider {
                                                 line_range.end.row + 1
                                             )
                                             .into(),
-                                            IconName::Context.path().into(),
+                                            IconName::Reader.path().into(),
                                             range,
                                             editor.downgrade(),
                                         );
@@ -441,7 +430,7 @@ impl ContextPickerCompletionProvider {
                 excerpt_id,
                 source_range.start,
                 new_text_len - 1,
-                editor.clone(),
+                editor,
                 context_store.clone(),
                 move |window, cx| match &thread_entry {
                     ThreadContextEntry::Thread { id, .. } => {
@@ -510,7 +499,7 @@ impl ContextPickerCompletionProvider {
                 excerpt_id,
                 source_range.start,
                 new_text_len - 1,
-                editor.clone(),
+                editor,
                 context_store.clone(),
                 move |_, cx| {
                     let user_prompt_id = rules.prompt_id;
@@ -539,15 +528,15 @@ impl ContextPickerCompletionProvider {
             label: CodeLabel::plain(url_to_fetch.to_string(), None),
             documentation: None,
             source: project::CompletionSource::Custom,
-            icon_path: Some(IconName::Globe.path().into()),
+            icon_path: Some(IconName::ToolWeb.path().into()),
             insert_text_mode: None,
             confirm: Some(confirm_completion_callback(
-                IconName::Globe.path().into(),
+                IconName::ToolWeb.path().into(),
                 url_to_fetch.clone(),
                 excerpt_id,
                 source_range.start,
                 new_text_len - 1,
-                editor.clone(),
+                editor,
                 context_store.clone(),
                 move |_, cx| {
                     let context_store = context_store.clone();
@@ -704,16 +693,16 @@ impl ContextPickerCompletionProvider {
                 excerpt_id,
                 source_range.start,
                 new_text_len - 1,
-                editor.clone(),
+                editor,
                 context_store.clone(),
                 move |_, cx| {
                     let symbol = symbol.clone();
                     let context_store = context_store.clone();
                     let workspace = workspace.clone();
                     let result = super::symbol_context_picker::add_symbol(
-                        symbol.clone(),
+                        symbol,
                         false,
-                        workspace.clone(),
+                        workspace,
                         context_store.downgrade(),
                         cx,
                     );
@@ -728,11 +717,11 @@ fn build_code_label_for_full_path(file_name: &str, directory: Option<&str>, cx:
     let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId);
     let mut label = CodeLabel::default();
 
-    label.push_str(&file_name, None);
+    label.push_str(file_name, None);
     label.push_str(" ", None);
 
     if let Some(directory) = directory {
-        label.push_str(&directory, comment_id);
+        label.push_str(directory, comment_id);
     }
 
     label.filter_range = 0..label.text().len();
@@ -787,7 +776,7 @@ impl CompletionProvider for ContextPickerCompletionProvider {
             .and_then(|b| b.read(cx).file())
             .map(|file| ProjectPath::from_file(file.as_ref(), cx));
 
-        let recent_entries = recent_context_picker_entries(
+        let recent_entries = recent_context_picker_entries_with_store(
             context_store.clone(),
             thread_store.clone(),
             text_thread_store.clone(),
@@ -1020,7 +1009,7 @@ impl MentionCompletion {
             && line
                 .chars()
                 .nth(last_mention_start - 1)
-                .map_or(false, |c| !c.is_whitespace())
+                .is_some_and(|c| !c.is_whitespace())
         {
             return None;
         }
@@ -1162,7 +1151,7 @@ mod tests {
 
     impl Focusable for AtMentionEditor {
         fn focus_handle(&self, cx: &App) -> FocusHandle {
-            self.0.read(cx).focus_handle(cx).clone()
+            self.0.read(cx).focus_handle(cx)
         }
     }
 
@@ -1480,7 +1469,7 @@ mod tests {
         let completions = editor.current_completions().expect("Missing completions");
         completions
             .into_iter()
-            .map(|completion| completion.label.text.to_string())
+            .map(|completion| completion.label.text)
             .collect::<Vec<_>>()
     }
 

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

@@ -226,9 +226,10 @@ impl PickerDelegate for FetchContextPickerDelegate {
         _window: &mut Window,
         cx: &mut Context<Picker<Self>>,
     ) -> Option<Self::ListItem> {
-        let added = self.context_store.upgrade().map_or(false, |context_store| {
-            context_store.read(cx).includes_url(&self.url)
-        });
+        let added = self
+            .context_store
+            .upgrade()
+            .is_some_and(|context_store| context_store.read(cx).includes_url(&self.url));
 
         Some(
             ListItem::new(ix)

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

@@ -239,9 +239,7 @@ pub(crate) fn search_files(
 
                 PathMatchCandidateSet {
                     snapshot: worktree.snapshot(),
-                    include_ignored: worktree
-                        .root_entry()
-                        .map_or(false, |entry| entry.is_ignored),
+                    include_ignored: worktree.root_entry().is_some_and(|entry| entry.is_ignored),
                     include_root_name: true,
                     candidates: project::Candidates::Entries,
                 }
@@ -315,7 +313,7 @@ pub fn render_file_context_entry(
     context_store: WeakEntity<ContextStore>,
     cx: &App,
 ) -> Stateful<Div> {
-    let (file_name, directory) = extract_file_name_and_directory(&path, path_prefix);
+    let (file_name, directory) = extract_file_name_and_directory(path, path_prefix);
 
     let added = context_store.upgrade().and_then(|context_store| {
         let project_path = ProjectPath {
@@ -334,7 +332,7 @@ pub fn render_file_context_entry(
     let file_icon = if is_directory {
         FileIcons::get_folder_icon(false, cx)
     } else {
-        FileIcons::get_icon(&path, cx)
+        FileIcons::get_icon(path, cx)
     }
     .map(Icon::from_path)
     .unwrap_or_else(|| Icon::new(IconName::File));

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

@@ -159,7 +159,7 @@ pub fn render_thread_context_entry(
     context_store: WeakEntity<ContextStore>,
     cx: &mut App,
 ) -> Div {
-    let added = context_store.upgrade().map_or(false, |context_store| {
+    let added = context_store.upgrade().is_some_and(|context_store| {
         context_store
             .read(cx)
             .includes_user_rules(user_rules.prompt_id)

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

@@ -289,12 +289,12 @@ pub(crate) fn search_symbols(
                         .iter()
                         .enumerate()
                         .map(|(id, symbol)| {
-                            StringMatchCandidate::new(id, &symbol.label.filter_text())
+                            StringMatchCandidate::new(id, symbol.label.filter_text())
                         })
                         .partition(|candidate| {
                             project
                                 .entry_for_path(&symbols[candidate.id].path, cx)
-                                .map_or(false, |e| !e.is_ignored)
+                                .is_some_and(|e| !e.is_ignored)
                         })
                 })
                 .log_err()

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

@@ -167,7 +167,7 @@ impl PickerDelegate for ThreadContextPickerDelegate {
                     return;
                 };
                 let open_thread_task =
-                    thread_store.update(cx, |this, cx| this.open_thread(&id, window, cx));
+                    thread_store.update(cx, |this, cx| this.open_thread(id, window, cx));
 
                 cx.spawn(async move |this, cx| {
                     let thread = open_thread_task.await?;
@@ -236,12 +236,10 @@ pub fn render_thread_context_entry(
     let is_added = match entry {
         ThreadContextEntry::Thread { id, .. } => context_store
             .upgrade()
-            .map_or(false, |ctx_store| ctx_store.read(cx).includes_thread(&id)),
-        ThreadContextEntry::Context { path, .. } => {
-            context_store.upgrade().map_or(false, |ctx_store| {
-                ctx_store.read(cx).includes_text_thread(path)
-            })
-        }
+            .is_some_and(|ctx_store| ctx_store.read(cx).includes_thread(id)),
+        ThreadContextEntry::Context { path, .. } => context_store
+            .upgrade()
+            .is_some_and(|ctx_store| ctx_store.read(cx).includes_text_thread(path)),
     };
 
     h_flex()
@@ -338,7 +336,7 @@ pub(crate) fn search_threads(
             let candidates = threads
                 .iter()
                 .enumerate()
-                .map(|(id, (_, thread))| StringMatchCandidate::new(id, &thread.title()))
+                .map(|(id, (_, thread))| StringMatchCandidate::new(id, thread.title()))
                 .collect::<Vec<_>>();
             let matches = fuzzy::match_strings(
                 &candidates,

crates/agent_ui/src/context_strip.rs 🔗

@@ -145,7 +145,7 @@ impl ContextStrip {
         }
 
         let file_name = active_buffer.file()?.file_name(cx);
-        let icon_path = FileIcons::get_icon(&Path::new(&file_name), cx);
+        let icon_path = FileIcons::get_icon(Path::new(&file_name), cx);
         Some(SuggestedContext::File {
             name: file_name.to_string_lossy().into_owned().into(),
             buffer: active_buffer_entity.downgrade(),
@@ -368,16 +368,16 @@ impl ContextStrip {
         _window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        if let Some(suggested) = self.suggested_context(cx) {
-            if self.is_suggested_focused(&self.added_contexts(cx)) {
-                self.add_suggested_context(&suggested, cx);
-            }
+        if let Some(suggested) = self.suggested_context(cx)
+            && self.is_suggested_focused(&self.added_contexts(cx))
+        {
+            self.add_suggested_context(&suggested, cx);
         }
     }
 
     fn add_suggested_context(&mut self, suggested: &SuggestedContext, cx: &mut Context<Self>) {
         self.context_store.update(cx, |context_store, cx| {
-            context_store.add_suggested_context(&suggested, cx)
+            context_store.add_suggested_context(suggested, cx)
         });
         cx.notify();
     }

crates/agent_ui/src/inline_assistant.rs 🔗

@@ -72,7 +72,7 @@ pub fn init(
         let Some(window) = window else {
             return;
         };
-        let workspace = cx.entity().clone();
+        let workspace = cx.entity();
         InlineAssistant::update_global(cx, |inline_assistant, cx| {
             inline_assistant.register_workspace(&workspace, window, cx)
         });
@@ -182,13 +182,13 @@ impl InlineAssistant {
         match event {
             workspace::Event::UserSavedItem { item, .. } => {
                 // When the user manually saves an editor, automatically accepts all finished transformations.
-                if let Some(editor) = item.upgrade().and_then(|item| item.act_as::<Editor>(cx)) {
-                    if let Some(editor_assists) = self.assists_by_editor.get(&editor.downgrade()) {
-                        for assist_id in editor_assists.assist_ids.clone() {
-                            let assist = &self.assists[&assist_id];
-                            if let CodegenStatus::Done = assist.codegen.read(cx).status(cx) {
-                                self.finish_assist(assist_id, false, window, cx)
-                            }
+                if let Some(editor) = item.upgrade().and_then(|item| item.act_as::<Editor>(cx))
+                    && let Some(editor_assists) = self.assists_by_editor.get(&editor.downgrade())
+                {
+                    for assist_id in editor_assists.assist_ids.clone() {
+                        let assist = &self.assists[&assist_id];
+                        if let CodegenStatus::Done = assist.codegen.read(cx).status(cx) {
+                            self.finish_assist(assist_id, false, window, cx)
                         }
                     }
                 }
@@ -342,13 +342,11 @@ impl InlineAssistant {
                         )
                         .await
                         .ok();
-                    if let Some(answer) = answer {
-                        if answer == 0 {
-                            cx.update(|window, cx| {
-                                window.dispatch_action(Box::new(OpenSettings), cx)
-                            })
+                    if let Some(answer) = answer
+                        && answer == 0
+                    {
+                        cx.update(|window, cx| window.dispatch_action(Box::new(OpenSettings), cx))
                             .ok();
-                        }
                     }
                     anyhow::Ok(())
                 })
@@ -435,11 +433,11 @@ impl InlineAssistant {
                 }
             }
 
-            if let Some(prev_selection) = selections.last_mut() {
-                if selection.start <= prev_selection.end {
-                    prev_selection.end = selection.end;
-                    continue;
-                }
+            if let Some(prev_selection) = selections.last_mut()
+                && selection.start <= prev_selection.end
+            {
+                prev_selection.end = selection.end;
+                continue;
             }
 
             let latest_selection = newest_selection.get_or_insert_with(|| selection.clone());
@@ -526,9 +524,9 @@ impl InlineAssistant {
 
             if assist_to_focus.is_none() {
                 let focus_assist = if newest_selection.reversed {
-                    range.start.to_point(&snapshot) == newest_selection.start
+                    range.start.to_point(snapshot) == newest_selection.start
                 } else {
-                    range.end.to_point(&snapshot) == newest_selection.end
+                    range.end.to_point(snapshot) == newest_selection.end
                 };
                 if focus_assist {
                     assist_to_focus = Some(assist_id);
@@ -550,7 +548,7 @@ impl InlineAssistant {
         let editor_assists = self
             .assists_by_editor
             .entry(editor.downgrade())
-            .or_insert_with(|| EditorInlineAssists::new(&editor, window, cx));
+            .or_insert_with(|| EditorInlineAssists::new(editor, window, cx));
         let mut assist_group = InlineAssistGroup::new();
         for (assist_id, range, prompt_editor, prompt_block_id, end_block_id) in assists {
             let codegen = prompt_editor.read(cx).codegen().clone();
@@ -649,7 +647,7 @@ impl InlineAssistant {
         let editor_assists = self
             .assists_by_editor
             .entry(editor.downgrade())
-            .or_insert_with(|| EditorInlineAssists::new(&editor, window, cx));
+            .or_insert_with(|| EditorInlineAssists::new(editor, window, cx));
 
         let mut assist_group = InlineAssistGroup::new();
         self.assists.insert(
@@ -985,14 +983,13 @@ impl InlineAssistant {
             EditorEvent::SelectionsChanged { .. } => {
                 for assist_id in editor_assists.assist_ids.clone() {
                     let assist = &self.assists[&assist_id];
-                    if let Some(decorations) = assist.decorations.as_ref() {
-                        if decorations
+                    if let Some(decorations) = assist.decorations.as_ref()
+                        && decorations
                             .prompt_editor
                             .focus_handle(cx)
                             .is_focused(window)
-                        {
-                            return;
-                        }
+                    {
+                        return;
                     }
                 }
 
@@ -1123,7 +1120,7 @@ impl InlineAssistant {
             if editor_assists
                 .scroll_lock
                 .as_ref()
-                .map_or(false, |lock| lock.assist_id == assist_id)
+                .is_some_and(|lock| lock.assist_id == assist_id)
             {
                 editor_assists.scroll_lock = None;
             }
@@ -1503,20 +1500,18 @@ impl InlineAssistant {
         window: &mut Window,
         cx: &mut App,
     ) -> Option<InlineAssistTarget> {
-        if let Some(terminal_panel) = workspace.panel::<TerminalPanel>(cx) {
-            if terminal_panel
+        if let Some(terminal_panel) = workspace.panel::<TerminalPanel>(cx)
+            && terminal_panel
                 .read(cx)
                 .focus_handle(cx)
                 .contains_focused(window, cx)
-            {
-                if let Some(terminal_view) = terminal_panel.read(cx).pane().and_then(|pane| {
-                    pane.read(cx)
-                        .active_item()
-                        .and_then(|t| t.downcast::<TerminalView>())
-                }) {
-                    return Some(InlineAssistTarget::Terminal(terminal_view));
-                }
-            }
+            && let Some(terminal_view) = terminal_panel.read(cx).pane().and_then(|pane| {
+                pane.read(cx)
+                    .active_item()
+                    .and_then(|t| t.downcast::<TerminalView>())
+            })
+        {
+            return Some(InlineAssistTarget::Terminal(terminal_view));
         }
 
         let context_editor = agent_panel
@@ -1537,13 +1532,11 @@ impl InlineAssistant {
             .and_then(|item| item.act_as::<Editor>(cx))
         {
             Some(InlineAssistTarget::Editor(workspace_editor))
-        } else if let Some(terminal_view) = workspace
-            .active_item(cx)
-            .and_then(|item| item.act_as::<TerminalView>(cx))
-        {
-            Some(InlineAssistTarget::Terminal(terminal_view))
         } else {
-            None
+            workspace
+                .active_item(cx)
+                .and_then(|item| item.act_as::<TerminalView>(cx))
+                .map(InlineAssistTarget::Terminal)
         }
     }
 }
@@ -1698,7 +1691,7 @@ impl InlineAssist {
             }),
             range,
             codegen: codegen.clone(),
-            workspace: workspace.clone(),
+            workspace,
             _subscriptions: vec![
                 window.on_focus_in(&prompt_editor_focus_handle, cx, move |_, cx| {
                     InlineAssistant::update_global(cx, |this, cx| {
@@ -1741,22 +1734,20 @@ impl InlineAssist {
                                 return;
                             };
 
-                            if let CodegenStatus::Error(error) = codegen.read(cx).status(cx) {
-                                if assist.decorations.is_none() {
-                                    if let Some(workspace) = assist.workspace.upgrade() {
-                                        let error = format!("Inline assistant error: {}", error);
-                                        workspace.update(cx, |workspace, cx| {
-                                            struct InlineAssistantError;
-
-                                            let id =
-                                                NotificationId::composite::<InlineAssistantError>(
-                                                    assist_id.0,
-                                                );
-
-                                            workspace.show_toast(Toast::new(id, error), cx);
-                                        })
-                                    }
-                                }
+                            if let CodegenStatus::Error(error) = codegen.read(cx).status(cx)
+                                && assist.decorations.is_none()
+                                && let Some(workspace) = assist.workspace.upgrade()
+                            {
+                                let error = format!("Inline assistant error: {}", error);
+                                workspace.update(cx, |workspace, cx| {
+                                    struct InlineAssistantError;
+
+                                    let id = NotificationId::composite::<InlineAssistantError>(
+                                        assist_id.0,
+                                    );
+
+                                    workspace.show_toast(Toast::new(id, error), cx);
+                                })
                             }
 
                             if assist.decorations.is_none() {
@@ -1821,18 +1812,18 @@ impl CodeActionProvider for AssistantCodeActionProvider {
             has_diagnostics = true;
         }
         if has_diagnostics {
-            if let Some(symbols_containing_start) = snapshot.symbols_containing(range.start, None) {
-                if let Some(symbol) = symbols_containing_start.last() {
-                    range.start = cmp::min(range.start, symbol.range.start.to_point(&snapshot));
-                    range.end = cmp::max(range.end, symbol.range.end.to_point(&snapshot));
-                }
+            if let Some(symbols_containing_start) = snapshot.symbols_containing(range.start, None)
+                && let Some(symbol) = symbols_containing_start.last()
+            {
+                range.start = cmp::min(range.start, symbol.range.start.to_point(&snapshot));
+                range.end = cmp::max(range.end, symbol.range.end.to_point(&snapshot));
             }
 
-            if let Some(symbols_containing_end) = snapshot.symbols_containing(range.end, None) {
-                if let Some(symbol) = symbols_containing_end.last() {
-                    range.start = cmp::min(range.start, symbol.range.start.to_point(&snapshot));
-                    range.end = cmp::max(range.end, symbol.range.end.to_point(&snapshot));
-                }
+            if let Some(symbols_containing_end) = snapshot.symbols_containing(range.end, None)
+                && let Some(symbol) = symbols_containing_end.last()
+            {
+                range.start = cmp::min(range.start, symbol.range.start.to_point(&snapshot));
+                range.end = cmp::max(range.end, symbol.range.end.to_point(&snapshot));
             }
 
             Task::ready(Ok(vec![CodeAction {

crates/agent_ui/src/inline_prompt_editor.rs 🔗

@@ -75,7 +75,7 @@ impl<T: 'static> Render for PromptEditor<T> {
                 let codegen = codegen.read(cx);
 
                 if codegen.alternative_count(cx) > 1 {
-                    buttons.push(self.render_cycle_controls(&codegen, cx));
+                    buttons.push(self.render_cycle_controls(codegen, cx));
                 }
 
                 let editor_margins = editor_margins.lock();
@@ -345,7 +345,7 @@ impl<T: 'static> PromptEditor<T> {
                 let prompt = self.editor.read(cx).text(cx);
                 if self
                     .prompt_history_ix
-                    .map_or(true, |ix| self.prompt_history[ix] != prompt)
+                    .is_none_or(|ix| self.prompt_history[ix] != prompt)
                 {
                     self.prompt_history_ix.take();
                     self.pending_prompt = prompt;
@@ -541,7 +541,7 @@ impl<T: 'static> PromptEditor<T> {
                     match &self.mode {
                         PromptEditorMode::Terminal { .. } => vec![
                             accept,
-                            IconButton::new("confirm", IconName::PlayOutlined)
+                            IconButton::new("confirm", IconName::PlayFilled)
                                 .icon_color(Color::Info)
                                 .shape(IconButtonShape::Square)
                                 .tooltip(|window, cx| {
@@ -1229,27 +1229,27 @@ pub enum GenerationMode {
 impl GenerationMode {
     fn start_label(self) -> &'static str {
         match self {
-            GenerationMode::Generate { .. } => "Generate",
+            GenerationMode::Generate => "Generate",
             GenerationMode::Transform => "Transform",
         }
     }
     fn tooltip_interrupt(self) -> &'static str {
         match self {
-            GenerationMode::Generate { .. } => "Interrupt Generation",
+            GenerationMode::Generate => "Interrupt Generation",
             GenerationMode::Transform => "Interrupt Transform",
         }
     }
 
     fn tooltip_restart(self) -> &'static str {
         match self {
-            GenerationMode::Generate { .. } => "Restart Generation",
+            GenerationMode::Generate => "Restart Generation",
             GenerationMode::Transform => "Restart Transform",
         }
     }
 
     fn tooltip_accept(self) -> &'static str {
         match self {
-            GenerationMode::Generate { .. } => "Accept Generation",
+            GenerationMode::Generate => "Accept Generation",
             GenerationMode::Transform => "Accept Transform",
         }
     }

crates/agent_ui/src/language_model_selector.rs 🔗

@@ -1,5 +1,6 @@
 use std::{cmp::Reverse, sync::Arc};
 
+use cloud_llm_client::Plan;
 use collections::{HashSet, IndexMap};
 use feature_flags::ZedProFeatureFlag;
 use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
@@ -10,7 +11,6 @@ use language_model::{
 };
 use ordered_float::OrderedFloat;
 use picker::{Picker, PickerDelegate};
-use proto::Plan;
 use ui::{ListItem, ListItemSpacing, prelude::*};
 
 const TRY_ZED_PRO_URL: &str = "https://zed.dev/pro";
@@ -93,7 +93,7 @@ impl LanguageModelPickerDelegate {
         let entries = models.entries();
 
         Self {
-            on_model_changed: on_model_changed.clone(),
+            on_model_changed,
             all_models: Arc::new(models),
             selected_index: Self::get_active_model_index(&entries, get_active_model(cx)),
             filtered_entries: entries,
@@ -104,7 +104,7 @@ impl LanguageModelPickerDelegate {
                 window,
                 |picker, _, event, window, cx| {
                     match event {
-                        language_model::Event::ProviderStateChanged
+                        language_model::Event::ProviderStateChanged(_)
                         | language_model::Event::AddedProvider(_)
                         | language_model::Event::RemovedProvider(_) => {
                             let query = picker.query(cx);
@@ -296,7 +296,7 @@ impl ModelMatcher {
     pub fn fuzzy_search(&self, query: &str) -> Vec<ModelInfo> {
         let mut matches = self.bg_executor.block(match_strings(
             &self.candidates,
-            &query,
+            query,
             false,
             true,
             100,
@@ -514,7 +514,7 @@ impl PickerDelegate for LanguageModelPickerDelegate {
                                 .pl_0p5()
                                 .gap_1p5()
                                 .w(px(240.))
-                                .child(Label::new(model_info.model.name().0.clone()).truncate()),
+                                .child(Label::new(model_info.model.name().0).truncate()),
                         )
                         .end_slot(div().pr_3().when(is_selected, |this| {
                             this.child(
@@ -536,7 +536,7 @@ impl PickerDelegate for LanguageModelPickerDelegate {
     ) -> Option<gpui::AnyElement> {
         use feature_flags::FeatureFlagAppExt;
 
-        let plan = proto::Plan::ZedPro;
+        let plan = Plan::ZedPro;
 
         Some(
             h_flex()
@@ -557,7 +557,7 @@ impl PickerDelegate for LanguageModelPickerDelegate {
                                 window
                                     .dispatch_action(Box::new(zed_actions::OpenAccountSettings), cx)
                             }),
-                        Plan::Free | Plan::ZedProTrial => Button::new(
+                        Plan::ZedFree | Plan::ZedProTrial => Button::new(
                             "try-pro",
                             if plan == Plan::ZedProTrial {
                                 "Upgrade to Pro"

crates/agent_ui/src/message_editor.rs 🔗

@@ -6,7 +6,7 @@ use crate::agent_diff::AgentDiffThread;
 use crate::agent_model_selector::AgentModelSelector;
 use crate::tool_compatibility::{IncompatibleToolsState, IncompatibleToolsTooltip};
 use crate::ui::{
-    MaxModeTooltip,
+    BurnModeTooltip,
     preview::{AgentPreview, UsageCallout},
 };
 use agent::history_store::HistoryStore;
@@ -14,7 +14,7 @@ use agent::{
     context::{AgentContextKey, ContextLoadResult, load_context},
     context_store::ContextStoreEvent,
 };
-use agent_settings::{AgentSettings, CompletionMode};
+use agent_settings::{AgentProfileId, AgentSettings, CompletionMode};
 use ai_onboarding::ApiKeysWithProviders;
 use buffer_diff::BufferDiff;
 use cloud_llm_client::CompletionIntent;
@@ -55,7 +55,7 @@ use zed_actions::agent::ToggleModelSelector;
 
 use crate::context_picker::{ContextPicker, ContextPickerCompletionProvider, crease_for_mention};
 use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
-use crate::profile_selector::ProfileSelector;
+use crate::profile_selector::{ProfileProvider, ProfileSelector};
 use crate::{
     ActiveThread, AgentDiffPane, ChatWithFollow, ExpandMessageEditor, Follow, KeepAll,
     ModelUsageContext, NewThread, OpenAgentDiff, RejectAll, RemoveAllContext, ToggleBurnMode,
@@ -117,7 +117,7 @@ pub(crate) fn create_editor(
         let mut editor = Editor::new(
             editor::EditorMode::AutoHeight {
                 min_lines,
-                max_lines: max_lines,
+                max_lines,
             },
             buffer,
             None,
@@ -152,6 +152,24 @@ pub(crate) fn create_editor(
     editor
 }
 
+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()
+    }
+
+    fn set_profile(&self, profile_id: AgentProfileId, cx: &mut App) {
+        self.update(cx, |this, cx| {
+            this.set_profile(profile_id, cx);
+        });
+    }
+}
+
 impl MessageEditor {
     pub fn new(
         fs: Arc<dyn Fs>,
@@ -197,9 +215,10 @@ impl MessageEditor {
 
         let subscriptions = vec![
             cx.subscribe_in(&context_strip, window, Self::handle_context_strip_event),
-            cx.subscribe(&editor, |this, _, event, cx| match event {
-                EditorEvent::BufferEdited => this.handle_message_changed(cx),
-                _ => {}
+            cx.subscribe(&editor, |this, _, event: &EditorEvent, cx| {
+                if event == &EditorEvent::BufferEdited {
+                    this.handle_message_changed(cx)
+                }
             }),
             cx.observe(&context_store, |this, _, cx| {
                 // When context changes, reload it for token counting.
@@ -221,14 +240,15 @@ impl MessageEditor {
             )
         });
 
-        let profile_selector =
-            cx.new(|cx| ProfileSelector::new(fs, thread.clone(), editor.focus_handle(cx), cx));
+        let profile_selector = cx.new(|cx| {
+            ProfileSelector::new(fs, Arc::new(thread.clone()), editor.focus_handle(cx), cx)
+        });
 
         Self {
             editor: editor.clone(),
             project: thread.read(cx).project().clone(),
             thread,
-            incompatible_tools_state: incompatible_tools.clone(),
+            incompatible_tools_state: incompatible_tools,
             workspace,
             context_store,
             prompt_store,
@@ -422,11 +442,11 @@ impl MessageEditor {
             thread.cancel_editing(cx);
         });
 
-        let cancelled = self.thread.update(cx, |thread, cx| {
+        let canceled = self.thread.update(cx, |thread, cx| {
             thread.cancel_last_completion(Some(window.window_handle()), cx)
         });
 
-        if cancelled {
+        if canceled {
             self.set_editor_is_expanded(false, cx);
             self.send_to_model(window, cx);
         }
@@ -605,7 +625,7 @@ impl MessageEditor {
                     this.toggle_burn_mode(&ToggleBurnMode, window, cx);
                 }))
                 .tooltip(move |_window, cx| {
-                    cx.new(|_| MaxModeTooltip::new().selected(burn_mode_enabled))
+                    cx.new(|_| BurnModeTooltip::new().selected(burn_mode_enabled))
                         .into()
                 })
                 .into_any_element(),
@@ -671,11 +691,7 @@ impl MessageEditor {
             .as_ref()
             .map(|model| {
                 self.incompatible_tools_state.update(cx, |state, cx| {
-                    state
-                        .incompatible_tools(&model.model, cx)
-                        .iter()
-                        .cloned()
-                        .collect::<Vec<_>>()
+                    state.incompatible_tools(&model.model, cx).to_vec()
                 })
             })
             .unwrap_or_default();
@@ -725,7 +741,7 @@ impl MessageEditor {
                     .when(focus_handle.is_focused(window), |this| {
                         this.child(
                             IconButton::new("toggle-height", expand_icon)
-                                .icon_size(IconSize::XSmall)
+                                .icon_size(IconSize::Small)
                                 .icon_color(Color::Muted)
                                 .tooltip({
                                     let focus_handle = focus_handle.clone();
@@ -823,7 +839,6 @@ impl MessageEditor {
                                     .child(self.profile_selector.clone())
                                     .child(self.model_selector.clone())
                                     .map({
-                                        let focus_handle = focus_handle.clone();
                                         move |parent| {
                                             if is_generating {
                                                 parent
@@ -831,7 +846,7 @@ impl MessageEditor {
                                                         parent.child(
                                                             IconButton::new(
                                                                 "stop-generation",
-                                                                IconName::StopFilled,
+                                                                IconName::Stop,
                                                             )
                                                             .icon_color(Color::Error)
                                                             .style(ButtonStyle::Tinted(
@@ -1117,7 +1132,7 @@ impl MessageEditor {
             )
             .when(is_edit_changes_expanded, |parent| {
                 parent.child(
-                    v_flex().children(changed_buffers.into_iter().enumerate().flat_map(
+                    v_flex().children(changed_buffers.iter().enumerate().flat_map(
                         |(index, (buffer, _diff))| {
                             let file = buffer.read(cx).file()?;
                             let path = file.path();
@@ -1147,7 +1162,7 @@ impl MessageEditor {
                                     .buffer_font(cx)
                             });
 
-                            let file_icon = FileIcons::get_icon(&path, cx)
+                            let file_icon = FileIcons::get_icon(path, cx)
                                 .map(Icon::from_path)
                                 .map(|icon| icon.color(Color::Muted).size(IconSize::Small))
                                 .unwrap_or_else(|| {
@@ -1274,7 +1289,7 @@ impl MessageEditor {
         self.thread
             .read(cx)
             .configured_model()
-            .map_or(false, |model| model.provider.id() == ZED_CLOUD_PROVIDER_ID)
+            .is_some_and(|model| model.provider.id() == ZED_CLOUD_PROVIDER_ID)
     }
 
     fn render_usage_callout(&self, line_height: Pixels, cx: &mut Context<Self>) -> Option<Div> {
@@ -1304,14 +1319,10 @@ impl MessageEditor {
         token_usage_ratio: TokenUsageRatio,
         cx: &mut Context<Self>,
     ) -> Option<Div> {
-        let icon = if token_usage_ratio == TokenUsageRatio::Exceeded {
-            Icon::new(IconName::X)
-                .color(Color::Error)
-                .size(IconSize::XSmall)
+        let (icon, severity) = if token_usage_ratio == TokenUsageRatio::Exceeded {
+            (IconName::Close, Severity::Error)
         } else {
-            Icon::new(IconName::Warning)
-                .color(Color::Warning)
-                .size(IconSize::XSmall)
+            (IconName::Warning, Severity::Warning)
         };
 
         let title = if token_usage_ratio == TokenUsageRatio::Exceeded {
@@ -1326,29 +1337,33 @@ impl MessageEditor {
             "To continue, start a new thread from a summary."
         };
 
-        let mut callout = Callout::new()
+        let callout = Callout::new()
             .line_height(line_height)
+            .severity(severity)
             .icon(icon)
             .title(title)
             .description(description)
-            .primary_action(
-                Button::new("start-new-thread", "Start New Thread")
-                    .label_size(LabelSize::Small)
-                    .on_click(cx.listener(|this, _, window, cx| {
-                        let from_thread_id = Some(this.thread.read(cx).id().clone());
-                        window.dispatch_action(Box::new(NewThread { from_thread_id }), cx);
-                    })),
-            );
-
-        if self.is_using_zed_provider(cx) {
-            callout = callout.secondary_action(
-                IconButton::new("burn-mode-callout", IconName::ZedBurnMode)
-                    .icon_size(IconSize::XSmall)
-                    .on_click(cx.listener(|this, _event, window, cx| {
-                        this.toggle_burn_mode(&ToggleBurnMode, window, cx);
-                    })),
+            .actions_slot(
+                h_flex()
+                    .gap_0p5()
+                    .when(self.is_using_zed_provider(cx), |this| {
+                        this.child(
+                            IconButton::new("burn-mode-callout", IconName::ZedBurnMode)
+                                .icon_size(IconSize::XSmall)
+                                .on_click(cx.listener(|this, _event, window, cx| {
+                                    this.toggle_burn_mode(&ToggleBurnMode, window, cx);
+                                })),
+                        )
+                    })
+                    .child(
+                        Button::new("start-new-thread", "Start New Thread")
+                            .label_size(LabelSize::Small)
+                            .on_click(cx.listener(|this, _, window, cx| {
+                                let from_thread_id = Some(this.thread.read(cx).id().clone());
+                                window.dispatch_action(Box::new(NewThread { from_thread_id }), cx);
+                            })),
+                    ),
             );
-        }
 
         Some(
             div()
@@ -1385,7 +1400,7 @@ impl MessageEditor {
             })
             .ok();
         });
-        // Replace existing load task, if any, causing it to be cancelled.
+        // Replace existing load task, if any, causing it to be canceled.
         let load_task = load_task.shared();
         self.load_context_task = Some(load_task.clone());
         cx.spawn(async move |this, cx| {
@@ -1427,7 +1442,7 @@ impl MessageEditor {
                     let message_text = editor.read(cx).text(cx);
 
                     if message_text.is_empty()
-                        && loaded_context.map_or(true, |loaded_context| loaded_context.is_empty())
+                        && loaded_context.is_none_or(|loaded_context| loaded_context.is_empty())
                     {
                         return None;
                     }
@@ -1540,9 +1555,8 @@ impl ContextCreasesAddon {
         cx: &mut Context<Editor>,
     ) {
         self.creases.entry(key).or_default().extend(creases);
-        self._subscription = Some(cx.subscribe(
-            &context_store,
-            |editor, _, event, cx| match event {
+        self._subscription = Some(
+            cx.subscribe(context_store, |editor, _, event, cx| match event {
                 ContextStoreEvent::ContextRemoved(key) => {
                     let Some(this) = editor.addon_mut::<Self>() else {
                         return;
@@ -1562,8 +1576,8 @@ impl ContextCreasesAddon {
                     editor.edit(ranges.into_iter().zip(replacement_texts), cx);
                     cx.notify();
                 }
-            },
-        ))
+            }),
+        )
     }
 
     pub fn into_inner(self) -> HashMap<AgentContextKey, Vec<(CreaseId, SharedString)>> {
@@ -1591,7 +1605,8 @@ pub fn extract_message_creases(
         .collect::<HashMap<_, _>>();
     // Filter the addon's list of creases based on what the editor reports,
     // since the addon might have removed creases in it.
-    let creases = editor.display_map.update(cx, |display_map, cx| {
+
+    editor.display_map.update(cx, |display_map, cx| {
         display_map
             .snapshot(cx)
             .crease_snapshot
@@ -1615,8 +1630,7 @@ pub fn extract_message_creases(
                 }
             })
             .collect()
-    });
-    creases
+    })
 }
 
 impl EventEmitter<MessageEditorEvent> for MessageEditor {}
@@ -1668,7 +1682,7 @@ impl Render for MessageEditor {
         let has_history = self
             .history_store
             .as_ref()
-            .and_then(|hs| hs.update(cx, |hs, cx| hs.entries(cx).len() > 0).ok())
+            .and_then(|hs| hs.update(cx, |hs, cx| !hs.entries(cx).is_empty()).ok())
             .unwrap_or(false)
             || self
                 .thread
@@ -1681,7 +1695,7 @@ impl Render for MessageEditor {
                 !has_history && is_signed_out && has_configured_providers,
                 |this| this.child(cx.new(ApiKeysWithProviders::new)),
             )
-            .when(changed_buffers.len() > 0, |parent| {
+            .when(!changed_buffers.is_empty(), |parent| {
                 parent.child(self.render_edits_bar(&changed_buffers, window, cx))
             })
             .child(self.render_editor(window, cx))
@@ -1786,7 +1800,7 @@ impl AgentPreview for MessageEditor {
                             .bg(cx.theme().colors().panel_background)
                             .border_1()
                             .border_color(cx.theme().colors().border)
-                            .child(default_message_editor.clone())
+                            .child(default_message_editor)
                             .into_any_element(),
                     )])
                     .into_any_element(),

crates/agent_ui/src/profile_selector.rs 🔗

@@ -1,12 +1,8 @@
 use crate::{ManageProfiles, ToggleProfileSelector};
-use agent::{
-    Thread,
-    agent_profile::{AgentProfile, AvailableProfiles},
-};
+use agent::agent_profile::{AgentProfile, AvailableProfiles};
 use agent_settings::{AgentDockPosition, AgentProfileId, AgentSettings, builtin_profiles};
 use fs::Fs;
-use gpui::{Action, Empty, Entity, FocusHandle, Subscription, prelude::*};
-use language_model::LanguageModelRegistry;
+use gpui::{Action, Entity, FocusHandle, Subscription, prelude::*};
 use settings::{Settings as _, SettingsStore, update_settings_file};
 use std::sync::Arc;
 use ui::{
@@ -14,10 +10,22 @@ use ui::{
     prelude::*,
 };
 
+/// Trait for types that can provide and manage agent profiles
+pub trait ProfileProvider {
+    /// Get the current profile ID
+    fn profile_id(&self, cx: &App) -> AgentProfileId;
+
+    /// Set the profile ID
+    fn set_profile(&self, profile_id: AgentProfileId, cx: &mut App);
+
+    /// Check if profiles are supported in the current context (e.g. if the model that is selected has tool support)
+    fn profiles_supported(&self, cx: &App) -> bool;
+}
+
 pub struct ProfileSelector {
     profiles: AvailableProfiles,
     fs: Arc<dyn Fs>,
-    thread: Entity<Thread>,
+    provider: Arc<dyn ProfileProvider>,
     menu_handle: PopoverMenuHandle<ContextMenu>,
     focus_handle: FocusHandle,
     _subscriptions: Vec<Subscription>,
@@ -26,7 +34,7 @@ pub struct ProfileSelector {
 impl ProfileSelector {
     pub fn new(
         fs: Arc<dyn Fs>,
-        thread: Entity<Thread>,
+        provider: Arc<dyn ProfileProvider>,
         focus_handle: FocusHandle,
         cx: &mut Context<Self>,
     ) -> Self {
@@ -37,7 +45,7 @@ impl ProfileSelector {
         Self {
             profiles: AgentProfile::available_profiles(cx),
             fs,
-            thread,
+            provider,
             menu_handle: PopoverMenuHandle::default(),
             focus_handle,
             _subscriptions: vec![settings_subscription],
@@ -113,10 +121,10 @@ impl ProfileSelector {
             builtin_profiles::MINIMAL => Some("Chat about anything with no tools."),
             _ => None,
         };
-        let thread_profile_id = self.thread.read(cx).profile().id();
+        let thread_profile_id = self.provider.profile_id(cx);
 
         let entry = ContextMenuEntry::new(profile_name.clone())
-            .toggleable(IconPosition::End, &profile_id == thread_profile_id);
+            .toggleable(IconPosition::End, profile_id == thread_profile_id);
 
         let entry = if let Some(doc_text) = documentation {
             entry.documentation_aside(documentation_side(settings.dock), move |_| {
@@ -128,19 +136,16 @@ impl ProfileSelector {
 
         entry.handler({
             let fs = self.fs.clone();
-            let thread = self.thread.clone();
-            let profile_id = profile_id.clone();
+            let provider = self.provider.clone();
             move |_window, cx| {
                 update_settings_file::<AgentSettings>(fs.clone(), cx, {
                     let profile_id = profile_id.clone();
                     move |settings, _cx| {
-                        settings.set_profile(profile_id.clone());
+                        settings.set_profile(profile_id);
                     }
                 });
 
-                thread.update(cx, |this, cx| {
-                    this.set_profile(profile_id.clone(), cx);
-                });
+                provider.set_profile(profile_id.clone(), cx);
             }
         })
     }
@@ -149,23 +154,15 @@ impl ProfileSelector {
 impl Render for ProfileSelector {
     fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
         let settings = AgentSettings::get_global(cx);
-        let profile_id = self.thread.read(cx).profile().id();
-        let profile = settings.profiles.get(profile_id);
+        let profile_id = self.provider.profile_id(cx);
+        let profile = settings.profiles.get(&profile_id);
 
         let selected_profile = profile
             .map(|profile| profile.name.clone())
             .unwrap_or_else(|| "Unknown".into());
 
-        let configured_model = self.thread.read(cx).configured_model().or_else(|| {
-            let model_registry = LanguageModelRegistry::read_global(cx);
-            model_registry.default_model()
-        });
-        let Some(configured_model) = configured_model else {
-            return Empty.into_any_element();
-        };
-
-        if configured_model.model.supports_tools() {
-            let this = cx.entity().clone();
+        if self.provider.profiles_supported(cx) {
+            let this = cx.entity();
             let focus_handle = self.focus_handle.clone();
             let trigger_button = Button::new("profile-selector-model", selected_profile)
                 .label_size(LabelSize::Small)
@@ -177,7 +174,6 @@ impl Render for ProfileSelector {
 
             PopoverMenu::new("profile-selector")
                 .trigger_with_tooltip(trigger_button, {
-                    let focus_handle = focus_handle.clone();
                     move |window, cx| {
                         Tooltip::for_action_in(
                             "Toggle Profile Menu",

crates/agent_ui/src/slash_command.rs 🔗

@@ -88,8 +88,6 @@ impl SlashCommandCompletionProvider {
                                 .map(|(editor, workspace)| {
                                     let command_name = mat.string.clone();
                                     let command_range = command_range.clone();
-                                    let editor = editor.clone();
-                                    let workspace = workspace.clone();
                                     Arc::new(
                                             move |intent: CompletionIntent,
                                             window: &mut Window,
@@ -158,7 +156,7 @@ impl SlashCommandCompletionProvider {
         if let Some(command) = self.slash_commands.command(command_name, cx) {
             let completions = command.complete_argument(
                 arguments,
-                new_cancel_flag.clone(),
+                new_cancel_flag,
                 self.workspace.clone(),
                 window,
                 cx,

crates/agent_ui/src/slash_command_picker.rs 🔗

@@ -140,12 +140,10 @@ impl PickerDelegate for SlashCommandDelegate {
                     );
                     ret.push(index - 1);
                 }
-            } else {
-                if let SlashCommandEntry::Advert { .. } = command {
-                    previous_is_advert = true;
-                    if index != 0 {
-                        ret.push(index - 1);
-                    }
+            } else if let SlashCommandEntry::Advert { .. } = command {
+                previous_is_advert = true;
+                if index != 0 {
+                    ret.push(index - 1);
                 }
             }
         }
@@ -214,7 +212,7 @@ impl PickerDelegate for SlashCommandDelegate {
                                         let mut label = format!("{}", info.name);
                                         if let Some(args) = info.args.as_ref().filter(|_| selected)
                                         {
-                                            label.push_str(&args);
+                                            label.push_str(args);
                                         }
                                         Label::new(label)
                                             .single_line()
@@ -306,7 +304,7 @@ where
                                 )
                                 .child(
                                     Icon::new(IconName::ArrowUpRight)
-                                        .size(IconSize::XSmall)
+                                        .size(IconSize::Small)
                                         .color(Color::Muted),
                                 ),
                         )
@@ -329,9 +327,7 @@ where
         };
 
         let picker_view = cx.new(|cx| {
-            let picker =
-                Picker::uniform_list(delegate, window, cx).max_height(Some(rems(20.).into()));
-            picker
+            Picker::uniform_list(delegate, window, cx).max_height(Some(rems(20.).into()))
         });
 
         let handle = self

crates/agent_ui/src/slash_command_settings.rs 🔗

@@ -7,22 +7,11 @@ use settings::{Settings, SettingsSources};
 /// Settings for slash commands.
 #[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema)]
 pub struct SlashCommandSettings {
-    /// Settings for the `/docs` slash command.
-    #[serde(default)]
-    pub docs: DocsCommandSettings,
     /// Settings for the `/cargo-workspace` slash command.
     #[serde(default)]
     pub cargo_workspace: CargoWorkspaceCommandSettings,
 }
 
-/// Settings for the `/docs` slash command.
-#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema)]
-pub struct DocsCommandSettings {
-    /// Whether `/docs` is enabled.
-    #[serde(default)]
-    pub enabled: bool,
-}
-
 /// Settings for the `/cargo-workspace` slash command.
 #[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema)]
 pub struct CargoWorkspaceCommandSettings {

crates/agent_ui/src/terminal_codegen.rs 🔗

@@ -48,7 +48,7 @@ impl TerminalCodegen {
             let prompt = prompt_task.await;
             let model_telemetry_id = model.telemetry_id();
             let model_provider_id = model.provider_id();
-            let response = model.stream_completion_text(prompt, &cx).await;
+            let response = model.stream_completion_text(prompt, cx).await;
             let generate = async {
                 let message_id = response
                     .as_ref()

crates/agent_ui/src/terminal_inline_assistant.rs 🔗

@@ -388,20 +388,20 @@ impl TerminalInlineAssistant {
         window: &mut Window,
         cx: &mut App,
     ) {
-        if let Some(assist) = self.assists.get_mut(&assist_id) {
-            if let Some(prompt_editor) = assist.prompt_editor.as_ref().cloned() {
-                assist
-                    .terminal
-                    .update(cx, |terminal, cx| {
-                        terminal.clear_block_below_cursor(cx);
-                        let block = terminal_view::BlockProperties {
-                            height,
-                            render: Box::new(move |_| prompt_editor.clone().into_any_element()),
-                        };
-                        terminal.set_block_below_cursor(block, window, cx);
-                    })
-                    .log_err();
-            }
+        if let Some(assist) = self.assists.get_mut(&assist_id)
+            && let Some(prompt_editor) = assist.prompt_editor.as_ref().cloned()
+        {
+            assist
+                .terminal
+                .update(cx, |terminal, cx| {
+                    terminal.clear_block_below_cursor(cx);
+                    let block = terminal_view::BlockProperties {
+                        height,
+                        render: Box::new(move |_| prompt_editor.clone().into_any_element()),
+                    };
+                    terminal.set_block_below_cursor(block, window, cx);
+                })
+                .log_err();
         }
     }
 }
@@ -432,7 +432,7 @@ impl TerminalInlineAssist {
             terminal: terminal.downgrade(),
             prompt_editor: Some(prompt_editor.clone()),
             codegen: codegen.clone(),
-            workspace: workspace.clone(),
+            workspace,
             context_store,
             prompt_store,
             _subscriptions: vec![
@@ -450,23 +450,20 @@ impl TerminalInlineAssist {
                                 return;
                             };
 
-                            if let CodegenStatus::Error(error) = &codegen.read(cx).status {
-                                if assist.prompt_editor.is_none() {
-                                    if let Some(workspace) = assist.workspace.upgrade() {
-                                        let error =
-                                            format!("Terminal inline assistant error: {}", error);
-                                        workspace.update(cx, |workspace, cx| {
-                                            struct InlineAssistantError;
-
-                                            let id =
-                                                NotificationId::composite::<InlineAssistantError>(
-                                                    assist_id.0,
-                                                );
-
-                                            workspace.show_toast(Toast::new(id, error), cx);
-                                        })
-                                    }
-                                }
+                            if let CodegenStatus::Error(error) = &codegen.read(cx).status
+                                && assist.prompt_editor.is_none()
+                                && let Some(workspace) = assist.workspace.upgrade()
+                            {
+                                let error = format!("Terminal inline assistant error: {}", error);
+                                workspace.update(cx, |workspace, cx| {
+                                    struct InlineAssistantError;
+
+                                    let id = NotificationId::composite::<InlineAssistantError>(
+                                        assist_id.0,
+                                    );
+
+                                    workspace.show_toast(Toast::new(id, error), cx);
+                                })
                             }
 
                             if assist.prompt_editor.is_none() {

crates/agent_ui/src/text_thread_editor.rs 🔗

@@ -1,14 +1,12 @@
 use crate::{
-    burn_mode_tooltip::BurnModeTooltip,
+    QuoteSelection,
     language_model_selector::{LanguageModelSelector, language_model_selector},
+    ui::BurnModeTooltip,
 };
 use agent_settings::{AgentSettings, CompletionMode};
 use anyhow::Result;
 use assistant_slash_command::{SlashCommand, SlashCommandOutputSection, SlashCommandWorkingSet};
-use assistant_slash_commands::{
-    DefaultSlashCommand, DocsSlashCommand, DocsSlashCommandArgs, FileSlashCommand,
-    selections_creases,
-};
+use assistant_slash_commands::{DefaultSlashCommand, FileSlashCommand, selections_creases};
 use client::{proto, zed_urls};
 use collections::{BTreeSet, HashMap, HashSet, hash_map};
 use editor::{
@@ -30,7 +28,6 @@ use gpui::{
     StatefulInteractiveElement, Styled, Subscription, Task, Transformation, WeakEntity, actions,
     div, img, percentage, point, prelude::*, pulsating_between, size,
 };
-use indexed_docs::IndexedDocsStore;
 use language::{
     BufferSnapshot, LspAdapterDelegate, ToOffset,
     language_settings::{SoftWrap, all_language_settings},
@@ -77,7 +74,7 @@ use crate::{slash_command::SlashCommandCompletionProvider, slash_command_picker}
 use assistant_context::{
     AssistantContext, CacheStatus, Content, ContextEvent, ContextId, InvokedSlashCommandId,
     InvokedSlashCommandStatus, Message, MessageId, MessageMetadata, MessageStatus,
-    ParsedSlashCommand, PendingSlashCommandStatus, ThoughtProcessOutputSection,
+    PendingSlashCommandStatus, ThoughtProcessOutputSection,
 };
 
 actions!(
@@ -93,8 +90,6 @@ actions!(
         CycleMessageRole,
         /// Inserts the selected text into the active editor.
         InsertIntoEditor,
-        /// Quotes the current selection in the assistant conversation.
-        QuoteSelection,
         /// Splits the conversation at the current cursor position.
         Split,
     ]
@@ -377,7 +372,7 @@ impl TextThreadEditor {
             .map(|default| default.provider);
         if provider
             .as_ref()
-            .map_or(false, |provider| provider.must_accept_terms(cx))
+            .is_some_and(|provider| provider.must_accept_terms(cx))
         {
             self.show_accept_terms = true;
             cx.notify();
@@ -461,7 +456,7 @@ impl TextThreadEditor {
                         || snapshot
                             .chars_at(newest_cursor)
                             .next()
-                            .map_or(false, |ch| ch != '\n')
+                            .is_some_and(|ch| ch != '\n')
                     {
                         editor.move_to_end_of_line(
                             &MoveToEndOfLine {
@@ -544,7 +539,7 @@ impl TextThreadEditor {
             let context = self.context.read(cx);
             let sections = context
                 .slash_command_output_sections()
-                .into_iter()
+                .iter()
                 .filter(|section| section.is_valid(context.buffer().read(cx)))
                 .cloned()
                 .collect::<Vec<_>>();
@@ -701,19 +696,7 @@ impl TextThreadEditor {
                                 }
                             };
                             let render_trailer = {
-                                let command = command.clone();
-                                move |row, _unfold, _window: &mut Window, cx: &mut App| {
-                                    // TODO: In the future we should investigate how we can expose
-                                    // this as a hook on the `SlashCommand` trait so that we don't
-                                    // need to special-case it here.
-                                    if command.name == DocsSlashCommand::NAME {
-                                        return render_docs_slash_command_trailer(
-                                            row,
-                                            command.clone(),
-                                            cx,
-                                        );
-                                    }
-
+                                move |_row, _unfold, _window: &mut Window, _cx: &mut App| {
                                     Empty.into_any()
                                 }
                             };
@@ -761,32 +744,27 @@ impl TextThreadEditor {
     ) {
         if let Some(invoked_slash_command) =
             self.context.read(cx).invoked_slash_command(&command_id)
+            && let InvokedSlashCommandStatus::Finished = invoked_slash_command.status
         {
-            if let InvokedSlashCommandStatus::Finished = invoked_slash_command.status {
-                let run_commands_in_ranges = invoked_slash_command
-                    .run_commands_in_ranges
-                    .iter()
-                    .cloned()
-                    .collect::<Vec<_>>();
-                for range in run_commands_in_ranges {
-                    let commands = self.context.update(cx, |context, cx| {
-                        context.reparse(cx);
-                        context
-                            .pending_commands_for_range(range.clone(), cx)
-                            .to_vec()
-                    });
+            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
+                        .pending_commands_for_range(range.clone(), cx)
+                        .to_vec()
+                });
 
-                    for command in commands {
-                        self.run_command(
-                            command.source_range,
-                            &command.name,
-                            &command.arguments,
-                            false,
-                            self.workspace.clone(),
-                            window,
-                            cx,
-                        );
-                    }
+                for command in commands {
+                    self.run_command(
+                        command.source_range,
+                        &command.name,
+                        &command.arguments,
+                        false,
+                        self.workspace.clone(),
+                        window,
+                        cx,
+                    );
                 }
             }
         }
@@ -1258,7 +1236,7 @@ impl TextThreadEditor {
             let mut new_blocks = vec![];
             let mut block_index_to_message = vec![];
             for message in self.context.read(cx).messages(cx) {
-                if let Some(_) = blocks_to_remove.remove(&message.id) {
+                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 {
                         debug_assert!(
@@ -1296,7 +1274,7 @@ impl TextThreadEditor {
         context_editor_view: &Entity<TextThreadEditor>,
         cx: &mut Context<Workspace>,
     ) -> Option<(String, bool)> {
-        const CODE_FENCE_DELIMITER: &'static str = "```";
+        const CODE_FENCE_DELIMITER: &str = "```";
 
         let context_editor = context_editor_view.read(cx).editor.clone();
         context_editor.update(cx, |context_editor, cx| {
@@ -1760,7 +1738,7 @@ impl TextThreadEditor {
                                 render_slash_command_output_toggle,
                                 |_, _, _, _| Empty.into_any(),
                             )
-                            .with_metadata(metadata.crease.clone())
+                            .with_metadata(metadata.crease)
                         }),
                         cx,
                     );
@@ -1831,7 +1809,7 @@ impl TextThreadEditor {
                 .filter_map(|(anchor, render_image)| {
                     const MAX_HEIGHT_IN_LINES: u32 = 8;
                     let anchor = buffer.anchor_in_excerpt(excerpt_id, anchor).unwrap();
-                    let image = render_image.clone();
+                    let image = render_image;
                     anchor.is_valid(&buffer).then(|| BlockProperties {
                         placement: BlockPlacement::Above(anchor),
                         height: Some(MAX_HEIGHT_IN_LINES),
@@ -1894,7 +1872,7 @@ impl TextThreadEditor {
     }
 
     fn render_send_button(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
-        let focus_handle = self.focus_handle(cx).clone();
+        let focus_handle = self.focus_handle(cx);
 
         let (style, tooltip) = match token_state(&self.context, cx) {
             Some(TokenState::NoTokensLeft { .. }) => (
@@ -2036,7 +2014,7 @@ impl TextThreadEditor {
             None => IconName::Ai,
         };
 
-        let focus_handle = self.editor().focus_handle(cx).clone();
+        let focus_handle = self.editor().focus_handle(cx);
 
         PickerPopoverMenu::new(
             self.language_model_selector.clone(),
@@ -2182,8 +2160,8 @@ impl TextThreadEditor {
 
 /// Returns the contents of the *outermost* fenced code block that contains the given offset.
 fn find_surrounding_code_block(snapshot: &BufferSnapshot, offset: usize) -> Option<Range<usize>> {
-    const CODE_BLOCK_NODE: &'static str = "fenced_code_block";
-    const CODE_BLOCK_CONTENT: &'static str = "code_fence_content";
+    const CODE_BLOCK_NODE: &str = "fenced_code_block";
+    const CODE_BLOCK_CONTENT: &str = "code_fence_content";
 
     let layer = snapshot.syntax_layers().next()?;
 
@@ -2233,7 +2211,7 @@ fn render_thought_process_fold_icon_button(
         let button = match status {
             ThoughtProcessStatus::Pending => button
                 .child(
-                    Icon::new(IconName::LightBulb)
+                    Icon::new(IconName::ToolThink)
                         .size(IconSize::Small)
                         .color(Color::Muted),
                 )
@@ -2248,7 +2226,7 @@ fn render_thought_process_fold_icon_button(
                 ),
             ThoughtProcessStatus::Completed => button
                 .style(ButtonStyle::Filled)
-                .child(Icon::new(IconName::LightBulb).size(IconSize::Small))
+                .child(Icon::new(IconName::ToolThink).size(IconSize::Small))
                 .child(Label::new("Thought Process").single_line()),
         };
 
@@ -2398,70 +2376,6 @@ fn render_pending_slash_command_gutter_decoration(
     icon.into_any_element()
 }
 
-fn render_docs_slash_command_trailer(
-    row: MultiBufferRow,
-    command: ParsedSlashCommand,
-    cx: &mut App,
-) -> AnyElement {
-    if command.arguments.is_empty() {
-        return Empty.into_any();
-    }
-    let args = DocsSlashCommandArgs::parse(&command.arguments);
-
-    let Some(store) = args
-        .provider()
-        .and_then(|provider| IndexedDocsStore::try_global(provider, cx).ok())
-    else {
-        return Empty.into_any();
-    };
-
-    let Some(package) = args.package() else {
-        return Empty.into_any();
-    };
-
-    let mut children = Vec::new();
-
-    if store.is_indexing(&package) {
-        children.push(
-            div()
-                .id(("crates-being-indexed", row.0))
-                .child(Icon::new(IconName::ArrowCircle).with_animation(
-                    "arrow-circle",
-                    Animation::new(Duration::from_secs(4)).repeat(),
-                    |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
-                ))
-                .tooltip({
-                    let package = package.clone();
-                    Tooltip::text(format!("Indexing {package}…"))
-                })
-                .into_any_element(),
-        );
-    }
-
-    if let Some(latest_error) = store.latest_error_for_package(&package) {
-        children.push(
-            div()
-                .id(("latest-error", row.0))
-                .child(
-                    Icon::new(IconName::Warning)
-                        .size(IconSize::Small)
-                        .color(Color::Warning),
-                )
-                .tooltip(Tooltip::text(format!("Failed to index: {latest_error}")))
-                .into_any_element(),
-        )
-    }
-
-    let is_indexing = store.is_indexing(&package);
-    let latest_error = store.latest_error_for_package(&package);
-
-    if !is_indexing && latest_error.is_none() {
-        return Empty.into_any();
-    }
-
-    h_flex().gap_2().children(children).into_any_element()
-}
-
 #[derive(Debug, Clone, Serialize, Deserialize)]
 struct CopyMetadata {
     creases: Vec<SelectedCreaseMetadata>,
@@ -3214,7 +3128,7 @@ mod tests {
         let context_editor = window
             .update(&mut cx, |_, window, cx| {
                 cx.new(|cx| {
-                    let editor = TextThreadEditor::for_context(
+                    TextThreadEditor::for_context(
                         context.clone(),
                         fs,
                         workspace.downgrade(),
@@ -3222,8 +3136,7 @@ mod tests {
                         None,
                         window,
                         cx,
-                    );
-                    editor
+                    )
                 })
             })
             .unwrap();

crates/agent_ui/src/thread_history.rs 🔗

@@ -166,14 +166,13 @@ impl ThreadHistory {
                                 this.all_entries.len().saturating_sub(1),
                                 cx,
                             );
-                        } else if let Some(prev_id) = previously_selected_entry {
-                            if let Some(new_ix) = this
+                        } else if let Some(prev_id) = previously_selected_entry
+                            && let Some(new_ix) = this
                                 .all_entries
                                 .iter()
                                 .position(|probe| probe.id() == prev_id)
-                            {
-                                this.set_selected_entry_index(new_ix, cx);
-                            }
+                        {
+                            this.set_selected_entry_index(new_ix, cx);
                         }
                     }
                     SearchState::Searching { query, .. } | SearchState::Searched { query, .. } => {
@@ -541,6 +540,7 @@ impl Render for ThreadHistory {
         v_flex()
             .key_context("ThreadHistory")
             .size_full()
+            .bg(cx.theme().colors().panel_background)
             .on_action(cx.listener(Self::select_previous))
             .on_action(cx.listener(Self::select_next))
             .on_action(cx.listener(Self::select_first))

crates/agent_ui/src/tool_compatibility.rs 🔗

@@ -14,13 +14,11 @@ pub struct IncompatibleToolsState {
 
 impl IncompatibleToolsState {
     pub fn new(thread: Entity<Thread>, cx: &mut Context<Self>) -> Self {
-        let _tool_working_set_subscription =
-            cx.subscribe(&thread, |this, _, event, _| match event {
-                ThreadEvent::ProfileChanged => {
-                    this.cache.clear();
-                }
-                _ => {}
-            });
+        let _tool_working_set_subscription = cx.subscribe(&thread, |this, _, event, _| {
+            if let ThreadEvent::ProfileChanged = event {
+                this.cache.clear();
+            }
+        });
 
         Self {
             cache: HashMap::default(),

crates/agent_ui/src/ui.rs 🔗

@@ -2,7 +2,6 @@ mod agent_notification;
 mod burn_mode_tooltip;
 mod context_pill;
 mod end_trial_upsell;
-mod new_thread_button;
 mod onboarding_modal;
 pub mod preview;
 
@@ -10,5 +9,4 @@ pub use agent_notification::*;
 pub use burn_mode_tooltip::*;
 pub use context_pill::*;
 pub use end_trial_upsell::*;
-pub use new_thread_button::*;
 pub use onboarding_modal::*;

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

@@ -2,11 +2,11 @@ use crate::ToggleBurnMode;
 use gpui::{Context, FontWeight, IntoElement, Render, Window};
 use ui::{KeyBinding, prelude::*, tooltip_container};
 
-pub struct MaxModeTooltip {
+pub struct BurnModeTooltip {
     selected: bool,
 }
 
-impl MaxModeTooltip {
+impl BurnModeTooltip {
     pub fn new() -> Self {
         Self { selected: false }
     }
@@ -17,7 +17,7 @@ impl MaxModeTooltip {
     }
 }
 
-impl Render for MaxModeTooltip {
+impl Render for BurnModeTooltip {
     fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
         let (icon, color) = if self.selected {
             (IconName::ZedBurnModeOn, Color::Error)

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

@@ -353,7 +353,7 @@ impl AddedContext {
             name,
             parent,
             tooltip: Some(full_path_string),
-            icon_path: FileIcons::get_icon(&full_path, cx),
+            icon_path: FileIcons::get_icon(full_path, cx),
             status: ContextStatus::Ready,
             render_hover: None,
             handle: AgentContextHandle::File(handle),
@@ -499,7 +499,7 @@ impl AddedContext {
                 let thread = handle.thread.clone();
                 Some(Rc::new(move |_, cx| {
                     let text = thread.read(cx).latest_detailed_summary_or_text();
-                    ContextPillHover::new_text(text.clone(), cx).into()
+                    ContextPillHover::new_text(text, cx).into()
                 }))
             },
             handle: AgentContextHandle::Thread(handle),
@@ -574,7 +574,7 @@ impl AddedContext {
             .unwrap_or_else(|| "Unnamed Rule".into());
         Some(AddedContext {
             kind: ContextKind::Rules,
-            name: title.clone(),
+            name: title,
             parent: None,
             tooltip: None,
             icon_path: None,
@@ -615,7 +615,7 @@ impl AddedContext {
             let full_path_string: SharedString = full_path.to_string_lossy().into_owned().into();
             let (name, parent) =
                 extract_file_name_and_directory_from_full_path(full_path, &full_path_string);
-            let icon_path = FileIcons::get_icon(&full_path, cx);
+            let icon_path = FileIcons::get_icon(full_path, cx);
             (name, parent, icon_path)
         } else {
             ("Image".into(), None, None)
@@ -706,7 +706,7 @@ impl ContextFileExcerpt {
             .and_then(|p| p.file_name())
             .map(|n| n.to_string_lossy().into_owned().into());
 
-        let icon_path = FileIcons::get_icon(&full_path, cx);
+        let icon_path = FileIcons::get_icon(full_path, cx);
 
         ContextFileExcerpt {
             file_name_and_range: file_name_and_range.into(),

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

@@ -1,75 +0,0 @@
-use gpui::{ClickEvent, ElementId, IntoElement, ParentElement, Styled};
-use ui::prelude::*;
-
-#[derive(IntoElement)]
-pub struct NewThreadButton {
-    id: ElementId,
-    label: SharedString,
-    icon: IconName,
-    keybinding: Option<ui::KeyBinding>,
-    on_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
-}
-
-impl NewThreadButton {
-    pub fn new(id: impl Into<ElementId>, label: impl Into<SharedString>, icon: IconName) -> Self {
-        Self {
-            id: id.into(),
-            label: label.into(),
-            icon,
-            keybinding: None,
-            on_click: None,
-        }
-    }
-
-    pub fn keybinding(mut self, keybinding: Option<ui::KeyBinding>) -> Self {
-        self.keybinding = keybinding;
-        self
-    }
-
-    pub fn on_click<F>(mut self, handler: F) -> Self
-    where
-        F: Fn(&mut Window, &mut App) + 'static,
-    {
-        self.on_click = Some(Box::new(
-            move |_: &ClickEvent, window: &mut Window, cx: &mut App| handler(window, cx),
-        ));
-        self
-    }
-}
-
-impl RenderOnce for NewThreadButton {
-    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
-        h_flex()
-            .id(self.id)
-            .w_full()
-            .py_1p5()
-            .px_2()
-            .gap_1()
-            .justify_between()
-            .rounded_md()
-            .border_1()
-            .border_color(cx.theme().colors().border.opacity(0.4))
-            .bg(cx.theme().colors().element_active.opacity(0.2))
-            .hover(|style| {
-                style
-                    .bg(cx.theme().colors().element_hover)
-                    .border_color(cx.theme().colors().border)
-            })
-            .child(
-                h_flex()
-                    .gap_1p5()
-                    .child(
-                        Icon::new(self.icon)
-                            .size(IconSize::XSmall)
-                            .color(Color::Muted),
-                    )
-                    .child(Label::new(self.label).size(LabelSize::Small)),
-            )
-            .when_some(self.keybinding, |this, keybinding| {
-                this.child(keybinding.size(rems_from_px(10.)))
-            })
-            .when_some(self.on_click, |this, on_click| {
-                this.on_click(move |event, window, cx| on_click(event, window, cx))
-            })
-    }
-}

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

@@ -139,7 +139,7 @@ impl Render for AgentOnboardingModal {
                     .child(Headline::new("Agentic Editing in Zed").size(HeadlineSize::Large)),
             )
             .child(h_flex().absolute().top_2().right_2().child(
-                IconButton::new("cancel", IconName::X).on_click(cx.listener(
+                IconButton::new("cancel", IconName::Close).on_click(cx.listener(
                     |_, _: &ClickEvent, _window, cx| {
                         agent_onboarding_event!("Cancelled", trigger = "X click");
                         cx.emit(DismissEvent);

crates/agent_ui/src/ui/preview/usage_callouts.rs 🔗

@@ -80,14 +80,10 @@ impl RenderOnce for UsageCallout {
             }
         };
 
-        let icon = if is_limit_reached {
-            Icon::new(IconName::X)
-                .color(Color::Error)
-                .size(IconSize::XSmall)
+        let (icon, severity) = if is_limit_reached {
+            (IconName::Close, Severity::Error)
         } else {
-            Icon::new(IconName::Warning)
-                .color(Color::Warning)
-                .size(IconSize::XSmall)
+            (IconName::Warning, Severity::Warning)
         };
 
         div()
@@ -95,10 +91,12 @@ impl RenderOnce for UsageCallout {
             .border_color(cx.theme().colors().border)
             .child(
                 Callout::new()
+                    .icon(icon)
+                    .severity(severity)
                     .icon(icon)
                     .title(title)
                     .description(message)
-                    .primary_action(
+                    .actions_slot(
                         Button::new("upgrade", button_text)
                             .label_size(LabelSize::Small)
                             .on_click(move |_, _, cx| {

crates/ai_onboarding/src/agent_api_keys_onboarding.rs 🔗

@@ -11,7 +11,7 @@ impl ApiKeysWithProviders {
         cx.subscribe(
             &LanguageModelRegistry::global(cx),
             |this: &mut Self, _registry, event: &language_model::Event, cx| match event {
-                language_model::Event::ProviderStateChanged
+                language_model::Event::ProviderStateChanged(_)
                 | language_model::Event::AddedProvider(_)
                 | language_model::Event::RemovedProvider(_) => {
                     this.configured_providers = Self::compute_configured_providers(cx)
@@ -33,7 +33,7 @@ impl ApiKeysWithProviders {
             .filter(|provider| {
                 provider.is_authenticated(cx) && provider.id() != ZED_CLOUD_PROVIDER_ID
             })
-            .map(|provider| (provider.icon(), provider.name().0.clone()))
+            .map(|provider| (provider.icon(), provider.name().0))
             .collect()
     }
 }

crates/ai_onboarding/src/agent_panel_onboarding_content.rs 🔗

@@ -25,7 +25,7 @@ impl AgentPanelOnboarding {
         cx.subscribe(
             &LanguageModelRegistry::global(cx),
             |this: &mut Self, _registry, event: &language_model::Event, cx| match event {
-                language_model::Event::ProviderStateChanged
+                language_model::Event::ProviderStateChanged(_)
                 | language_model::Event::AddedProvider(_)
                 | language_model::Event::RemovedProvider(_) => {
                     this.configured_providers = Self::compute_available_providers(cx)
@@ -50,7 +50,7 @@ impl AgentPanelOnboarding {
             .filter(|provider| {
                 provider.is_authenticated(cx) && provider.id() != ZED_CLOUD_PROVIDER_ID
             })
-            .map(|provider| (provider.icon(), provider.name().0.clone()))
+            .map(|provider| (provider.icon(), provider.name().0))
             .collect()
     }
 }
@@ -74,7 +74,7 @@ impl Render for AgentPanelOnboarding {
                 }),
             )
             .map(|this| {
-                if enrolled_in_trial || is_pro_user || self.configured_providers.len() >= 1 {
+                if enrolled_in_trial || is_pro_user || !self.configured_providers.is_empty() {
                     this
                 } else {
                     this.child(ApiKeysWithoutProviders::new())

crates/ai_onboarding/src/ai_onboarding.rs 🔗

@@ -110,7 +110,7 @@ impl ZedAiOnboarding {
                     .style(ButtonStyle::Outlined)
                     .icon(IconName::ArrowUpRight)
                     .icon_color(Color::Muted)
-                    .icon_size(IconSize::XSmall)
+                    .icon_size(IconSize::Small)
                     .on_click(move |_, _window, cx| {
                         telemetry::event!("Review Terms of Service Clicked");
                         cx.open_url(&zed_urls::terms_of_service(cx))
@@ -332,17 +332,25 @@ impl ZedAiOnboarding {
                     .mb_2(),
             )
             .child(plan_definitions.pro_plan(false))
-            .child(
-                Button::new("pro", "Continue with Zed Pro")
-                    .full_width()
-                    .style(ButtonStyle::Outlined)
-                    .on_click({
-                        let callback = self.continue_with_zed_ai.clone();
-                        move |_, window, cx| {
-                            telemetry::event!("Banner Dismissed", source = "AI Onboarding");
-                            callback(window, cx)
-                        }
-                    }),
+            .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)
+                                }),
+                        ),
+                    )
+                },
             )
             .into_any_element()
     }

crates/ai_onboarding/src/young_account_banner.rs 🔗

@@ -17,6 +17,6 @@ impl RenderOnce for YoungAccountBanner {
         div()
             .max_w_full()
             .my_1()
-            .child(Banner::new().severity(ui::Severity::Warning).child(label))
+            .child(Banner::new().severity(Severity::Warning).child(label))
     }
 }

crates/askpass/src/askpass.rs 🔗

@@ -177,11 +177,11 @@ impl AskPassSession {
             _ = askpass_opened_rx.fuse() => {
                 // Note: this await can only resolve after we are dropped.
                 askpass_kill_master_rx.await.ok();
-                return AskPassResult::CancelledByUser
+                AskPassResult::CancelledByUser
             }
 
             _ = futures::FutureExt::fuse(smol::Timer::after(connection_timeout)) => {
-                return AskPassResult::Timedout
+                AskPassResult::Timedout
             }
         }
     }
@@ -215,7 +215,7 @@ pub fn main(socket: &str) {
     }
 
     #[cfg(target_os = "windows")]
-    while buffer.last().map_or(false, |&b| b == b'\n' || b == b'\r') {
+    while buffer.last().is_some_and(|&b| b == b'\n' || b == b'\r') {
         buffer.pop();
     }
     if buffer.last() != Some(&b'\0') {

crates/assets/src/assets.rs 🔗

@@ -58,9 +58,7 @@ impl Assets {
     pub fn load_test_fonts(&self, cx: &App) {
         cx.text_system()
             .add_fonts(vec![
-                self.load("fonts/plex-mono/ZedPlexMono-Regular.ttf")
-                    .unwrap()
-                    .unwrap(),
+                self.load("fonts/lilex/Lilex-Regular.ttf").unwrap().unwrap(),
             ])
             .unwrap()
     }

crates/assistant_context/Cargo.toml 🔗

@@ -11,6 +11,9 @@ workspace = true
 [lib]
 path = "src/assistant_context.rs"
 
+[features]
+test-support = []
+
 [dependencies]
 agent_settings.workspace = true
 anyhow.workspace = true

crates/assistant_context/src/assistant_context.rs 🔗

@@ -590,17 +590,16 @@ impl From<&Message> for MessageMetadata {
 
 impl MessageMetadata {
     pub fn is_cache_valid(&self, buffer: &BufferSnapshot, range: &Range<usize>) -> bool {
-        let result = match &self.cache {
+        match &self.cache {
             Some(MessageCacheMetadata { cached_at, .. }) => !buffer.has_edits_since_in_range(
-                &cached_at,
+                cached_at,
                 Range {
                     start: buffer.anchor_at(range.start, Bias::Right),
                     end: buffer.anchor_at(range.end, Bias::Left),
                 },
             ),
             _ => false,
-        };
-        result
+        }
     }
 }
 
@@ -1023,9 +1022,11 @@ impl AssistantContext {
                     summary: new_summary,
                     ..
                 } => {
-                    if self.summary.timestamp().map_or(true, |current_timestamp| {
-                        new_summary.timestamp > current_timestamp
-                    }) {
+                    if self
+                        .summary
+                        .timestamp()
+                        .is_none_or(|current_timestamp| new_summary.timestamp > current_timestamp)
+                    {
                         self.summary = ContextSummary::Content(new_summary);
                         summary_generated = true;
                     }
@@ -1076,20 +1077,20 @@ impl AssistantContext {
                     timestamp,
                     ..
                 } => {
-                    if let Some(slash_command) = self.invoked_slash_commands.get_mut(&id) {
-                        if timestamp > slash_command.timestamp {
-                            slash_command.timestamp = timestamp;
-                            match error_message {
-                                Some(message) => {
-                                    slash_command.status =
-                                        InvokedSlashCommandStatus::Error(message.into());
-                                }
-                                None => {
-                                    slash_command.status = InvokedSlashCommandStatus::Finished;
-                                }
+                    if let Some(slash_command) = self.invoked_slash_commands.get_mut(&id)
+                        && timestamp > slash_command.timestamp
+                    {
+                        slash_command.timestamp = timestamp;
+                        match error_message {
+                            Some(message) => {
+                                slash_command.status =
+                                    InvokedSlashCommandStatus::Error(message.into());
+                            }
+                            None => {
+                                slash_command.status = InvokedSlashCommandStatus::Finished;
                             }
-                            cx.emit(ContextEvent::InvokedSlashCommandChanged { command_id: id });
                         }
+                        cx.emit(ContextEvent::InvokedSlashCommandChanged { command_id: id });
                     }
                 }
                 ContextOperation::BufferOperation(_) => unreachable!(),
@@ -1339,7 +1340,7 @@ impl AssistantContext {
                 let is_invalid = self
                     .messages_metadata
                     .get(&message_id)
-                    .map_or(true, |metadata| {
+                    .is_none_or(|metadata| {
                         !metadata.is_cache_valid(&buffer, &message.offset_range)
                             || *encountered_invalid
                     });
@@ -1368,10 +1369,10 @@ impl AssistantContext {
                 continue;
             }
 
-            if let Some(last_anchor) = last_anchor {
-                if message.id == last_anchor {
-                    hit_last_anchor = true;
-                }
+            if let Some(last_anchor) = last_anchor
+                && message.id == last_anchor
+            {
+                hit_last_anchor = true;
             }
 
             new_anchor_needs_caching = new_anchor_needs_caching
@@ -1406,14 +1407,14 @@ impl AssistantContext {
         if !self.pending_completions.is_empty() {
             return;
         }
-        if let Some(cache_configuration) = cache_configuration {
-            if !cache_configuration.should_speculate {
-                return;
-            }
+        if let Some(cache_configuration) = cache_configuration
+            && !cache_configuration.should_speculate
+        {
+            return;
         }
 
         let request = {
-            let mut req = self.to_completion_request(Some(&model), cx);
+            let mut req = self.to_completion_request(Some(model), cx);
             // Skip the last message because it's likely to change and
             // therefore would be a waste to cache.
             req.messages.pop();
@@ -1428,7 +1429,7 @@ impl AssistantContext {
         let model = Arc::clone(model);
         self.pending_cache_warming_task = cx.spawn(async move |this, cx| {
             async move {
-                match model.stream_completion(request, &cx).await {
+                match model.stream_completion(request, cx).await {
                     Ok(mut stream) => {
                         stream.next().await;
                         log::info!("Cache warming completed successfully");
@@ -1552,25 +1553,24 @@ impl AssistantContext {
                     })
                     .map(ToOwned::to_owned)
                     .collect::<SmallVec<_>>();
-                if let Some(command) = self.slash_commands.command(name, cx) {
-                    if !command.requires_argument() || !arguments.is_empty() {
-                        let start_ix = offset + command_line.name.start - 1;
-                        let end_ix = offset
-                            + command_line
-                                .arguments
-                                .last()
-                                .map_or(command_line.name.end, |argument| argument.end);
-                        let source_range =
-                            buffer.anchor_after(start_ix)..buffer.anchor_after(end_ix);
-                        let pending_command = ParsedSlashCommand {
-                            name: name.to_string(),
-                            arguments,
-                            source_range,
-                            status: PendingSlashCommandStatus::Idle,
-                        };
-                        updated.push(pending_command.clone());
-                        new_commands.push(pending_command);
-                    }
+                if let Some(command) = self.slash_commands.command(name, cx)
+                    && (!command.requires_argument() || !arguments.is_empty())
+                {
+                    let start_ix = offset + command_line.name.start - 1;
+                    let end_ix = offset
+                        + command_line
+                            .arguments
+                            .last()
+                            .map_or(command_line.name.end, |argument| argument.end);
+                    let source_range = buffer.anchor_after(start_ix)..buffer.anchor_after(end_ix);
+                    let pending_command = ParsedSlashCommand {
+                        name: name.to_string(),
+                        arguments,
+                        source_range,
+                        status: PendingSlashCommandStatus::Idle,
+                    };
+                    updated.push(pending_command.clone());
+                    new_commands.push(pending_command);
                 }
             }
 
@@ -1661,12 +1661,12 @@ impl AssistantContext {
     ) -> Range<usize> {
         let buffer = self.buffer.read(cx);
         let start_ix = match all_annotations
-            .binary_search_by(|probe| probe.range().end.cmp(&range.start, &buffer))
+            .binary_search_by(|probe| probe.range().end.cmp(&range.start, buffer))
         {
             Ok(ix) | Err(ix) => ix,
         };
         let end_ix = match all_annotations
-            .binary_search_by(|probe| probe.range().start.cmp(&range.end, &buffer))
+            .binary_search_by(|probe| probe.range().start.cmp(&range.end, buffer))
         {
             Ok(ix) => ix + 1,
             Err(ix) => ix,
@@ -1799,14 +1799,13 @@ impl AssistantContext {
                                 });
 
                                 let end = this.buffer.read(cx).anchor_before(insert_position);
-                                if run_commands_in_text {
-                                    if let Some(invoked_slash_command) =
+                                if run_commands_in_text
+                                    && let Some(invoked_slash_command) =
                                         this.invoked_slash_commands.get_mut(&command_id)
-                                    {
-                                        invoked_slash_command
-                                            .run_commands_in_ranges
-                                            .push(start..end);
-                                    }
+                                {
+                                    invoked_slash_command
+                                        .run_commands_in_ranges
+                                        .push(start..end);
                                 }
                             }
                             SlashCommandEvent::EndSection => {
@@ -1862,7 +1861,7 @@ impl AssistantContext {
                         {
                             let newline_offset = insert_position.saturating_sub(1);
                             if buffer.contains_str_at(newline_offset, "\n")
-                                && last_section_range.map_or(true, |last_section_range| {
+                                && last_section_range.is_none_or(|last_section_range| {
                                     !last_section_range
                                         .to_offset(buffer)
                                         .contains(&newline_offset)
@@ -2045,7 +2044,7 @@ impl AssistantContext {
 
         let task = cx.spawn({
             async move |this, cx| {
-                let stream = model.stream_completion(request, &cx);
+                let stream = model.stream_completion(request, cx);
                 let assistant_message_id = assistant_message.id;
                 let mut response_latency = None;
                 let stream_completion = async {
@@ -2081,15 +2080,12 @@ impl AssistantContext {
 
                                 match event {
                                     LanguageModelCompletionEvent::StatusUpdate(status_update) => {
-                                        match status_update {
-                                            CompletionRequestStatus::UsageUpdated { amount, limit } => {
-                                                this.update_model_request_usage(
-                                                    amount as u32,
-                                                    limit,
-                                                    cx,
-                                                );
-                                            }
-                                            _ => {}
+                                        if let CompletionRequestStatus::UsageUpdated { amount, limit } = status_update {
+                                            this.update_model_request_usage(
+                                                amount as u32,
+                                                limit,
+                                                cx,
+                                            );
                                         }
                                     }
                                     LanguageModelCompletionEvent::StartMessage { .. } => {}
@@ -2286,7 +2282,7 @@ impl AssistantContext {
         let mut contents = self.contents(cx).peekable();
 
         fn collect_text_content(buffer: &Buffer, range: Range<usize>) -> Option<String> {
-            let text: String = buffer.text_for_range(range.clone()).collect();
+            let text: String = buffer.text_for_range(range).collect();
             if text.trim().is_empty() {
                 None
             } else {
@@ -2315,10 +2311,7 @@ impl AssistantContext {
             let mut request_message = LanguageModelRequestMessage {
                 role: message.role,
                 content: Vec::new(),
-                cache: message
-                    .cache
-                    .as_ref()
-                    .map_or(false, |cache| cache.is_anchor),
+                cache: message.cache.as_ref().is_some_and(|cache| cache.is_anchor),
             };
 
             while let Some(content) = contents.peek() {
@@ -2708,7 +2701,7 @@ impl AssistantContext {
 
             self.summary_task = cx.spawn(async move |this, cx| {
                 let result = async {
-                    let stream = model.model.stream_completion_text(request, &cx);
+                    let stream = model.model.stream_completion_text(request, cx);
                     let mut messages = stream.await?;
 
                     let mut replaced = !replace_old;
@@ -2741,10 +2734,10 @@ impl AssistantContext {
                     }
 
                     this.read_with(cx, |this, _cx| {
-                        if let Some(summary) = this.summary.content() {
-                            if summary.text.is_empty() {
-                                bail!("Model generated an empty summary");
-                            }
+                        if let Some(summary) = this.summary.content()
+                            && summary.text.is_empty()
+                        {
+                            bail!("Model generated an empty summary");
                         }
                         Ok(())
                     })??;
@@ -2799,7 +2792,7 @@ impl AssistantContext {
         let mut current_message = messages.next();
         while let Some(offset) = offsets.next() {
             // Locate the message that contains the offset.
-            while current_message.as_ref().map_or(false, |message| {
+            while current_message.as_ref().is_some_and(|message| {
                 !message.offset_range.contains(&offset) && messages.peek().is_some()
             }) {
                 current_message = messages.next();
@@ -2809,7 +2802,7 @@ impl AssistantContext {
             };
 
             // Skip offsets that are in the same message.
-            while offsets.peek().map_or(false, |offset| {
+            while offsets.peek().is_some_and(|offset| {
                 message.offset_range.contains(offset) || messages.peek().is_none()
             }) {
                 offsets.next();
@@ -2924,18 +2917,18 @@ impl AssistantContext {
                 fs.create_dir(contexts_dir().as_ref()).await?;
 
                 // rename before write ensures that only one file exists
-                if let Some(old_path) = old_path.as_ref() {
-                    if new_path.as_path() != old_path.as_ref() {
-                        fs.rename(
-                            &old_path,
-                            &new_path,
-                            RenameOptions {
-                                overwrite: true,
-                                ignore_if_exists: true,
-                            },
-                        )
-                        .await?;
-                    }
+                if let Some(old_path) = old_path.as_ref()
+                    && new_path.as_path() != old_path.as_ref()
+                {
+                    fs.rename(
+                        old_path,
+                        &new_path,
+                        RenameOptions {
+                            overwrite: true,
+                            ignore_if_exists: true,
+                        },
+                    )
+                    .await?;
                 }
 
                 // update path before write in case it fails

crates/assistant_context/src/assistant_context_tests.rs 🔗

@@ -1055,7 +1055,7 @@ fn test_mark_cache_anchors(cx: &mut App) {
     assert_eq!(
         messages_cache(&context, cx)
             .iter()
-            .filter(|(_, cache)| cache.as_ref().map_or(false, |cache| cache.is_anchor))
+            .filter(|(_, cache)| cache.as_ref().is_some_and(|cache| cache.is_anchor))
             .count(),
         0,
         "Empty messages should not have any cache anchors."
@@ -1083,7 +1083,7 @@ fn test_mark_cache_anchors(cx: &mut App) {
     assert_eq!(
         messages_cache(&context, cx)
             .iter()
-            .filter(|(_, cache)| cache.as_ref().map_or(false, |cache| cache.is_anchor))
+            .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."
@@ -1098,7 +1098,7 @@ fn test_mark_cache_anchors(cx: &mut App) {
     assert_eq!(
         messages_cache(&context, cx)
             .iter()
-            .map(|(_, cache)| cache.as_ref().map_or(false, |cache| cache.is_anchor))
+            .map(|(_, cache)| cache.as_ref().is_some_and(|cache| cache.is_anchor))
             .collect::<Vec<bool>>(),
         vec![true, true, false],
         "Last message should not be an anchor on speculative request."
@@ -1116,7 +1116,7 @@ fn test_mark_cache_anchors(cx: &mut App) {
     assert_eq!(
         messages_cache(&context, cx)
             .iter()
-            .map(|(_, cache)| cache.as_ref().map_or(false, |cache| cache.is_anchor))
+            .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."
@@ -1300,7 +1300,7 @@ fn test_summarize_error(
         context.assist(cx);
     });
 
-    simulate_successful_response(&model, cx);
+    simulate_successful_response(model, cx);
 
     context.read_with(cx, |context, _| {
         assert!(!context.summary().content().unwrap().done);
@@ -1321,7 +1321,7 @@ fn test_summarize_error(
 fn setup_context_editor_with_fake_model(
     cx: &mut TestAppContext,
 ) -> (Entity<AssistantContext>, Arc<FakeLanguageModel>) {
-    let registry = Arc::new(LanguageRegistry::test(cx.executor().clone()));
+    let registry = Arc::new(LanguageRegistry::test(cx.executor()));
 
     let fake_provider = Arc::new(FakeLanguageModelProvider::default());
     let fake_model = Arc::new(fake_provider.test_model());
@@ -1376,7 +1376,7 @@ fn messages_cache(
     context
         .read(cx)
         .messages(cx)
-        .map(|message| (message.id, message.cache.clone()))
+        .map(|message| (message.id, message.cache))
         .collect()
 }
 
@@ -1436,6 +1436,6 @@ impl SlashCommand for FakeSlashCommand {
             sections: vec![],
             run_commands_in_text: false,
         }
-        .to_event_stream()))
+        .into_event_stream()))
     }
 }

crates/assistant_context/src/context_store.rs 🔗

@@ -138,6 +138,27 @@ 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(),
+            context_server_slash_command_ids: Default::default(),
+            host_contexts: Default::default(),
+            fs: project.read(cx).fs().clone(),
+            languages: project.read(cx).languages().clone(),
+            slash_commands: Arc::default(),
+            telemetry: project.read(cx).client().telemetry().clone(),
+            _watch_updates: Task::ready(None),
+            client: project.read(cx).client(),
+            project,
+            project_is_shared: false,
+            client_subscription: None,
+            _project_subscriptions: Default::default(),
+            prompt_builder: Arc::new(PromptBuilder::new(None).unwrap()),
+        }
+    }
+
     async fn handle_advertise_contexts(
         this: Entity<Self>,
         envelope: TypedEnvelope<proto::AdvertiseContexts>,
@@ -299,7 +320,7 @@ impl ContextStore {
                 .client
                 .subscribe_to_entity(remote_id)
                 .log_err()
-                .map(|subscription| subscription.set_entity(&cx.entity(), &mut cx.to_async()));
+                .map(|subscription| subscription.set_entity(&cx.entity(), &cx.to_async()));
             self.advertise_contexts(cx);
         } else {
             self.client_subscription = None;
@@ -768,7 +789,7 @@ impl ContextStore {
         let fs = self.fs.clone();
         cx.spawn(async move |this, cx| {
             pub static ZED_STATELESS: LazyLock<bool> =
-                LazyLock::new(|| std::env::var("ZED_STATELESS").map_or(false, |v| !v.is_empty()));
+                LazyLock::new(|| std::env::var("ZED_STATELESS").is_ok_and(|v| !v.is_empty()));
             if *ZED_STATELESS {
                 return Ok(());
             }
@@ -841,7 +862,7 @@ impl ContextStore {
                     ContextServerStatus::Running => {
                         self.load_context_server_slash_commands(
                             server_id.clone(),
-                            context_server_store.clone(),
+                            context_server_store,
                             cx,
                         );
                     }
@@ -873,34 +894,33 @@ impl ContextStore {
                 return;
             };
 
-            if protocol.capable(context_server::protocol::ServerCapability::Prompts) {
-                if let Some(response) = protocol
+            if protocol.capable(context_server::protocol::ServerCapability::Prompts)
+                && let Some(response) = protocol
                     .request::<context_server::types::requests::PromptsList>(())
                     .await
                     .log_err()
-                {
-                    let slash_command_ids = response
-                        .prompts
-                        .into_iter()
-                        .filter(assistant_slash_commands::acceptable_prompt)
-                        .map(|prompt| {
-                            log::info!("registering context server command: {:?}", prompt.name);
-                            slash_command_working_set.insert(Arc::new(
-                                assistant_slash_commands::ContextServerSlashCommand::new(
-                                    context_server_store.clone(),
-                                    server.id(),
-                                    prompt,
-                                ),
-                            ))
-                        })
-                        .collect::<Vec<_>>();
-
-                    this.update(cx, |this, _cx| {
-                        this.context_server_slash_command_ids
-                            .insert(server_id.clone(), slash_command_ids);
+            {
+                let slash_command_ids = response
+                    .prompts
+                    .into_iter()
+                    .filter(assistant_slash_commands::acceptable_prompt)
+                    .map(|prompt| {
+                        log::info!("registering context server command: {:?}", prompt.name);
+                        slash_command_working_set.insert(Arc::new(
+                            assistant_slash_commands::ContextServerSlashCommand::new(
+                                context_server_store.clone(),
+                                server.id(),
+                                prompt,
+                            ),
+                        ))
                     })
-                    .log_err();
-                }
+                    .collect::<Vec<_>>();
+
+                this.update(cx, |this, _cx| {
+                    this.context_server_slash_command_ids
+                        .insert(server_id.clone(), slash_command_ids);
+                })
+                .log_err();
             }
         })
         .detach();

crates/assistant_slash_command/src/assistant_slash_command.rs 🔗

@@ -161,7 +161,7 @@ impl SlashCommandOutput {
     }
 
     /// Returns this [`SlashCommandOutput`] as a stream of [`SlashCommandEvent`]s.
-    pub fn to_event_stream(mut self) -> BoxStream<'static, Result<SlashCommandEvent>> {
+    pub fn into_event_stream(mut self) -> BoxStream<'static, Result<SlashCommandEvent>> {
         self.ensure_valid_section_ranges();
 
         let mut events = Vec::new();
@@ -363,7 +363,7 @@ mod tests {
                 run_commands_in_text: false,
             };
 
-            let events = output.clone().to_event_stream().collect::<Vec<_>>().await;
+            let events = output.clone().into_event_stream().collect::<Vec<_>>().await;
             let events = events
                 .into_iter()
                 .filter_map(|event| event.ok())
@@ -386,7 +386,7 @@ mod tests {
             );
 
             let new_output =
-                SlashCommandOutput::from_event_stream(output.clone().to_event_stream())
+                SlashCommandOutput::from_event_stream(output.clone().into_event_stream())
                     .await
                     .unwrap();
 
@@ -415,7 +415,7 @@ mod tests {
                 run_commands_in_text: false,
             };
 
-            let events = output.clone().to_event_stream().collect::<Vec<_>>().await;
+            let events = output.clone().into_event_stream().collect::<Vec<_>>().await;
             let events = events
                 .into_iter()
                 .filter_map(|event| event.ok())
@@ -452,7 +452,7 @@ mod tests {
             );
 
             let new_output =
-                SlashCommandOutput::from_event_stream(output.clone().to_event_stream())
+                SlashCommandOutput::from_event_stream(output.clone().into_event_stream())
                     .await
                     .unwrap();
 
@@ -493,7 +493,7 @@ mod tests {
                 run_commands_in_text: false,
             };
 
-            let events = output.clone().to_event_stream().collect::<Vec<_>>().await;
+            let events = output.clone().into_event_stream().collect::<Vec<_>>().await;
             let events = events
                 .into_iter()
                 .filter_map(|event| event.ok())
@@ -562,7 +562,7 @@ mod tests {
             );
 
             let new_output =
-                SlashCommandOutput::from_event_stream(output.clone().to_event_stream())
+                SlashCommandOutput::from_event_stream(output.clone().into_event_stream())
                     .await
                     .unwrap();
 

crates/assistant_slash_commands/Cargo.toml 🔗

@@ -27,7 +27,6 @@ globset.workspace = true
 gpui.workspace = true
 html_to_markdown.workspace = true
 http_client.workspace = true
-indexed_docs.workspace = true
 language.workspace = true
 project.workspace = true
 prompt_store.workspace = true

crates/assistant_slash_commands/src/assistant_slash_commands.rs 🔗

@@ -3,7 +3,6 @@ mod context_server_command;
 mod default_command;
 mod delta_command;
 mod diagnostics_command;
-mod docs_command;
 mod fetch_command;
 mod file_command;
 mod now_command;
@@ -18,7 +17,6 @@ pub use crate::context_server_command::*;
 pub use crate::default_command::*;
 pub use crate::delta_command::*;
 pub use crate::diagnostics_command::*;
-pub use crate::docs_command::*;
 pub use crate::fetch_command::*;
 pub use crate::file_command::*;
 pub use crate::now_command::*;

crates/assistant_slash_commands/src/context_server_command.rs 🔗

@@ -39,12 +39,12 @@ impl SlashCommand for ContextServerSlashCommand {
 
     fn label(&self, cx: &App) -> language::CodeLabel {
         let mut parts = vec![self.prompt.name.as_str()];
-        if let Some(args) = &self.prompt.arguments {
-            if let Some(arg) = args.first() {
-                parts.push(arg.name.as_str());
-            }
+        if let Some(args) = &self.prompt.arguments
+            && let Some(arg) = args.first()
+        {
+            parts.push(arg.name.as_str());
         }
-        create_label_for_command(&parts[0], &parts[1..], cx)
+        create_label_for_command(parts[0], &parts[1..], cx)
     }
 
     fn description(&self) -> String {
@@ -62,9 +62,10 @@ impl SlashCommand for ContextServerSlashCommand {
     }
 
     fn requires_argument(&self) -> bool {
-        self.prompt.arguments.as_ref().map_or(false, |args| {
-            args.iter().any(|arg| arg.required == Some(true))
-        })
+        self.prompt
+            .arguments
+            .as_ref()
+            .is_some_and(|args| args.iter().any(|arg| arg.required == Some(true)))
     }
 
     fn complete_argument(
@@ -190,7 +191,7 @@ impl SlashCommand for ContextServerSlashCommand {
                     text: prompt,
                     run_commands_in_text: false,
                 }
-                .to_event_stream())
+                .into_event_stream())
             })
         } else {
             Task::ready(Err(anyhow!("Context server not found")))

crates/assistant_slash_commands/src/delta_command.rs 🔗

@@ -66,23 +66,22 @@ impl SlashCommand for DeltaSlashCommand {
                 .metadata
                 .as_ref()
                 .and_then(|value| serde_json::from_value::<FileCommandMetadata>(value.clone()).ok())
+                && paths.insert(metadata.path.clone())
             {
-                if paths.insert(metadata.path.clone()) {
-                    file_command_old_outputs.push(
-                        context_buffer
-                            .as_rope()
-                            .slice(section.range.to_offset(&context_buffer)),
-                    );
-                    file_command_new_outputs.push(Arc::new(FileSlashCommand).run(
-                        std::slice::from_ref(&metadata.path),
-                        context_slash_command_output_sections,
-                        context_buffer.clone(),
-                        workspace.clone(),
-                        delegate.clone(),
-                        window,
-                        cx,
-                    ));
-                }
+                file_command_old_outputs.push(
+                    context_buffer
+                        .as_rope()
+                        .slice(section.range.to_offset(&context_buffer)),
+                );
+                file_command_new_outputs.push(Arc::new(FileSlashCommand).run(
+                    std::slice::from_ref(&metadata.path),
+                    context_slash_command_output_sections,
+                    context_buffer.clone(),
+                    workspace.clone(),
+                    delegate.clone(),
+                    window,
+                    cx,
+                ));
             }
         }
 
@@ -95,31 +94,31 @@ impl SlashCommand for DeltaSlashCommand {
                 .into_iter()
                 .zip(file_command_new_outputs)
             {
-                if let Ok(new_output) = new_output {
-                    if let Ok(new_output) = SlashCommandOutput::from_event_stream(new_output).await
-                    {
-                        if let Some(file_command_range) = new_output.sections.first() {
-                            let new_text = &new_output.text[file_command_range.range.clone()];
-                            if old_text.chars().ne(new_text.chars()) {
-                                changes_detected = true;
-                                output.sections.extend(new_output.sections.into_iter().map(
-                                    |section| SlashCommandOutputSection {
-                                        range: output.text.len() + section.range.start
-                                            ..output.text.len() + section.range.end,
-                                        icon: section.icon,
-                                        label: section.label,
-                                        metadata: section.metadata,
-                                    },
-                                ));
-                                output.text.push_str(&new_output.text);
-                            }
-                        }
+                if let Ok(new_output) = new_output
+                    && let Ok(new_output) = SlashCommandOutput::from_event_stream(new_output).await
+                    && let Some(file_command_range) = new_output.sections.first()
+                {
+                    let new_text = &new_output.text[file_command_range.range.clone()];
+                    if old_text.chars().ne(new_text.chars()) {
+                        changes_detected = true;
+                        output
+                            .sections
+                            .extend(new_output.sections.into_iter().map(|section| {
+                                SlashCommandOutputSection {
+                                    range: output.text.len() + section.range.start
+                                        ..output.text.len() + section.range.end,
+                                    icon: section.icon,
+                                    label: section.label,
+                                    metadata: section.metadata,
+                                }
+                            }));
+                        output.text.push_str(&new_output.text);
                     }
                 }
             }
 
             anyhow::ensure!(changes_detected, "no new changes detected");
-            Ok(output.to_event_stream())
+            Ok(output.into_event_stream())
         })
     }
 }

crates/assistant_slash_commands/src/diagnostics_command.rs 🔗

@@ -44,7 +44,7 @@ impl DiagnosticsSlashCommand {
                         score: 0.,
                         positions: Vec::new(),
                         worktree_id: entry.worktree_id.to_usize(),
-                        path: entry.path.clone(),
+                        path: entry.path,
                         path_prefix: path_prefix.clone(),
                         is_dir: false, // Diagnostics can't be produced for directories
                         distance_to_relative_ancestor: 0,
@@ -61,7 +61,7 @@ impl DiagnosticsSlashCommand {
                         snapshot: worktree.snapshot(),
                         include_ignored: worktree
                             .root_entry()
-                            .map_or(false, |entry| entry.is_ignored),
+                            .is_some_and(|entry| entry.is_ignored),
                         include_root_name: true,
                         candidates: project::Candidates::Entries,
                     }
@@ -189,7 +189,7 @@ impl SlashCommand for DiagnosticsSlashCommand {
 
         window.spawn(cx, async move |_| {
             task.await?
-                .map(|output| output.to_event_stream())
+                .map(|output| output.into_event_stream())
                 .context("No diagnostics found")
         })
     }
@@ -249,7 +249,7 @@ fn collect_diagnostics(
                     let worktree = worktree.read(cx);
                     let worktree_root_path = Path::new(worktree.root_name());
                     let relative_path = path.strip_prefix(worktree_root_path).ok()?;
-                    worktree.absolutize(&relative_path).ok()
+                    worktree.absolutize(relative_path).ok()
                 })
             })
             .is_some()
@@ -280,10 +280,10 @@ fn collect_diagnostics(
 
         let mut project_summary = DiagnosticSummary::default();
         for (project_path, path, summary) in diagnostic_summaries {
-            if let Some(path_matcher) = &options.path_matcher {
-                if !path_matcher.is_match(&path) {
-                    continue;
-                }
+            if let Some(path_matcher) = &options.path_matcher
+                && !path_matcher.is_match(&path)
+            {
+                continue;
             }
 
             project_summary.error_count += summary.error_count;
@@ -365,7 +365,7 @@ pub fn collect_buffer_diagnostics(
 ) {
     for (_, group) in snapshot.diagnostic_groups(None) {
         let entry = &group.entries[group.primary_ix];
-        collect_diagnostic(output, entry, &snapshot, include_warnings)
+        collect_diagnostic(output, entry, snapshot, include_warnings)
     }
 }
 
@@ -396,7 +396,7 @@ fn collect_diagnostic(
     let start_row = range.start.row.saturating_sub(EXCERPT_EXPANSION_SIZE);
     let end_row = (range.end.row + EXCERPT_EXPANSION_SIZE).min(snapshot.max_point().row) + 1;
     let excerpt_range =
-        Point::new(start_row, 0).to_offset(&snapshot)..Point::new(end_row, 0).to_offset(&snapshot);
+        Point::new(start_row, 0).to_offset(snapshot)..Point::new(end_row, 0).to_offset(snapshot);
 
     output.text.push_str("```");
     if let Some(language_name) = snapshot.language().map(|l| l.code_fence_block_name()) {

crates/assistant_slash_commands/src/docs_command.rs 🔗

@@ -1,543 +0,0 @@
-use std::path::Path;
-use std::sync::Arc;
-use std::sync::atomic::AtomicBool;
-use std::time::Duration;
-
-use anyhow::{Context as _, Result, anyhow, bail};
-use assistant_slash_command::{
-    ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
-    SlashCommandResult,
-};
-use gpui::{App, BackgroundExecutor, Entity, Task, WeakEntity};
-use indexed_docs::{
-    DocsDotRsProvider, IndexedDocsRegistry, IndexedDocsStore, LocalRustdocProvider, PackageName,
-    ProviderId,
-};
-use language::{BufferSnapshot, LspAdapterDelegate};
-use project::{Project, ProjectPath};
-use ui::prelude::*;
-use util::{ResultExt, maybe};
-use workspace::Workspace;
-
-pub struct DocsSlashCommand;
-
-impl DocsSlashCommand {
-    pub const NAME: &'static str = "docs";
-
-    fn path_to_cargo_toml(project: Entity<Project>, cx: &mut App) -> Option<Arc<Path>> {
-        let worktree = project.read(cx).worktrees(cx).next()?;
-        let worktree = worktree.read(cx);
-        let entry = worktree.entry_for_path("Cargo.toml")?;
-        let path = ProjectPath {
-            worktree_id: worktree.id(),
-            path: entry.path.clone(),
-        };
-        Some(Arc::from(
-            project.read(cx).absolute_path(&path, cx)?.as_path(),
-        ))
-    }
-
-    /// Ensures that the indexed doc providers for Rust are registered.
-    ///
-    /// Ideally we would do this sooner, but we need to wait until we're able to
-    /// access the workspace so we can read the project.
-    fn ensure_rust_doc_providers_are_registered(
-        &self,
-        workspace: Option<WeakEntity<Workspace>>,
-        cx: &mut App,
-    ) {
-        let indexed_docs_registry = IndexedDocsRegistry::global(cx);
-        if indexed_docs_registry
-            .get_provider_store(LocalRustdocProvider::id())
-            .is_none()
-        {
-            let index_provider_deps = maybe!({
-                let workspace = workspace
-                    .as_ref()
-                    .context("no workspace")?
-                    .upgrade()
-                    .context("workspace dropped")?;
-                let project = workspace.read(cx).project().clone();
-                let fs = project.read(cx).fs().clone();
-                let cargo_workspace_root = Self::path_to_cargo_toml(project, cx)
-                    .and_then(|path| path.parent().map(|path| path.to_path_buf()))
-                    .context("no Cargo workspace root found")?;
-
-                anyhow::Ok((fs, cargo_workspace_root))
-            });
-
-            if let Some((fs, cargo_workspace_root)) = index_provider_deps.log_err() {
-                indexed_docs_registry.register_provider(Box::new(LocalRustdocProvider::new(
-                    fs,
-                    cargo_workspace_root,
-                )));
-            }
-        }
-
-        if indexed_docs_registry
-            .get_provider_store(DocsDotRsProvider::id())
-            .is_none()
-        {
-            let http_client = maybe!({
-                let workspace = workspace
-                    .as_ref()
-                    .context("no workspace")?
-                    .upgrade()
-                    .context("workspace was dropped")?;
-                let project = workspace.read(cx).project().clone();
-                anyhow::Ok(project.read(cx).client().http_client())
-            });
-
-            if let Some(http_client) = http_client.log_err() {
-                indexed_docs_registry
-                    .register_provider(Box::new(DocsDotRsProvider::new(http_client)));
-            }
-        }
-    }
-
-    /// Runs just-in-time indexing for a given package, in case the slash command
-    /// is run without any entries existing in the index.
-    fn run_just_in_time_indexing(
-        store: Arc<IndexedDocsStore>,
-        key: String,
-        package: PackageName,
-        executor: BackgroundExecutor,
-    ) -> Task<()> {
-        executor.clone().spawn(async move {
-            let (prefix, needs_full_index) = if let Some((prefix, _)) = key.split_once('*') {
-                // If we have a wildcard in the search, we want to wait until
-                // we've completely finished indexing so we get a full set of
-                // results for the wildcard.
-                (prefix.to_string(), true)
-            } else {
-                (key, false)
-            };
-
-            // If we already have some entries, we assume that we've indexed the package before
-            // and don't need to do it again.
-            let has_any_entries = store
-                .any_with_prefix(prefix.clone())
-                .await
-                .unwrap_or_default();
-            if has_any_entries {
-                return ();
-            };
-
-            let index_task = store.clone().index(package.clone());
-
-            if needs_full_index {
-                _ = index_task.await;
-            } else {
-                loop {
-                    executor.timer(Duration::from_millis(200)).await;
-
-                    if store
-                        .any_with_prefix(prefix.clone())
-                        .await
-                        .unwrap_or_default()
-                        || !store.is_indexing(&package)
-                    {
-                        break;
-                    }
-                }
-            }
-        })
-    }
-}
-
-impl SlashCommand for DocsSlashCommand {
-    fn name(&self) -> String {
-        Self::NAME.into()
-    }
-
-    fn description(&self) -> String {
-        "insert docs".into()
-    }
-
-    fn menu_text(&self) -> String {
-        "Insert Documentation".into()
-    }
-
-    fn requires_argument(&self) -> bool {
-        true
-    }
-
-    fn complete_argument(
-        self: Arc<Self>,
-        arguments: &[String],
-        _cancel: Arc<AtomicBool>,
-        workspace: Option<WeakEntity<Workspace>>,
-        _: &mut Window,
-        cx: &mut App,
-    ) -> Task<Result<Vec<ArgumentCompletion>>> {
-        self.ensure_rust_doc_providers_are_registered(workspace, cx);
-
-        let indexed_docs_registry = IndexedDocsRegistry::global(cx);
-        let args = DocsSlashCommandArgs::parse(arguments);
-        let store = args
-            .provider()
-            .context("no docs provider specified")
-            .and_then(|provider| IndexedDocsStore::try_global(provider, cx));
-        cx.background_spawn(async move {
-            fn build_completions(items: Vec<String>) -> Vec<ArgumentCompletion> {
-                items
-                    .into_iter()
-                    .map(|item| ArgumentCompletion {
-                        label: item.clone().into(),
-                        new_text: item.to_string(),
-                        after_completion: assistant_slash_command::AfterCompletion::Run,
-                        replace_previous_arguments: false,
-                    })
-                    .collect()
-            }
-
-            match args {
-                DocsSlashCommandArgs::NoProvider => {
-                    let providers = indexed_docs_registry.list_providers();
-                    if providers.is_empty() {
-                        return Ok(vec![ArgumentCompletion {
-                            label: "No available docs providers.".into(),
-                            new_text: String::new(),
-                            after_completion: false.into(),
-                            replace_previous_arguments: false,
-                        }]);
-                    }
-
-                    Ok(providers
-                        .into_iter()
-                        .map(|provider| ArgumentCompletion {
-                            label: provider.to_string().into(),
-                            new_text: provider.to_string(),
-                            after_completion: false.into(),
-                            replace_previous_arguments: false,
-                        })
-                        .collect())
-                }
-                DocsSlashCommandArgs::SearchPackageDocs {
-                    provider,
-                    package,
-                    index,
-                } => {
-                    let store = store?;
-
-                    if index {
-                        // We don't need to hold onto this task, as the `IndexedDocsStore` will hold it
-                        // until it completes.
-                        drop(store.clone().index(package.as_str().into()));
-                    }
-
-                    let suggested_packages = store.clone().suggest_packages().await?;
-                    let search_results = store.search(package).await;
-
-                    let mut items = build_completions(search_results);
-                    let workspace_crate_completions = suggested_packages
-                        .into_iter()
-                        .filter(|package_name| {
-                            !items
-                                .iter()
-                                .any(|item| item.label.text() == package_name.as_ref())
-                        })
-                        .map(|package_name| ArgumentCompletion {
-                            label: format!("{package_name} (unindexed)").into(),
-                            new_text: format!("{package_name}"),
-                            after_completion: true.into(),
-                            replace_previous_arguments: false,
-                        })
-                        .collect::<Vec<_>>();
-                    items.extend(workspace_crate_completions);
-
-                    if items.is_empty() {
-                        return Ok(vec![ArgumentCompletion {
-                            label: format!(
-                                "Enter a {package_term} name.",
-                                package_term = package_term(&provider)
-                            )
-                            .into(),
-                            new_text: provider.to_string(),
-                            after_completion: false.into(),
-                            replace_previous_arguments: false,
-                        }]);
-                    }
-
-                    Ok(items)
-                }
-                DocsSlashCommandArgs::SearchItemDocs { item_path, .. } => {
-                    let store = store?;
-                    let items = store.search(item_path).await;
-                    Ok(build_completions(items))
-                }
-            }
-        })
-    }
-
-    fn run(
-        self: Arc<Self>,
-        arguments: &[String],
-        _context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
-        _context_buffer: BufferSnapshot,
-        _workspace: WeakEntity<Workspace>,
-        _delegate: Option<Arc<dyn LspAdapterDelegate>>,
-        _: &mut Window,
-        cx: &mut App,
-    ) -> Task<SlashCommandResult> {
-        if arguments.is_empty() {
-            return Task::ready(Err(anyhow!("missing an argument")));
-        };
-
-        let args = DocsSlashCommandArgs::parse(arguments);
-        let executor = cx.background_executor().clone();
-        let task = cx.background_spawn({
-            let store = args
-                .provider()
-                .context("no docs provider specified")
-                .and_then(|provider| IndexedDocsStore::try_global(provider, cx));
-            async move {
-                let (provider, key) = match args.clone() {
-                    DocsSlashCommandArgs::NoProvider => bail!("no docs provider specified"),
-                    DocsSlashCommandArgs::SearchPackageDocs {
-                        provider, package, ..
-                    } => (provider, package),
-                    DocsSlashCommandArgs::SearchItemDocs {
-                        provider,
-                        item_path,
-                        ..
-                    } => (provider, item_path),
-                };
-
-                if key.trim().is_empty() {
-                    bail!(
-                        "no {package_term} name provided",
-                        package_term = package_term(&provider)
-                    );
-                }
-
-                let store = store?;
-
-                if let Some(package) = args.package() {
-                    Self::run_just_in_time_indexing(store.clone(), key.clone(), package, executor)
-                        .await;
-                }
-
-                let (text, ranges) = if let Some((prefix, _)) = key.split_once('*') {
-                    let docs = store.load_many_by_prefix(prefix.to_string()).await?;
-
-                    let mut text = String::new();
-                    let mut ranges = Vec::new();
-
-                    for (key, docs) in docs {
-                        let prev_len = text.len();
-
-                        text.push_str(&docs.0);
-                        text.push_str("\n");
-                        ranges.push((key, prev_len..text.len()));
-                        text.push_str("\n");
-                    }
-
-                    (text, ranges)
-                } else {
-                    let item_docs = store.load(key.clone()).await?;
-                    let text = item_docs.to_string();
-                    let range = 0..text.len();
-
-                    (text, vec![(key, range)])
-                };
-
-                anyhow::Ok((provider, text, ranges))
-            }
-        });
-
-        cx.foreground_executor().spawn(async move {
-            let (provider, text, ranges) = task.await?;
-            Ok(SlashCommandOutput {
-                text,
-                sections: ranges
-                    .into_iter()
-                    .map(|(key, range)| SlashCommandOutputSection {
-                        range,
-                        icon: IconName::FileDoc,
-                        label: format!("docs ({provider}): {key}",).into(),
-                        metadata: None,
-                    })
-                    .collect(),
-                run_commands_in_text: false,
-            }
-            .to_event_stream())
-        })
-    }
-}
-
-fn is_item_path_delimiter(char: char) -> bool {
-    !char.is_alphanumeric() && char != '-' && char != '_'
-}
-
-#[derive(Debug, PartialEq, Clone)]
-pub enum DocsSlashCommandArgs {
-    NoProvider,
-    SearchPackageDocs {
-        provider: ProviderId,
-        package: String,
-        index: bool,
-    },
-    SearchItemDocs {
-        provider: ProviderId,
-        package: String,
-        item_path: String,
-    },
-}
-
-impl DocsSlashCommandArgs {
-    pub fn parse(arguments: &[String]) -> Self {
-        let Some(provider) = arguments
-            .get(0)
-            .cloned()
-            .filter(|arg| !arg.trim().is_empty())
-        else {
-            return Self::NoProvider;
-        };
-        let provider = ProviderId(provider.into());
-        let Some(argument) = arguments.get(1) else {
-            return Self::NoProvider;
-        };
-
-        if let Some((package, rest)) = argument.split_once(is_item_path_delimiter) {
-            if rest.trim().is_empty() {
-                Self::SearchPackageDocs {
-                    provider,
-                    package: package.to_owned(),
-                    index: true,
-                }
-            } else {
-                Self::SearchItemDocs {
-                    provider,
-                    package: package.to_owned(),
-                    item_path: argument.to_owned(),
-                }
-            }
-        } else {
-            Self::SearchPackageDocs {
-                provider,
-                package: argument.to_owned(),
-                index: false,
-            }
-        }
-    }
-
-    pub fn provider(&self) -> Option<ProviderId> {
-        match self {
-            Self::NoProvider => None,
-            Self::SearchPackageDocs { provider, .. } | Self::SearchItemDocs { provider, .. } => {
-                Some(provider.clone())
-            }
-        }
-    }
-
-    pub fn package(&self) -> Option<PackageName> {
-        match self {
-            Self::NoProvider => None,
-            Self::SearchPackageDocs { package, .. } | Self::SearchItemDocs { package, .. } => {
-                Some(package.as_str().into())
-            }
-        }
-    }
-}
-
-/// Returns the term used to refer to a package.
-fn package_term(provider: &ProviderId) -> &'static str {
-    if provider == &DocsDotRsProvider::id() || provider == &LocalRustdocProvider::id() {
-        return "crate";
-    }
-
-    "package"
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-
-    #[test]
-    fn test_parse_docs_slash_command_args() {
-        assert_eq!(
-            DocsSlashCommandArgs::parse(&["".to_string()]),
-            DocsSlashCommandArgs::NoProvider
-        );
-        assert_eq!(
-            DocsSlashCommandArgs::parse(&["rustdoc".to_string()]),
-            DocsSlashCommandArgs::NoProvider
-        );
-
-        assert_eq!(
-            DocsSlashCommandArgs::parse(&["rustdoc".to_string(), "".to_string()]),
-            DocsSlashCommandArgs::SearchPackageDocs {
-                provider: ProviderId("rustdoc".into()),
-                package: "".into(),
-                index: false
-            }
-        );
-        assert_eq!(
-            DocsSlashCommandArgs::parse(&["gleam".to_string(), "".to_string()]),
-            DocsSlashCommandArgs::SearchPackageDocs {
-                provider: ProviderId("gleam".into()),
-                package: "".into(),
-                index: false
-            }
-        );
-
-        assert_eq!(
-            DocsSlashCommandArgs::parse(&["rustdoc".to_string(), "gpui".to_string()]),
-            DocsSlashCommandArgs::SearchPackageDocs {
-                provider: ProviderId("rustdoc".into()),
-                package: "gpui".into(),
-                index: false,
-            }
-        );
-        assert_eq!(
-            DocsSlashCommandArgs::parse(&["gleam".to_string(), "gleam_stdlib".to_string()]),
-            DocsSlashCommandArgs::SearchPackageDocs {
-                provider: ProviderId("gleam".into()),
-                package: "gleam_stdlib".into(),
-                index: false
-            }
-        );
-
-        // Adding an item path delimiter indicates we can start indexing.
-        assert_eq!(
-            DocsSlashCommandArgs::parse(&["rustdoc".to_string(), "gpui:".to_string()]),
-            DocsSlashCommandArgs::SearchPackageDocs {
-                provider: ProviderId("rustdoc".into()),
-                package: "gpui".into(),
-                index: true,
-            }
-        );
-        assert_eq!(
-            DocsSlashCommandArgs::parse(&["gleam".to_string(), "gleam_stdlib/".to_string()]),
-            DocsSlashCommandArgs::SearchPackageDocs {
-                provider: ProviderId("gleam".into()),
-                package: "gleam_stdlib".into(),
-                index: true
-            }
-        );
-
-        assert_eq!(
-            DocsSlashCommandArgs::parse(&[
-                "rustdoc".to_string(),
-                "gpui::foo::bar::Baz".to_string()
-            ]),
-            DocsSlashCommandArgs::SearchItemDocs {
-                provider: ProviderId("rustdoc".into()),
-                package: "gpui".into(),
-                item_path: "gpui::foo::bar::Baz".into()
-            }
-        );
-        assert_eq!(
-            DocsSlashCommandArgs::parse(&[
-                "gleam".to_string(),
-                "gleam_stdlib/gleam/int".to_string()
-            ]),
-            DocsSlashCommandArgs::SearchItemDocs {
-                provider: ProviderId("gleam".into()),
-                package: "gleam_stdlib".into(),
-                item_path: "gleam_stdlib/gleam/int".into()
-            }
-        );
-    }
-}

crates/assistant_slash_commands/src/fetch_command.rs 🔗

@@ -112,7 +112,7 @@ impl SlashCommand for FetchSlashCommand {
     }
 
     fn icon(&self) -> IconName {
-        IconName::Globe
+        IconName::ToolWeb
     }
 
     fn menu_text(&self) -> String {
@@ -171,13 +171,13 @@ impl SlashCommand for FetchSlashCommand {
                 text,
                 sections: vec![SlashCommandOutputSection {
                     range,
-                    icon: IconName::Globe,
+                    icon: IconName::ToolWeb,
                     label: format!("fetch {}", url).into(),
                     metadata: None,
                 }],
                 run_commands_in_text: false,
             }
-            .to_event_stream())
+            .into_event_stream())
         })
     }
 }

crates/assistant_slash_commands/src/file_command.rs 🔗

@@ -92,7 +92,7 @@ impl FileSlashCommand {
                         snapshot: worktree.snapshot(),
                         include_ignored: worktree
                             .root_entry()
-                            .map_or(false, |entry| entry.is_ignored),
+                            .is_some_and(|entry| entry.is_ignored),
                         include_root_name: true,
                         candidates: project::Candidates::Entries,
                     }
@@ -223,7 +223,7 @@ fn collect_files(
     cx: &mut App,
 ) -> impl Stream<Item = Result<SlashCommandEvent>> + use<> {
     let Ok(matchers) = glob_inputs
-        .into_iter()
+        .iter()
         .map(|glob_input| {
             custom_path_matcher::PathMatcher::new(&[glob_input.to_owned()])
                 .with_context(|| format!("invalid path {glob_input}"))
@@ -371,7 +371,7 @@ fn collect_files(
                             &mut output,
                         )
                         .log_err();
-                        let mut buffer_events = output.to_event_stream();
+                        let mut buffer_events = output.into_event_stream();
                         while let Some(event) = buffer_events.next().await {
                             events_tx.unbounded_send(event)?;
                         }
@@ -379,7 +379,7 @@ fn collect_files(
                 }
             }
 
-            while let Some(_) = directory_stack.pop() {
+            while directory_stack.pop().is_some() {
                 events_tx.unbounded_send(Ok(SlashCommandEvent::EndSection))?;
             }
         }
@@ -491,7 +491,7 @@ mod custom_path_matcher {
     impl PathMatcher {
         pub fn new(globs: &[String]) -> Result<Self, globset::Error> {
             let globs = globs
-                .into_iter()
+                .iter()
                 .map(|glob| Glob::new(&SanitizedPath::from(glob).to_glob_string()))
                 .collect::<Result<Vec<_>, _>>()?;
             let sources = globs.iter().map(|glob| glob.glob().to_owned()).collect();
@@ -536,7 +536,7 @@ mod custom_path_matcher {
             let path_str = path.to_string_lossy();
             let separator = std::path::MAIN_SEPARATOR_STR;
             if path_str.ends_with(separator) {
-                return false;
+                false
             } else {
                 self.glob.is_match(path_str.to_string() + separator)
             }

crates/assistant_slash_commands/src/prompt_command.rs 🔗

@@ -80,7 +80,7 @@ impl SlashCommand for PromptSlashCommand {
         };
 
         let store = PromptStore::global(cx);
-        let title = SharedString::from(title.clone());
+        let title = SharedString::from(title);
         let prompt = cx.spawn({
             let title = title.clone();
             async move |cx| {
@@ -117,7 +117,7 @@ impl SlashCommand for PromptSlashCommand {
                 }],
                 run_commands_in_text: true,
             }
-            .to_event_stream())
+            .into_event_stream())
         })
     }
 }

crates/assistant_slash_commands/src/tab_command.rs 🔗

@@ -157,7 +157,7 @@ impl SlashCommand for TabSlashCommand {
             for (full_path, buffer, _) in tab_items_search.await? {
                 append_buffer_to_output(&buffer, full_path.as_deref(), &mut output).log_err();
             }
-            Ok(output.to_event_stream())
+            Ok(output.into_event_stream())
         })
     }
 }
@@ -195,16 +195,14 @@ fn tab_items_for_queries(
                     }
 
                     for editor in workspace.items_of_type::<Editor>(cx) {
-                        if let Some(buffer) = editor.read(cx).buffer().read(cx).as_singleton() {
-                            if let Some(timestamp) =
+                        if let Some(buffer) = editor.read(cx).buffer().read(cx).as_singleton()
+                            && let Some(timestamp) =
                                 timestamps_by_entity_id.get(&editor.entity_id())
-                            {
-                                if visited_buffers.insert(buffer.read(cx).remote_id()) {
-                                    let snapshot = buffer.read(cx).snapshot();
-                                    let full_path = snapshot.resolve_file_path(cx, true);
-                                    open_buffers.push((full_path, snapshot, *timestamp));
-                                }
-                            }
+                            && visited_buffers.insert(buffer.read(cx).remote_id())
+                        {
+                            let snapshot = buffer.read(cx).snapshot();
+                            let full_path = snapshot.resolve_file_path(cx, true);
+                            open_buffers.push((full_path, snapshot, *timestamp));
                         }
                     }
 

crates/assistant_tool/Cargo.toml 🔗

@@ -12,12 +12,10 @@ workspace = true
 path = "src/assistant_tool.rs"
 
 [dependencies]
+action_log.workspace = true
 anyhow.workspace = true
-buffer_diff.workspace = true
-clock.workspace = true
 collections.workspace = true
 derive_more.workspace = true
-futures.workspace = true
 gpui.workspace = true
 icons.workspace = true
 language.workspace = true
@@ -30,7 +28,6 @@ serde.workspace = true
 serde_json.workspace = true
 text.workspace = true
 util.workspace = true
-watch.workspace = true
 workspace.workspace = true
 workspace-hack.workspace = true
 

crates/assistant_tool/src/assistant_tool.rs 🔗

@@ -1,4 +1,3 @@
-mod action_log;
 pub mod outline;
 mod tool_registry;
 mod tool_schema;
@@ -10,6 +9,7 @@ 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;
@@ -25,7 +25,6 @@ use language_model::LanguageModelToolSchemaFormat;
 use project::Project;
 use workspace::Workspace;
 
-pub use crate::action_log::*;
 pub use crate::tool_registry::*;
 pub use crate::tool_schema::*;
 pub use crate::tool_working_set::*;

crates/assistant_tool/src/outline.rs 🔗

@@ -1,4 +1,4 @@
-use crate::ActionLog;
+use action_log::ActionLog;
 use anyhow::{Context as _, Result};
 use gpui::{AsyncApp, Entity};
 use language::{OutlineItem, ParseStatus};

crates/assistant_tool/src/tool_schema.rs 🔗

@@ -24,16 +24,16 @@ pub fn adapt_schema_to_format(
 fn preprocess_json_schema(json: &mut Value) -> Result<()> {
     // `additionalProperties` defaults to `false` unless explicitly specified.
     // This prevents models from hallucinating tool parameters.
-    if let Value::Object(obj) = json {
-        if matches!(obj.get("type"), Some(Value::String(s)) if s == "object") {
-            if !obj.contains_key("additionalProperties") {
-                obj.insert("additionalProperties".to_string(), Value::Bool(false));
-            }
+    if let Value::Object(obj) = json
+        && matches!(obj.get("type"), Some(Value::String(s)) if s == "object")
+    {
+        if !obj.contains_key("additionalProperties") {
+            obj.insert("additionalProperties".to_string(), Value::Bool(false));
+        }
 
-            // OpenAI API requires non-missing `properties`
-            if !obj.contains_key("properties") {
-                obj.insert("properties".to_string(), Value::Object(Default::default()));
-            }
+        // OpenAI API requires non-missing `properties`
+        if !obj.contains_key("properties") {
+            obj.insert("properties".to_string(), Value::Object(Default::default()));
         }
     }
     Ok(())
@@ -59,10 +59,10 @@ fn adapt_to_json_schema_subset(json: &mut Value) -> Result<()> {
             ("optional", |value| value.is_boolean()),
         ];
         for (key, predicate) in KEYS_TO_REMOVE {
-            if let Some(value) = obj.get(key) {
-                if predicate(value) {
-                    obj.remove(key);
-                }
+            if let Some(value) = obj.get(key)
+                && predicate(value)
+            {
+                obj.remove(key);
             }
         }
 
@@ -77,12 +77,12 @@ fn adapt_to_json_schema_subset(json: &mut Value) -> Result<()> {
         }
 
         // Handle oneOf -> anyOf conversion
-        if let Some(subschemas) = obj.get_mut("oneOf") {
-            if subschemas.is_array() {
-                let subschemas_clone = subschemas.clone();
-                obj.remove("oneOf");
-                obj.insert("anyOf".to_string(), subschemas_clone);
-            }
+        if let Some(subschemas) = obj.get_mut("oneOf")
+            && subschemas.is_array()
+        {
+            let subschemas_clone = subschemas.clone();
+            obj.remove("oneOf");
+            obj.insert("anyOf".to_string(), subschemas_clone);
         }
 
         // Recursively process all nested objects and arrays

crates/assistant_tool/src/tool_working_set.rs 🔗

@@ -156,13 +156,13 @@ fn resolve_context_server_tool_name_conflicts(
 
     if duplicated_tool_names.is_empty() {
         return context_server_tools
-            .into_iter()
+            .iter()
             .map(|tool| (resolve_tool_name(tool).into(), tool.clone()))
             .collect();
     }
 
     context_server_tools
-        .into_iter()
+        .iter()
         .filter_map(|tool| {
             let mut tool_name = resolve_tool_name(tool);
             if !duplicated_tool_names.contains(&tool_name) {

crates/assistant_tools/Cargo.toml 🔗

@@ -15,6 +15,7 @@ path = "src/assistant_tools.rs"
 eval = []
 
 [dependencies]
+action_log.workspace = true
 agent_settings.workspace = true
 anyhow.workspace = true
 assistant_tool.workspace = true

crates/assistant_tools/src/assistant_tools.rs 🔗

@@ -2,7 +2,7 @@ mod copy_path_tool;
 mod create_directory_tool;
 mod delete_path_tool;
 mod diagnostics_tool;
-mod edit_agent;
+pub mod edit_agent;
 mod edit_file_tool;
 mod fetch_tool;
 mod find_path_tool;
@@ -14,7 +14,7 @@ mod open_tool;
 mod project_notifications_tool;
 mod read_file_tool;
 mod schema;
-mod templates;
+pub mod templates;
 mod terminal_tool;
 mod thinking_tool;
 mod ui;
@@ -72,11 +72,10 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) {
     register_web_search_tool(&LanguageModelRegistry::global(cx), cx);
     cx.subscribe(
         &LanguageModelRegistry::global(cx),
-        move |registry, event, cx| match event {
-            language_model::Event::DefaultModelChanged => {
+        move |registry, event, cx| {
+            if let language_model::Event::DefaultModelChanged = event {
                 register_web_search_tool(&registry, cx);
             }
-            _ => {}
         },
     )
     .detach();
@@ -86,7 +85,7 @@ fn register_web_search_tool(registry: &Entity<LanguageModelRegistry>, cx: &mut A
     let using_zed_provider = registry
         .read(cx)
         .default_model()
-        .map_or(false, |default| default.is_provided_by_zed());
+        .is_some_and(|default| default.is_provided_by_zed());
     if using_zed_provider {
         ToolRegistry::global(cx).register_tool(WebSearchTool);
     } else {

crates/assistant_tools/src/copy_path_tool.rs 🔗

@@ -1,6 +1,7 @@
 use crate::schema::json_schema_for;
+use action_log::ActionLog;
 use anyhow::{Context as _, Result, anyhow};
-use assistant_tool::{ActionLog, Tool, ToolResult};
+use assistant_tool::{Tool, ToolResult};
 use gpui::AnyWindowHandle;
 use gpui::{App, AppContext, Entity, Task};
 use language_model::LanguageModel;

crates/assistant_tools/src/create_directory_tool.rs 🔗

@@ -1,6 +1,7 @@
 use crate::schema::json_schema_for;
+use action_log::ActionLog;
 use anyhow::{Context as _, Result, anyhow};
-use assistant_tool::{ActionLog, Tool, ToolResult};
+use assistant_tool::{Tool, ToolResult};
 use gpui::AnyWindowHandle;
 use gpui::{App, Entity, Task};
 use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};

crates/assistant_tools/src/delete_path_tool.rs 🔗

@@ -1,6 +1,7 @@
 use crate::schema::json_schema_for;
+use action_log::ActionLog;
 use anyhow::{Context as _, Result, anyhow};
-use assistant_tool::{ActionLog, Tool, ToolResult};
+use assistant_tool::{Tool, ToolResult};
 use futures::{SinkExt, StreamExt, channel::mpsc};
 use gpui::{AnyWindowHandle, App, AppContext, Entity, Task};
 use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};

crates/assistant_tools/src/diagnostics_tool.rs 🔗

@@ -1,6 +1,7 @@
 use crate::schema::json_schema_for;
+use action_log::ActionLog;
 use anyhow::{Result, anyhow};
-use assistant_tool::{ActionLog, Tool, ToolResult};
+use assistant_tool::{Tool, ToolResult};
 use gpui::{AnyWindowHandle, App, Entity, Task};
 use language::{DiagnosticSeverity, OffsetRangeExt};
 use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
@@ -85,7 +86,7 @@ impl Tool for DiagnosticsTool {
         input: serde_json::Value,
         _request: Arc<LanguageModelRequest>,
         project: Entity<Project>,
-        action_log: Entity<ActionLog>,
+        _action_log: Entity<ActionLog>,
         _model: Arc<dyn LanguageModel>,
         _window: Option<AnyWindowHandle>,
         cx: &mut App,
@@ -158,10 +159,6 @@ impl Tool for DiagnosticsTool {
                     }
                 }
 
-                action_log.update(cx, |action_log, _cx| {
-                    action_log.checked_project_diagnostics();
-                });
-
                 if has_diagnostics {
                     Task::ready(Ok(output.into())).into()
                 } else {

crates/assistant_tools/src/edit_agent.rs 🔗

@@ -5,8 +5,8 @@ mod evals;
 mod streaming_fuzzy_matcher;
 
 use crate::{Template, Templates};
+use action_log::ActionLog;
 use anyhow::Result;
-use assistant_tool::ActionLog;
 use cloud_llm_client::CompletionIntent;
 use create_file_parser::{CreateFileParser, CreateFileParserEvent};
 pub use edit_parser::EditFormat;
@@ -29,7 +29,6 @@ use serde::{Deserialize, Serialize};
 use std::{cmp, iter, mem, ops::Range, path::PathBuf, pin::Pin, sync::Arc, task::Poll};
 use streaming_diff::{CharOperation, StreamingDiff};
 use streaming_fuzzy_matcher::StreamingFuzzyMatcher;
-use util::debug_panic;
 
 #[derive(Serialize)]
 struct CreateFilePromptTemplate {
@@ -66,7 +65,7 @@ pub enum EditAgentOutputEvent {
     ResolvingEditRange(Range<Anchor>),
     UnresolvedEditRange,
     AmbiguousEditRange(Vec<Range<usize>>),
-    Edited,
+    Edited(Range<Anchor>),
 }
 
 #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
@@ -179,7 +178,9 @@ impl EditAgent {
                 )
             });
             output_events_tx
-                .unbounded_send(EditAgentOutputEvent::Edited)
+                .unbounded_send(EditAgentOutputEvent::Edited(
+                    language::Anchor::MIN..language::Anchor::MAX,
+                ))
                 .ok();
         })?;
 
@@ -201,7 +202,9 @@ impl EditAgent {
                         });
                     })?;
                     output_events_tx
-                        .unbounded_send(EditAgentOutputEvent::Edited)
+                        .unbounded_send(EditAgentOutputEvent::Edited(
+                            language::Anchor::MIN..language::Anchor::MAX,
+                        ))
                         .ok();
                 }
             }
@@ -337,8 +340,8 @@ impl EditAgent {
                 // Edit the buffer and report edits to the action log as part of the
                 // same effect cycle, otherwise the edit will be reported as if the
                 // user made it.
-                cx.update(|cx| {
-                    let max_edit_end = buffer.update(cx, |buffer, cx| {
+                let (min_edit_start, max_edit_end) = cx.update(|cx| {
+                    let (min_edit_start, max_edit_end) = buffer.update(cx, |buffer, cx| {
                         buffer.edit(edits.iter().cloned(), None, cx);
                         let max_edit_end = buffer
                             .summaries_for_anchors::<Point, _>(
@@ -346,7 +349,16 @@ impl EditAgent {
                             )
                             .max()
                             .unwrap();
-                        buffer.anchor_before(max_edit_end)
+                        let min_edit_start = buffer
+                            .summaries_for_anchors::<Point, _>(
+                                edits.iter().map(|(range, _)| &range.start),
+                            )
+                            .min()
+                            .unwrap();
+                        (
+                            buffer.anchor_after(min_edit_start),
+                            buffer.anchor_before(max_edit_end),
+                        )
                     });
                     self.action_log
                         .update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
@@ -359,9 +371,10 @@ impl EditAgent {
                             cx,
                         );
                     });
+                    (min_edit_start, max_edit_end)
                 })?;
                 output_events
-                    .unbounded_send(EditAgentOutputEvent::Edited)
+                    .unbounded_send(EditAgentOutputEvent::Edited(min_edit_start..max_edit_end))
                     .ok();
             }
 
@@ -659,34 +672,30 @@ impl EditAgent {
         cx: &mut AsyncApp,
     ) -> Result<BoxStream<'static, Result<String, LanguageModelCompletionError>>> {
         let mut messages_iter = conversation.messages.iter_mut();
-        if let Some(last_message) = messages_iter.next_back() {
-            if last_message.role == Role::Assistant {
-                let old_content_len = last_message.content.len();
-                last_message
-                    .content
-                    .retain(|content| !matches!(content, MessageContent::ToolUse(_)));
-                let new_content_len = last_message.content.len();
-
-                // We just removed pending tool uses from the content of the
-                // last message, so it doesn't make sense to cache it anymore
-                // (e.g., the message will look very different on the next
-                // request). Thus, we move the flag to the message prior to it,
-                // as it will still be a valid prefix of the conversation.
-                if old_content_len != new_content_len && last_message.cache {
-                    if let Some(prev_message) = messages_iter.next_back() {
-                        last_message.cache = false;
-                        prev_message.cache = true;
-                    }
-                }
+        if let Some(last_message) = messages_iter.next_back()
+            && last_message.role == Role::Assistant
+        {
+            let old_content_len = last_message.content.len();
+            last_message
+                .content
+                .retain(|content| !matches!(content, MessageContent::ToolUse(_)));
+            let new_content_len = last_message.content.len();
+
+            // We just removed pending tool uses from the content of the
+            // last message, so it doesn't make sense to cache it anymore
+            // (e.g., the message will look very different on the next
+            // request). Thus, we move the flag to the message prior to it,
+            // as it will still be a valid prefix of the conversation.
+            if old_content_len != new_content_len
+                && last_message.cache
+                && let Some(prev_message) = messages_iter.next_back()
+            {
+                last_message.cache = false;
+                prev_message.cache = true;
+            }
 
-                if last_message.content.is_empty() {
-                    conversation.messages.pop();
-                }
-            } else {
-                debug_panic!(
-                    "Last message must be an Assistant tool calling! Got {:?}",
-                    last_message.content
-                );
+            if last_message.content.is_empty() {
+                conversation.messages.pop();
             }
         }
 
@@ -761,6 +770,7 @@ mod tests {
     use gpui::{AppContext, TestAppContext};
     use indoc::indoc;
     use language_model::fake_provider::FakeLanguageModel;
+    use pretty_assertions::assert_matches;
     use project::{AgentLocation, Project};
     use rand::prelude::*;
     use rand::rngs::StdRng;
@@ -998,7 +1008,10 @@ mod tests {
 
         model.send_last_completion_stream_text_chunk("<new_text>abX");
         cx.run_until_parked();
-        assert_eq!(drain_events(&mut events), [EditAgentOutputEvent::Edited]);
+        assert_matches!(
+            drain_events(&mut events).as_slice(),
+            [EditAgentOutputEvent::Edited(_)]
+        );
         assert_eq!(
             buffer.read_with(cx, |buffer, _| buffer.snapshot().text()),
             "abXc\ndef\nghi\njkl"
@@ -1013,7 +1026,10 @@ mod tests {
 
         model.send_last_completion_stream_text_chunk("cY");
         cx.run_until_parked();
-        assert_eq!(drain_events(&mut events), [EditAgentOutputEvent::Edited]);
+        assert_matches!(
+            drain_events(&mut events).as_slice(),
+            [EditAgentOutputEvent::Edited { .. }]
+        );
         assert_eq!(
             buffer.read_with(cx, |buffer, _| buffer.snapshot().text()),
             "abXcY\ndef\nghi\njkl"
@@ -1124,9 +1140,9 @@ mod tests {
 
         model.send_last_completion_stream_text_chunk("GHI</new_text>");
         cx.run_until_parked();
-        assert_eq!(
-            drain_events(&mut events),
-            vec![EditAgentOutputEvent::Edited]
+        assert_matches!(
+            drain_events(&mut events).as_slice(),
+            [EditAgentOutputEvent::Edited { .. }]
         );
         assert_eq!(
             buffer.read_with(cx, |buffer, _| buffer.snapshot().text()),
@@ -1171,9 +1187,9 @@ mod tests {
         );
 
         cx.run_until_parked();
-        assert_eq!(
-            drain_events(&mut events),
-            vec![EditAgentOutputEvent::Edited]
+        assert_matches!(
+            drain_events(&mut events).as_slice(),
+            [EditAgentOutputEvent::Edited(_)]
         );
         assert_eq!(
             buffer.read_with(cx, |buffer, _| buffer.snapshot().text()),
@@ -1189,9 +1205,9 @@ mod tests {
 
         chunks_tx.unbounded_send("```\njkl\n").unwrap();
         cx.run_until_parked();
-        assert_eq!(
-            drain_events(&mut events),
-            vec![EditAgentOutputEvent::Edited]
+        assert_matches!(
+            drain_events(&mut events).as_slice(),
+            [EditAgentOutputEvent::Edited { .. }]
         );
         assert_eq!(
             buffer.read_with(cx, |buffer, _| buffer.snapshot().text()),
@@ -1207,9 +1223,9 @@ mod tests {
 
         chunks_tx.unbounded_send("mno\n").unwrap();
         cx.run_until_parked();
-        assert_eq!(
-            drain_events(&mut events),
-            vec![EditAgentOutputEvent::Edited]
+        assert_matches!(
+            drain_events(&mut events).as_slice(),
+            [EditAgentOutputEvent::Edited { .. }]
         );
         assert_eq!(
             buffer.read_with(cx, |buffer, _| buffer.snapshot().text()),
@@ -1225,9 +1241,9 @@ mod tests {
 
         chunks_tx.unbounded_send("pqr\n```").unwrap();
         cx.run_until_parked();
-        assert_eq!(
-            drain_events(&mut events),
-            vec![EditAgentOutputEvent::Edited]
+        assert_matches!(
+            drain_events(&mut events).as_slice(),
+            [EditAgentOutputEvent::Edited(_)],
         );
         assert_eq!(
             buffer.read_with(cx, |buffer, _| buffer.snapshot().text()),

crates/assistant_tools/src/edit_agent/create_file_parser.rs 🔗

@@ -1,10 +1,11 @@
+use std::sync::OnceLock;
+
 use regex::Regex;
 use smallvec::SmallVec;
-use std::cell::LazyCell;
 use util::debug_panic;
 
-const START_MARKER: LazyCell<Regex> = LazyCell::new(|| Regex::new(r"\n?```\S*\n").unwrap());
-const END_MARKER: LazyCell<Regex> = LazyCell::new(|| Regex::new(r"(^|\n)```\s*$").unwrap());
+static START_MARKER: OnceLock<Regex> = OnceLock::new();
+static END_MARKER: OnceLock<Regex> = OnceLock::new();
 
 #[derive(Debug)]
 pub enum CreateFileParserEvent {
@@ -43,10 +44,12 @@ impl CreateFileParser {
         self.buffer.push_str(chunk);
 
         let mut edit_events = SmallVec::new();
+        let start_marker_regex = START_MARKER.get_or_init(|| Regex::new(r"\n?```\S*\n").unwrap());
+        let end_marker_regex = END_MARKER.get_or_init(|| Regex::new(r"(^|\n)```\s*$").unwrap());
         loop {
             match &mut self.state {
                 ParserState::Pending => {
-                    if let Some(m) = START_MARKER.find(&self.buffer) {
+                    if let Some(m) = start_marker_regex.find(&self.buffer) {
                         self.buffer.drain(..m.end());
                         self.state = ParserState::WithinText;
                     } else {
@@ -65,7 +68,7 @@ impl CreateFileParser {
                     break;
                 }
                 ParserState::Finishing => {
-                    if let Some(m) = END_MARKER.find(&self.buffer) {
+                    if let Some(m) = end_marker_regex.find(&self.buffer) {
                         self.buffer.drain(m.start()..);
                     }
                     if !self.buffer.is_empty() {

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

@@ -1153,8 +1153,7 @@ impl EvalInput {
             .expect("Conversation must end with an edit_file tool use")
             .clone();
 
-        let edit_file_input: EditFileToolInput =
-            serde_json::from_value(tool_use.input.clone()).unwrap();
+        let edit_file_input: EditFileToolInput = serde_json::from_value(tool_use.input).unwrap();
 
         EvalInput {
             conversation,
@@ -1283,14 +1282,14 @@ impl EvalAssertion {
 
             // Parse the score from the response
             let re = regex::Regex::new(r"<score>(\d+)</score>").unwrap();
-            if let Some(captures) = re.captures(&output) {
-                if let Some(score_match) = captures.get(1) {
-                    let score = score_match.as_str().parse().unwrap_or(0);
-                    return Ok(EvalAssertionOutcome {
-                        score,
-                        message: Some(output),
-                    });
-                }
+            if let Some(captures) = re.captures(&output)
+                && let Some(score_match) = captures.get(1)
+            {
+                let score = score_match.as_str().parse().unwrap_or(0);
+                return Ok(EvalAssertionOutcome {
+                    score,
+                    message: Some(output),
+                });
             }
 
             anyhow::bail!("No score found in response. Raw output: {output}");
@@ -1460,7 +1459,7 @@ impl EditAgentTest {
     async fn new(cx: &mut TestAppContext) -> Self {
         cx.executor().allow_parking();
 
-        let fs = FakeFs::new(cx.executor().clone());
+        let fs = FakeFs::new(cx.executor());
         cx.update(|cx| {
             settings::init(cx);
             gpui_tokio::init(cx);
@@ -1475,7 +1474,7 @@ impl EditAgentTest {
             Project::init_settings(cx);
             language::init(cx);
             language_model::init(client.clone(), cx);
-            language_models::init(user_store.clone(), client.clone(), cx);
+            language_models::init(user_store, client.clone(), cx);
             crate::init(client.http_client(), cx);
         });
 
@@ -1586,7 +1585,7 @@ impl EditAgentTest {
         let has_system_prompt = eval
             .conversation
             .first()
-            .map_or(false, |msg| msg.role == Role::System);
+            .is_some_and(|msg| msg.role == Role::System);
         let messages = if has_system_prompt {
             eval.conversation
         } else {

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

@@ -319,7 +319,7 @@ mod tests {
         );
         let snapshot = buffer.snapshot();
 
-        let mut finder = StreamingFuzzyMatcher::new(snapshot.clone());
+        let mut finder = StreamingFuzzyMatcher::new(snapshot);
         assert_eq!(push(&mut finder, ""), None);
         assert_eq!(finish(finder), None);
     }
@@ -333,7 +333,7 @@ mod tests {
         );
         let snapshot = buffer.snapshot();
 
-        let mut finder = StreamingFuzzyMatcher::new(snapshot.clone());
+        let mut finder = StreamingFuzzyMatcher::new(snapshot);
 
         // Push partial query
         assert_eq!(push(&mut finder, "This"), None);
@@ -365,7 +365,7 @@ mod tests {
         );
         let snapshot = buffer.snapshot();
 
-        let mut finder = StreamingFuzzyMatcher::new(snapshot.clone());
+        let mut finder = StreamingFuzzyMatcher::new(snapshot);
 
         // Push a fuzzy query that should match the first function
         assert_eq!(
@@ -391,7 +391,7 @@ mod tests {
         );
         let snapshot = buffer.snapshot();
 
-        let mut finder = StreamingFuzzyMatcher::new(snapshot.clone());
+        let mut finder = StreamingFuzzyMatcher::new(snapshot);
 
         // No match initially
         assert_eq!(push(&mut finder, "Lin"), None);
@@ -420,7 +420,7 @@ mod tests {
         );
         let snapshot = buffer.snapshot();
 
-        let mut finder = StreamingFuzzyMatcher::new(snapshot.clone());
+        let mut finder = StreamingFuzzyMatcher::new(snapshot);
 
         // Push text in small chunks across line boundaries
         assert_eq!(push(&mut finder, "jumps "), None); // No newline yet
@@ -458,7 +458,7 @@ mod tests {
         );
         let snapshot = buffer.snapshot();
 
-        let mut finder = StreamingFuzzyMatcher::new(snapshot.clone());
+        let mut finder = StreamingFuzzyMatcher::new(snapshot);
 
         assert_eq!(
             push(&mut finder, "impl Debug for User {\n"),
@@ -711,7 +711,7 @@ mod tests {
             "Expected to match `second_function` based on the line hint"
         );
 
-        let mut matcher = StreamingFuzzyMatcher::new(snapshot.clone());
+        let mut matcher = StreamingFuzzyMatcher::new(snapshot);
         matcher.push(query, None);
         matcher.finish();
         let best_match = matcher.select_best_match();
@@ -727,7 +727,7 @@ mod tests {
         let buffer = TextBuffer::new(0, BufferId::new(1).unwrap(), text.clone());
         let snapshot = buffer.snapshot();
 
-        let mut matcher = StreamingFuzzyMatcher::new(snapshot.clone());
+        let mut matcher = StreamingFuzzyMatcher::new(snapshot);
 
         // Split query into random chunks
         let chunks = to_random_chunks(rng, query);
@@ -794,10 +794,8 @@ mod tests {
     fn finish(mut finder: StreamingFuzzyMatcher) -> Option<String> {
         let snapshot = finder.snapshot.clone();
         let matches = finder.finish();
-        if let Some(range) = matches.first() {
-            Some(snapshot.text_for_range(range.clone()).collect::<String>())
-        } else {
-            None
-        }
+        matches
+            .first()
+            .map(|range| snapshot.text_for_range(range.clone()).collect::<String>())
     }
 }

crates/assistant_tools/src/edit_file_tool.rs 🔗

@@ -4,11 +4,11 @@ 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::{
-    ActionLog, AnyToolCard, Tool, ToolCard, ToolResult, ToolResultContent, ToolResultOutput,
-    ToolUseStatus,
+    AnyToolCard, Tool, ToolCard, ToolResult, ToolResultContent, ToolResultOutput, ToolUseStatus,
 };
 use buffer_diff::{BufferDiff, BufferDiffSnapshot};
 use editor::{Editor, EditorMode, MinimapVisibility, MultiBuffer, PathKey};
@@ -155,10 +155,10 @@ impl Tool for EditFileTool {
 
         // 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) {
-            if canonical_path.starts_with(paths::config_dir()) {
-                return true;
-            }
+        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
@@ -199,10 +199,10 @@ impl Tool for EditFileTool {
                     .any(|c| c.as_os_str() == local_settings_folder.as_os_str())
                 {
                     description.push_str(" (local settings)");
-                } else if let Ok(canonical_path) = std::fs::canonicalize(&input.path) {
-                    if canonical_path.starts_with(paths::config_dir()) {
-                        description.push_str(" (global 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
@@ -307,7 +307,7 @@ impl Tool for EditFileTool {
             let mut ambiguous_ranges = Vec::new();
             while let Some(event) = events.next().await {
                 match event {
-                    EditAgentOutputEvent::Edited => {
+                    EditAgentOutputEvent::Edited { .. } => {
                         if let Some(card) = card_clone.as_ref() {
                             card.update(cx, |card, cx| card.update_diff(cx))?;
                         }
@@ -376,7 +376,7 @@ impl Tool for EditFileTool {
 
             let output = EditFileToolOutput {
                 original_path: project_path.path.to_path_buf(),
-                new_text: new_text.clone(),
+                new_text,
                 old_text,
                 raw_output: Some(agent_output),
             };
@@ -536,7 +536,7 @@ fn resolve_path(
 
             let parent_entry = parent_project_path
                 .as_ref()
-                .and_then(|path| project.entry_for_path(&path, cx))
+                .and_then(|path| project.entry_for_path(path, cx))
                 .context("Can't create file: parent directory doesn't exist")?;
 
             anyhow::ensure!(
@@ -643,7 +643,7 @@ impl EditFileToolCard {
             diff
         });
 
-        self.buffer = Some(buffer.clone());
+        self.buffer = Some(buffer);
         self.base_text = Some(base_text.into());
         self.buffer_diff = Some(buffer_diff.clone());
 
@@ -723,13 +723,13 @@ impl EditFileToolCard {
         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))
+            .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)),
+                .map(|range| range.to_point(buffer)),
         );
         ranges.sort_unstable_by_key(|range| (range.start, Reverse(range.end)));
 
@@ -776,7 +776,6 @@ impl EditFileToolCard {
 
         let buffer_diff = cx.spawn({
             let buffer = buffer.clone();
-            let language_registry = language_registry.clone();
             async move |_this, cx| {
                 build_buffer_diff(base_text, &buffer, &language_registry, cx).await
             }
@@ -857,13 +856,12 @@ impl ToolCard for EditFileToolCard {
                     )
                     .child(
                         Icon::new(IconName::ArrowUpRight)
-                            .size(IconSize::XSmall)
+                            .size(IconSize::Small)
                             .color(Color::Ignored),
                     ),
             )
             .on_click({
                 let path = self.path.clone();
-                let workspace = workspace.clone();
                 move |_, window, cx| {
                     workspace
                         .update(cx, {
@@ -1356,8 +1354,7 @@ mod tests {
             mode: mode.clone(),
         };
 
-        let result = cx.update(|cx| resolve_path(&input, project, cx));
-        result
+        cx.update(|cx| resolve_path(&input, project, cx))
     }
 
     fn assert_resolved_path_eq(path: anyhow::Result<ProjectPath>, expected: &str) {

crates/assistant_tools/src/fetch_tool.rs 🔗

@@ -3,8 +3,9 @@ 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::{ActionLog, Tool, ToolResult};
+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};

crates/assistant_tools/src/find_path_tool.rs 🔗

@@ -1,7 +1,8 @@
 use crate::{schema::json_schema_for, ui::ToolCallCardHeader};
+use action_log::ActionLog;
 use anyhow::{Result, anyhow};
 use assistant_tool::{
-    ActionLog, Tool, ToolCard, ToolResult, ToolResultContent, ToolResultOutput, ToolUseStatus,
+    Tool, ToolCard, ToolResult, ToolResultContent, ToolResultOutput, ToolUseStatus,
 };
 use editor::Editor;
 use futures::channel::oneshot::{self, Receiver};
@@ -233,7 +234,7 @@ impl ToolCard for FindPathToolCard {
         workspace: WeakEntity<Workspace>,
         cx: &mut Context<Self>,
     ) -> impl IntoElement {
-        let matches_label: SharedString = if self.paths.len() == 0 {
+        let matches_label: SharedString = if self.paths.is_empty() {
             "No matches".into()
         } else if self.paths.len() == 1 {
             "1 match".into()
@@ -257,7 +258,7 @@ impl ToolCard for FindPathToolCard {
 
                         Button::new(("path", index), button_label)
                             .icon(IconName::ArrowUpRight)
-                            .icon_size(IconSize::XSmall)
+                            .icon_size(IconSize::Small)
                             .icon_position(IconPosition::End)
                             .label_size(LabelSize::Small)
                             .color(Color::Muted)

crates/assistant_tools/src/grep_tool.rs 🔗

@@ -1,6 +1,7 @@
 use crate::schema::json_schema_for;
+use action_log::ActionLog;
 use anyhow::{Result, anyhow};
-use assistant_tool::{ActionLog, Tool, ToolResult};
+use assistant_tool::{Tool, ToolResult};
 use futures::StreamExt;
 use gpui::{AnyWindowHandle, App, Entity, Task};
 use language::{OffsetRangeExt, ParseStatus, Point};
@@ -187,15 +188,14 @@ impl Tool for GrepTool {
                 // 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)
-                }) {
-                    if cx.update(|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?;
@@ -283,12 +283,11 @@ impl Tool for GrepTool {
                     output.extend(snapshot.text_for_range(range));
                     output.push_str("\n```\n");
 
-                    if let Some(ancestor_range) = ancestor_range {
-                        if end_row < ancestor_range.end.row {
+                    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;
                 }
@@ -328,7 +327,7 @@ mod tests {
         init_test(cx);
         cx.executor().allow_parking();
 
-        let fs = FakeFs::new(cx.executor().clone());
+        let fs = FakeFs::new(cx.executor());
         fs.insert_tree(
             path!("/root"),
             serde_json::json!({
@@ -416,7 +415,7 @@ mod tests {
         init_test(cx);
         cx.executor().allow_parking();
 
-        let fs = FakeFs::new(cx.executor().clone());
+        let fs = FakeFs::new(cx.executor());
         fs.insert_tree(
             path!("/root"),
             serde_json::json!({
@@ -495,7 +494,7 @@ mod tests {
         init_test(cx);
         cx.executor().allow_parking();
 
-        let fs = FakeFs::new(cx.executor().clone());
+        let fs = FakeFs::new(cx.executor());
 
         // Create test file with syntax structures
         fs.insert_tree(
@@ -893,7 +892,7 @@ mod tests {
             })
             .await;
         let results = result.unwrap();
-        let paths = extract_paths_from_results(&results.content.as_str().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"
@@ -919,7 +918,7 @@ mod tests {
             })
             .await;
         let results = result.unwrap();
-        let paths = extract_paths_from_results(&results.content.as_str().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"
@@ -945,7 +944,7 @@ mod tests {
             })
             .await;
         let results = result.unwrap();
-        let paths = extract_paths_from_results(&results.content.as_str().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)"
@@ -970,7 +969,7 @@ mod tests {
             })
             .await;
         let results = result.unwrap();
-        let paths = extract_paths_from_results(&results.content.as_str().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)"
@@ -996,7 +995,7 @@ mod tests {
             })
             .await;
         let results = result.unwrap();
-        let paths = extract_paths_from_results(&results.content.as_str().unwrap());
+        let paths = extract_paths_from_results(results.content.as_str().unwrap());
         assert!(
             paths.is_empty(),
             "grep_tool should not search .mysecrets (private_files)"
@@ -1021,7 +1020,7 @@ mod tests {
             })
             .await;
         let results = result.unwrap();
-        let paths = extract_paths_from_results(&results.content.as_str().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)"
@@ -1046,7 +1045,7 @@ mod tests {
             })
             .await;
         let results = result.unwrap();
-        let paths = extract_paths_from_results(&results.content.as_str().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)"
@@ -1072,7 +1071,7 @@ mod tests {
             })
             .await;
         let results = result.unwrap();
-        let paths = extract_paths_from_results(&results.content.as_str().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"
@@ -1099,7 +1098,7 @@ mod tests {
             })
             .await;
         let results = result.unwrap();
-        let paths = extract_paths_from_results(&results.content.as_str().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"
@@ -1205,7 +1204,7 @@ mod tests {
             .unwrap();
 
         let content = result.content.as_str().unwrap();
-        let paths = extract_paths_from_results(&content);
+        let paths = extract_paths_from_results(content);
 
         // Should find matches in non-private files
         assert!(
@@ -1270,7 +1269,7 @@ mod tests {
             .unwrap();
 
         let content = result.content.as_str().unwrap();
-        let paths = extract_paths_from_results(&content);
+        let paths = extract_paths_from_results(content);
 
         // Should only find matches in worktree1 *.rs files (excluding private ones)
         assert!(

crates/assistant_tools/src/list_directory_tool.rs 🔗

@@ -1,6 +1,7 @@
 use crate::schema::json_schema_for;
+use action_log::ActionLog;
 use anyhow::{Result, anyhow};
-use assistant_tool::{ActionLog, Tool, ToolResult};
+use assistant_tool::{Tool, ToolResult};
 use gpui::{AnyWindowHandle, App, Entity, Task};
 use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
 use project::{Project, WorktreeSettings};

crates/assistant_tools/src/move_path_tool.rs 🔗

@@ -1,6 +1,7 @@
 use crate::schema::json_schema_for;
+use action_log::ActionLog;
 use anyhow::{Context as _, Result, anyhow};
-use assistant_tool::{ActionLog, Tool, ToolResult};
+use assistant_tool::{Tool, ToolResult};
 use gpui::{AnyWindowHandle, App, AppContext, Entity, Task};
 use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
 use project::Project;

crates/assistant_tools/src/now_tool.rs 🔗

@@ -1,8 +1,9 @@
 use std::sync::Arc;
 
 use crate::schema::json_schema_for;
+use action_log::ActionLog;
 use anyhow::{Result, anyhow};
-use assistant_tool::{ActionLog, Tool, ToolResult};
+use assistant_tool::{Tool, ToolResult};
 use chrono::{Local, Utc};
 use gpui::{AnyWindowHandle, App, Entity, Task};
 use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};

crates/assistant_tools/src/open_tool.rs 🔗

@@ -1,6 +1,7 @@
 use crate::schema::json_schema_for;
+use action_log::ActionLog;
 use anyhow::{Context as _, Result, anyhow};
-use assistant_tool::{ActionLog, Tool, ToolResult};
+use assistant_tool::{Tool, ToolResult};
 use gpui::{AnyWindowHandle, App, AppContext, Entity, Task};
 use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
 use project::Project;

crates/assistant_tools/src/project_notifications_tool.rs 🔗

@@ -1,6 +1,7 @@
 use crate::schema::json_schema_for;
+use action_log::ActionLog;
 use anyhow::Result;
-use assistant_tool::{ActionLog, Tool, ToolResult};
+use assistant_tool::{Tool, ToolResult};
 use gpui::{AnyWindowHandle, App, Entity, Task};
 use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
 use project::Project;
@@ -80,7 +81,7 @@ fn fit_patch_to_size(patch: &str, max_size: usize) -> 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);
+    let mut file_patches = split_patch(patch);
     file_patches.sort_by_key(|patch| patch.len());
     let compressed_patches = file_patches
         .iter()

crates/assistant_tools/src/read_file_tool.rs 🔗

@@ -1,6 +1,7 @@
 use crate::schema::json_schema_for;
+use action_log::ActionLog;
 use anyhow::{Context as _, Result, anyhow};
-use assistant_tool::{ActionLog, Tool, ToolResult};
+use assistant_tool::{Tool, ToolResult};
 use assistant_tool::{ToolResultContent, outline};
 use gpui::{AnyWindowHandle, App, Entity, Task};
 use project::{ImageItem, image_store};
@@ -200,7 +201,7 @@ impl Tool for ReadFileTool {
                 buffer
                     .file()
                     .as_ref()
-                    .map_or(true, |file| !file.disk_state().exists())
+                    .is_none_or(|file| !file.disk_state().exists())
             })? {
                 anyhow::bail!("{file_path} not found");
             }
@@ -286,7 +287,7 @@ impl Tool for ReadFileTool {
                         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."
                     }

crates/assistant_tools/src/schema.rs 🔗

@@ -43,12 +43,11 @@ 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") {
-            if let Some(types) = type_field.as_array() {
-                if let Some(first_type) = types.first() {
-                    *type_field = first_type.clone();
-                }
-            }
+        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

crates/assistant_tools/src/terminal_tool.rs 🔗

@@ -2,9 +2,10 @@ 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::{ActionLog, Tool, ToolCard, ToolResult, ToolUseStatus};
+use assistant_tool::{Tool, ToolCard, ToolResult, ToolUseStatus};
 use futures::{FutureExt as _, future::Shared};
 use gpui::{
     Animation, AnimationExt, AnyWindowHandle, App, AppContext, Empty, Entity, EntityId, Task,
@@ -58,12 +59,9 @@ impl TerminalTool {
             }
 
             if which::which("bash").is_ok() {
-                log::info!("agent selected bash for terminal tool");
                 "bash".into()
             } else {
-                let shell = get_system_shell();
-                log::info!("agent selected {shell} for terminal tool");
-                shell
+                get_system_shell()
             }
         });
         Self {
@@ -104,7 +102,7 @@ impl Tool for TerminalTool {
                 let first_line = lines.next().unwrap_or_default();
                 let remaining_line_count = lines.count();
                 match remaining_line_count {
-                    0 => MarkdownInlineCode(&first_line).to_string(),
+                    0 => MarkdownInlineCode(first_line).to_string(),
                     1 => MarkdownInlineCode(&format!(
                         "{} - {} more line",
                         first_line, remaining_line_count
@@ -215,7 +213,8 @@ impl Tool for TerminalTool {
             async move |cx| {
                 let program = program.await;
                 let env = env.await;
-                let terminal = project
+
+                project
                     .update(cx, |project, cx| {
                         project.create_terminal(
                             TerminalKind::Task(task::SpawnInTerminal {
@@ -225,12 +224,10 @@ impl Tool for TerminalTool {
                                 env,
                                 ..Default::default()
                             }),
-                            window,
                             cx,
                         )
                     })?
-                    .await;
-                terminal
+                    .await
             }
         });
 
@@ -353,7 +350,7 @@ fn process_content(
             if is_empty {
                 "Command executed successfully.".to_string()
             } else {
-                content.to_string()
+                content
             }
         }
         Some(exit_status) => {
@@ -387,7 +384,7 @@ fn working_dir(
     let project = project.read(cx);
     let cd = &input.cd;
 
-    if cd == "." || 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);
 
@@ -412,10 +409,8 @@ fn working_dir(
             {
                 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()));
-            }
+        } 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.");

crates/assistant_tools/src/thinking_tool.rs 🔗

@@ -1,8 +1,9 @@
 use std::sync::Arc;
 
 use crate::schema::json_schema_for;
+use action_log::ActionLog;
 use anyhow::{Result, anyhow};
-use assistant_tool::{ActionLog, Tool, ToolResult};
+use assistant_tool::{Tool, ToolResult};
 use gpui::{AnyWindowHandle, App, Entity, Task};
 use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
 use project::Project;

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

@@ -101,14 +101,11 @@ impl RenderOnce for ToolCallCardHeader {
                     })
                     .when_some(secondary_text, |this, secondary_text| {
                         this.child(bullet_divider())
-                            .child(div().text_size(font_size).child(secondary_text.clone()))
+                            .child(div().text_size(font_size).child(secondary_text))
                     })
                     .when_some(code_path, |this, code_path| {
-                        this.child(bullet_divider()).child(
-                            Label::new(code_path.clone())
-                                .size(LabelSize::Small)
-                                .inline_code(cx),
-                        )
+                        this.child(bullet_divider())
+                            .child(Label::new(code_path).size(LabelSize::Small).inline_code(cx))
                     })
                     .with_animation(
                         "loading-label",

crates/assistant_tools/src/web_search_tool.rs 🔗

@@ -2,9 +2,10 @@ 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::{
-    ActionLog, Tool, ToolCard, ToolResult, ToolResultContent, ToolResultOutput, ToolUseStatus,
+    Tool, ToolCard, ToolResult, ToolResultContent, ToolResultOutput, ToolUseStatus,
 };
 use cloud_llm_client::{WebSearchResponse, WebSearchResult};
 use futures::{Future, FutureExt, TryFutureExt};
@@ -45,7 +46,7 @@ impl Tool for WebSearchTool {
     }
 
     fn icon(&self) -> IconName {
-        IconName::Globe
+        IconName::ToolWeb
     }
 
     fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
@@ -177,7 +178,7 @@ impl ToolCard for WebSearchToolCard {
                             .label_size(LabelSize::Small)
                             .color(Color::Muted)
                             .icon(IconName::ArrowUpRight)
-                            .icon_size(IconSize::XSmall)
+                            .icon_size(IconSize::Small)
                             .icon_position(IconPosition::End)
                             .truncate(true)
                             .tooltip({
@@ -192,10 +193,7 @@ impl ToolCard for WebSearchToolCard {
                                     )
                                 }
                             })
-                            .on_click({
-                                let url = url.clone();
-                                move |_, _, cx| cx.open_url(&url)
-                            })
+                            .on_click(move |_, _, cx| cx.open_url(&url))
                     }))
                     .into_any(),
             ),

crates/audio/Cargo.toml 🔗

@@ -15,9 +15,10 @@ doctest = false
 [dependencies]
 anyhow.workspace = true
 collections.workspace = true
-derive_more.workspace = true
 gpui.workspace = true
-parking_lot.workspace = true
-rodio = { version = "0.21.1", default-features = false, features = ["wav", "playback", "tracing"] }
+settings.workspace = true
+schemars.workspace = true
+serde.workspace = true
+rodio = { workspace = true, features = [ "wav", "playback", "tracing" ] }
 util.workspace = true
 workspace-hack.workspace = true

crates/audio/src/assets.rs 🔗

@@ -1,54 +0,0 @@
-use std::{io::Cursor, sync::Arc};
-
-use anyhow::{Context as _, Result};
-use collections::HashMap;
-use gpui::{App, AssetSource, Global};
-use rodio::{Decoder, Source, source::Buffered};
-
-type Sound = Buffered<Decoder<Cursor<Vec<u8>>>>;
-
-pub struct SoundRegistry {
-    cache: Arc<parking_lot::Mutex<HashMap<String, Sound>>>,
-    assets: Box<dyn AssetSource>,
-}
-
-struct GlobalSoundRegistry(Arc<SoundRegistry>);
-
-impl Global for GlobalSoundRegistry {}
-
-impl SoundRegistry {
-    pub fn new(source: impl AssetSource) -> Arc<Self> {
-        Arc::new(Self {
-            cache: Default::default(),
-            assets: Box::new(source),
-        })
-    }
-
-    pub fn global(cx: &App) -> Arc<Self> {
-        cx.global::<GlobalSoundRegistry>().0.clone()
-    }
-
-    pub(crate) fn set_global(source: impl AssetSource, cx: &mut App) {
-        cx.set_global(GlobalSoundRegistry(SoundRegistry::new(source)));
-    }
-
-    pub fn get(&self, name: &str) -> Result<impl Source<Item = f32> + use<>> {
-        if let Some(wav) = self.cache.lock().get(name) {
-            return Ok(wav.clone());
-        }
-
-        let path = format!("sounds/{}.wav", name);
-        let bytes = self
-            .assets
-            .load(&path)?
-            .map(anyhow::Ok)
-            .with_context(|| format!("No asset available for path {path}"))??
-            .into_owned();
-        let cursor = Cursor::new(bytes);
-        let source = Decoder::new(cursor)?.buffered();
-
-        self.cache.lock().insert(name.to_string(), source.clone());
-
-        Ok(source)
-    }
-}

crates/audio/src/audio.rs 🔗

@@ -1,16 +1,19 @@
-use assets::SoundRegistry;
-use derive_more::{Deref, DerefMut};
-use gpui::{App, AssetSource, BorrowAppContext, Global};
-use rodio::{OutputStream, OutputStreamBuilder};
+use anyhow::{Context as _, Result, anyhow};
+use collections::HashMap;
+use gpui::{App, BorrowAppContext, Global};
+use rodio::{Decoder, OutputStream, OutputStreamBuilder, Source, source::Buffered};
+use settings::Settings;
+use std::io::Cursor;
 use util::ResultExt;
 
-mod assets;
+mod audio_settings;
+pub use audio_settings::AudioSettings;
 
-pub fn init(source: impl AssetSource, cx: &mut App) {
-    SoundRegistry::set_global(source, cx);
-    cx.set_global(GlobalAudio(Audio::new()));
+pub fn init(cx: &mut App) {
+    AudioSettings::register(cx);
 }
 
+#[derive(Copy, Clone, Eq, Hash, PartialEq)]
 pub enum Sound {
     Joined,
     Leave,
@@ -38,18 +41,12 @@ impl Sound {
 #[derive(Default)]
 pub struct Audio {
     output_handle: Option<OutputStream>,
+    source_cache: HashMap<Sound, Buffered<Decoder<Cursor<Vec<u8>>>>>,
 }
 
-#[derive(Deref, DerefMut)]
-struct GlobalAudio(Audio);
-
-impl Global for GlobalAudio {}
+impl Global for Audio {}
 
 impl Audio {
-    pub fn new() -> Self {
-        Self::default()
-    }
-
     fn ensure_output_exists(&mut self) -> Option<&OutputStream> {
         if self.output_handle.is_none() {
             self.output_handle = OutputStreamBuilder::open_default_stream().log_err();
@@ -58,26 +55,51 @@ impl Audio {
         self.output_handle.as_ref()
     }
 
-    pub fn play_sound(sound: Sound, cx: &mut App) {
-        if !cx.has_global::<GlobalAudio>() {
-            return;
-        }
+    pub fn play_source(
+        source: impl rodio::Source + Send + 'static,
+        cx: &mut App,
+    ) -> anyhow::Result<()> {
+        cx.update_default_global(|this: &mut Self, _cx| {
+            let output_handle = this
+                .ensure_output_exists()
+                .ok_or_else(|| anyhow!("Could not open audio output"))?;
+            output_handle.mixer().add(source);
+            Ok(())
+        })
+    }
 
-        cx.update_global::<GlobalAudio, _>(|this, cx| {
+    pub fn play_sound(sound: Sound, cx: &mut App) {
+        cx.update_default_global(|this: &mut Self, cx| {
+            let source = this.sound_source(sound, cx).log_err()?;
             let output_handle = this.ensure_output_exists()?;
-            let source = SoundRegistry::global(cx).get(sound.file()).log_err()?;
             output_handle.mixer().add(source);
             Some(())
         });
     }
 
     pub fn end_call(cx: &mut App) {
-        if !cx.has_global::<GlobalAudio>() {
-            return;
-        }
-
-        cx.update_global::<GlobalAudio, _>(|this, _| {
+        cx.update_default_global(|this: &mut Self, _cx| {
             this.output_handle.take();
         });
     }
+
+    fn sound_source(&mut self, sound: Sound, cx: &App) -> Result<impl Source + use<>> {
+        if let Some(wav) = self.source_cache.get(&sound) {
+            return Ok(wav.clone());
+        }
+
+        let path = format!("sounds/{}.wav", sound.file());
+        let bytes = cx
+            .asset_source()
+            .load(&path)?
+            .map(anyhow::Ok)
+            .with_context(|| format!("No asset available for path {path}"))??
+            .into_owned();
+        let cursor = Cursor::new(bytes);
+        let source = Decoder::new(cursor)?.buffered();
+
+        self.source_cache.insert(sound, source.clone());
+
+        Ok(source)
+    }
 }

crates/audio/src/audio_settings.rs 🔗

@@ -0,0 +1,33 @@
+use anyhow::Result;
+use gpui::App;
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+use settings::{Settings, SettingsSources};
+
+#[derive(Deserialize, Debug)]
+pub struct AudioSettings {
+    /// Opt into the new audio system.
+    #[serde(rename = "experimental.rodio_audio", default)]
+    pub rodio_audio: bool, // default is false
+}
+
+/// Configuration of audio in Zed.
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
+#[serde(default)]
+pub struct AudioSettingsContent {
+    /// Whether to use the experimental audio system
+    #[serde(rename = "experimental.rodio_audio", default)]
+    pub rodio_audio: bool,
+}
+
+impl Settings for AudioSettings {
+    const KEY: Option<&'static str> = Some("audio");
+
+    type FileContent = AudioSettingsContent;
+
+    fn load(sources: SettingsSources<Self::FileContent>, _cx: &mut App) -> Result<Self> {
+        sources.json_merge()
+    }
+
+    fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
+}

crates/auto_update/src/auto_update.rs 🔗

@@ -59,16 +59,9 @@ pub enum VersionCheckType {
 pub enum AutoUpdateStatus {
     Idle,
     Checking,
-    Downloading {
-        version: VersionCheckType,
-    },
-    Installing {
-        version: VersionCheckType,
-    },
-    Updated {
-        binary_path: PathBuf,
-        version: VersionCheckType,
-    },
+    Downloading { version: VersionCheckType },
+    Installing { version: VersionCheckType },
+    Updated { version: VersionCheckType },
     Errored,
 }
 
@@ -83,6 +76,7 @@ pub struct AutoUpdater {
     current_version: SemanticVersion,
     http_client: Arc<HttpClientWithUrl>,
     pending_poll: Option<Task<Option<()>>>,
+    quit_subscription: Option<gpui::Subscription>,
 }
 
 #[derive(Deserialize, Clone, Debug)]
@@ -164,7 +158,7 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) {
     AutoUpdateSetting::register(cx);
 
     cx.observe_new(|workspace: &mut Workspace, _window, _cx| {
-        workspace.register_action(|_, action: &Check, window, cx| check(action, window, cx));
+        workspace.register_action(|_, action, window, cx| check(action, window, cx));
 
         workspace.register_action(|_, action, _, cx| {
             view_release_notes(action, cx);
@@ -174,7 +168,7 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) {
 
     let version = release_channel::AppVersion::global(cx);
     let auto_updater = cx.new(|cx| {
-        let updater = AutoUpdater::new(version, http_client);
+        let updater = AutoUpdater::new(version, http_client, cx);
 
         let poll_for_updates = ReleaseChannel::try_global(cx)
             .map(|channel| channel.poll_for_updates())
@@ -321,12 +315,34 @@ impl AutoUpdater {
         cx.default_global::<GlobalAutoUpdate>().0.clone()
     }
 
-    fn new(current_version: SemanticVersion, http_client: Arc<HttpClientWithUrl>) -> Self {
+    fn new(
+        current_version: SemanticVersion,
+        http_client: Arc<HttpClientWithUrl>,
+        cx: &mut Context<Self>,
+    ) -> Self {
+        // On windows, executable files cannot be overwritten while they are
+        // running, so we must wait to overwrite the application until quitting
+        // or restarting. When quitting the app, we spawn the auto update helper
+        // to finish the auto update process after Zed exits. When restarting
+        // the app after an update, we use `set_restart_path` to run the auto
+        // update helper instead of the app, so that it can overwrite the app
+        // and then spawn the new binary.
+        let quit_subscription = Some(cx.on_app_quit(|_, _| async move {
+            #[cfg(target_os = "windows")]
+            finalize_auto_update_on_quit();
+        }));
+
+        cx.on_app_restart(|this, _| {
+            this.quit_subscription.take();
+        })
+        .detach();
+
         Self {
             status: AutoUpdateStatus::Idle,
             current_version,
             http_client,
             pending_poll: None,
+            quit_subscription,
         }
     }
 
@@ -527,7 +543,7 @@ impl AutoUpdater {
 
     async fn update(this: Entity<Self>, mut cx: AsyncApp) -> Result<()> {
         let (client, installed_version, previous_status, release_channel) =
-            this.read_with(&mut cx, |this, cx| {
+            this.read_with(&cx, |this, cx| {
                 (
                     this.http_client.clone(),
                     this.current_version,
@@ -536,6 +552,8 @@ impl AutoUpdater {
                 )
             })?;
 
+        Self::check_dependencies()?;
+
         this.update(&mut cx, |this, cx| {
             this.status = AutoUpdateStatus::Checking;
             cx.notify();
@@ -582,13 +600,15 @@ impl AutoUpdater {
             cx.notify();
         })?;
 
-        let binary_path = Self::binary_path(installer_dir, target_path, &cx).await?;
+        let new_binary_path = Self::install_release(installer_dir, target_path, &cx).await?;
+        if let Some(new_binary_path) = new_binary_path {
+            cx.update(|cx| cx.set_restart_path(new_binary_path))?;
+        }
 
         this.update(&mut cx, |this, cx| {
             this.set_should_show_update_notification(true, cx)
                 .detach_and_log_err(cx);
             this.status = AutoUpdateStatus::Updated {
-                binary_path,
                 version: newer_version,
             };
             cx.notify();
@@ -639,6 +659,15 @@ impl AutoUpdater {
         }
     }
 
+    fn check_dependencies() -> Result<()> {
+        #[cfg(not(target_os = "windows"))]
+        anyhow::ensure!(
+            which::which("rsync").is_ok(),
+            "Aborting. Could not find rsync which is required for auto-updates."
+        );
+        Ok(())
+    }
+
     async fn target_path(installer_dir: &InstallerDir) -> Result<PathBuf> {
         let filename = match OS {
             "macos" => anyhow::Ok("Zed.dmg"),
@@ -647,20 +676,14 @@ impl AutoUpdater {
             unsupported_os => anyhow::bail!("not supported: {unsupported_os}"),
         }?;
 
-        #[cfg(not(target_os = "windows"))]
-        anyhow::ensure!(
-            which::which("rsync").is_ok(),
-            "Aborting. Could not find rsync which is required for auto-updates."
-        );
-
         Ok(installer_dir.path().join(filename))
     }
 
-    async fn binary_path(
+    async fn install_release(
         installer_dir: InstallerDir,
         target_path: PathBuf,
         cx: &AsyncApp,
-    ) -> Result<PathBuf> {
+    ) -> Result<Option<PathBuf>> {
         match OS {
             "macos" => install_release_macos(&installer_dir, target_path, cx).await,
             "linux" => install_release_linux(&installer_dir, target_path, cx).await,
@@ -801,7 +824,7 @@ async fn install_release_linux(
     temp_dir: &InstallerDir,
     downloaded_tar_gz: PathBuf,
     cx: &AsyncApp,
-) -> Result<PathBuf> {
+) -> Result<Option<PathBuf>> {
     let channel = cx.update(|cx| ReleaseChannel::global(cx).dev_name())?;
     let home_dir = PathBuf::from(env::var("HOME").context("no HOME env var set")?);
     let running_app_path = cx.update(|cx| cx.app_path())??;
@@ -861,14 +884,14 @@ async fn install_release_linux(
         String::from_utf8_lossy(&output.stderr)
     );
 
-    Ok(to.join(expected_suffix))
+    Ok(Some(to.join(expected_suffix)))
 }
 
 async fn install_release_macos(
     temp_dir: &InstallerDir,
     downloaded_dmg: PathBuf,
     cx: &AsyncApp,
-) -> Result<PathBuf> {
+) -> Result<Option<PathBuf>> {
     let running_app_path = cx.update(|cx| cx.app_path())??;
     let running_app_filename = running_app_path
         .file_name()
@@ -910,10 +933,10 @@ async fn install_release_macos(
         String::from_utf8_lossy(&output.stderr)
     );
 
-    Ok(running_app_path)
+    Ok(None)
 }
 
-async fn install_release_windows(downloaded_installer: PathBuf) -> Result<PathBuf> {
+async fn install_release_windows(downloaded_installer: PathBuf) -> Result<Option<PathBuf>> {
     let output = Command::new(downloaded_installer)
         .arg("/verysilent")
         .arg("/update=true")
@@ -926,29 +949,36 @@ async fn install_release_windows(downloaded_installer: PathBuf) -> Result<PathBu
         "failed to start installer: {:?}",
         String::from_utf8_lossy(&output.stderr)
     );
-    Ok(std::env::current_exe()?)
+    // We return the path to the update helper program, because it will
+    // perform the final steps of the update process, copying the new binary,
+    // deleting the old one, and launching the new binary.
+    let helper_path = std::env::current_exe()?
+        .parent()
+        .context("No parent dir for Zed.exe")?
+        .join("tools\\auto_update_helper.exe");
+    Ok(Some(helper_path))
 }
 
-pub fn check_pending_installation() -> bool {
+pub fn finalize_auto_update_on_quit() {
     let Some(installer_path) = std::env::current_exe()
         .ok()
         .and_then(|p| p.parent().map(|p| p.join("updates")))
     else {
-        return false;
+        return;
     };
 
     // The installer will create a flag file after it finishes updating
     let flag_file = installer_path.join("versions.txt");
-    if flag_file.exists() {
-        if let Some(helper) = installer_path
+    if flag_file.exists()
+        && let Some(helper) = installer_path
             .parent()
             .map(|p| p.join("tools\\auto_update_helper.exe"))
-        {
-            let _ = std::process::Command::new(helper).spawn();
-            return true;
-        }
+    {
+        let mut command = std::process::Command::new(helper);
+        command.arg("--launch");
+        command.arg("false");
+        let _ = command.spawn();
     }
-    false
 }
 
 #[cfg(test)]
@@ -1002,7 +1032,6 @@ mod tests {
         let app_commit_sha = Ok(Some("a".to_string()));
         let installed_version = SemanticVersion::new(1, 0, 0);
         let status = AutoUpdateStatus::Updated {
-            binary_path: PathBuf::new(),
             version: VersionCheckType::Semantic(SemanticVersion::new(1, 0, 1)),
         };
         let fetched_version = SemanticVersion::new(1, 0, 1);
@@ -1024,7 +1053,6 @@ mod tests {
         let app_commit_sha = Ok(Some("a".to_string()));
         let installed_version = SemanticVersion::new(1, 0, 0);
         let status = AutoUpdateStatus::Updated {
-            binary_path: PathBuf::new(),
             version: VersionCheckType::Semantic(SemanticVersion::new(1, 0, 1)),
         };
         let fetched_version = SemanticVersion::new(1, 0, 2);
@@ -1090,7 +1118,6 @@ mod tests {
         let app_commit_sha = Ok(Some("a".to_string()));
         let installed_version = SemanticVersion::new(1, 0, 0);
         let status = AutoUpdateStatus::Updated {
-            binary_path: PathBuf::new(),
             version: VersionCheckType::Sha(AppCommitSha::new("b".to_string())),
         };
         let fetched_sha = "b".to_string();
@@ -1112,7 +1139,6 @@ mod tests {
         let app_commit_sha = Ok(Some("a".to_string()));
         let installed_version = SemanticVersion::new(1, 0, 0);
         let status = AutoUpdateStatus::Updated {
-            binary_path: PathBuf::new(),
             version: VersionCheckType::Sha(AppCommitSha::new("b".to_string())),
         };
         let fetched_sha = "c".to_string();
@@ -1160,7 +1186,6 @@ mod tests {
         let app_commit_sha = Ok(None);
         let installed_version = SemanticVersion::new(1, 0, 0);
         let status = AutoUpdateStatus::Updated {
-            binary_path: PathBuf::new(),
             version: VersionCheckType::Sha(AppCommitSha::new("b".to_string())),
         };
         let fetched_sha = "b".to_string();
@@ -1183,7 +1208,6 @@ mod tests {
         let app_commit_sha = Ok(None);
         let installed_version = SemanticVersion::new(1, 0, 0);
         let status = AutoUpdateStatus::Updated {
-            binary_path: PathBuf::new(),
             version: VersionCheckType::Sha(AppCommitSha::new("b".to_string())),
         };
         let fetched_sha = "c".to_string();

crates/auto_update_helper/src/auto_update_helper.rs 🔗

@@ -18,7 +18,7 @@ fn main() {}
 
 #[cfg(target_os = "windows")]
 mod windows_impl {
-    use std::path::Path;
+    use std::{borrow::Cow, path::Path};
 
     use super::dialog::create_dialog_window;
     use super::updater::perform_update;
@@ -37,6 +37,11 @@ mod windows_impl {
     pub(crate) const WM_JOB_UPDATED: u32 = WM_USER + 1;
     pub(crate) const WM_TERMINATE: u32 = WM_USER + 2;
 
+    #[derive(Debug, Default)]
+    struct Args {
+        launch: bool,
+    }
+
     pub(crate) fn run() -> Result<()> {
         let helper_dir = std::env::current_exe()?
             .parent()
@@ -51,8 +56,9 @@ mod windows_impl {
         log::info!("======= Starting Zed update =======");
         let (tx, rx) = std::sync::mpsc::channel();
         let hwnd = create_dialog_window(rx)?.0 as isize;
+        let args = parse_args(std::env::args().skip(1));
         std::thread::spawn(move || {
-            let result = perform_update(app_dir.as_path(), Some(hwnd));
+            let result = perform_update(app_dir.as_path(), Some(hwnd), args.launch);
             tx.send(result).ok();
             unsafe { PostMessageW(Some(HWND(hwnd as _)), WM_TERMINATE, WPARAM(0), LPARAM(0)) }.ok();
         });
@@ -77,6 +83,29 @@ mod windows_impl {
         Ok(())
     }
 
+    fn parse_args(input: impl IntoIterator<Item = String>) -> Args {
+        let mut args: Args = Args { launch: true };
+
+        let mut input = input.into_iter();
+        if let Some(arg) = input.next() {
+            let launch_arg;
+
+            if arg == "--launch" {
+                launch_arg = input.next().map(Cow::Owned);
+            } else if let Some(rest) = arg.strip_prefix("--launch=") {
+                launch_arg = Some(Cow::Borrowed(rest));
+            } else {
+                launch_arg = None;
+            }
+
+            if launch_arg.as_deref() == Some("false") {
+                args.launch = false;
+            }
+        }
+
+        args
+    }
+
     pub(crate) fn show_error(mut content: String) {
         if content.len() > 600 {
             content.truncate(600);
@@ -91,4 +120,28 @@ mod windows_impl {
             )
         };
     }
+
+    #[cfg(test)]
+    mod tests {
+        use crate::windows_impl::parse_args;
+
+        #[test]
+        fn test_parse_args() {
+            // launch can be specified via two separate arguments
+            assert!(parse_args(["--launch".into(), "true".into()]).launch);
+            assert!(!parse_args(["--launch".into(), "false".into()]).launch);
+
+            // launch can be specified via one single argument
+            assert!(parse_args(["--launch=true".into()]).launch);
+            assert!(!parse_args(["--launch=false".into()]).launch);
+
+            // launch defaults to true on no arguments
+            assert!(parse_args([]).launch);
+
+            // launch defaults to true on invalid arguments
+            assert!(parse_args(["--launch".into()]).launch);
+            assert!(parse_args(["--launch=".into()]).launch);
+            assert!(parse_args(["--launch=invalid".into()]).launch);
+        }
+    }
 }

crates/auto_update_helper/src/dialog.rs 🔗

@@ -72,7 +72,7 @@ pub(crate) fn create_dialog_window(receiver: Receiver<Result<()>>) -> Result<HWN
         let hwnd = CreateWindowExW(
             WS_EX_TOPMOST,
             class_name,
-            windows::core::w!("Zed Editor"),
+            windows::core::w!("Zed"),
             WS_VISIBLE | WS_POPUP | WS_CAPTION,
             rect.right / 2 - width / 2,
             rect.bottom / 2 - height / 2,
@@ -171,7 +171,7 @@ unsafe extern "system" fn wnd_proc(
                 &HSTRING::from(font_name),
             );
             let temp = SelectObject(hdc, font.into());
-            let string = HSTRING::from("Zed Editor is updating...");
+            let string = HSTRING::from("Updating Zed...");
             return_if_failed!(TextOutW(hdc, 20, 15, &string).ok());
             return_if_failed!(DeleteObject(temp).ok());
 
@@ -186,11 +186,11 @@ unsafe extern "system" fn wnd_proc(
         }),
         WM_TERMINATE => {
             with_dialog_data(hwnd, |data| {
-                if let Ok(result) = data.borrow_mut().rx.recv() {
-                    if let Err(e) = result {
-                        log::error!("Failed to update Zed: {:?}", e);
-                        show_error(format!("Error: {:?}", e));
-                    }
+                if let Ok(result) = data.borrow_mut().rx.recv()
+                    && let Err(e) = result
+                {
+                    log::error!("Failed to update Zed: {:?}", e);
+                    show_error(format!("Error: {:?}", e));
                 }
             });
             unsafe { PostQuitMessage(0) };

crates/auto_update_helper/src/updater.rs 🔗

@@ -90,11 +90,7 @@ pub(crate) const JOBS: [Job; 2] = [
         std::thread::sleep(Duration::from_millis(1000));
         if let Ok(config) = std::env::var("ZED_AUTO_UPDATE") {
             match config.as_str() {
-                "err" => Err(std::io::Error::new(
-                    std::io::ErrorKind::Other,
-                    "Simulated error",
-                ))
-                .context("Anyhow!"),
+                "err" => Err(std::io::Error::other("Simulated error")).context("Anyhow!"),
                 _ => panic!("Unknown ZED_AUTO_UPDATE value: {}", config),
             }
         } else {
@@ -105,11 +101,7 @@ pub(crate) const JOBS: [Job; 2] = [
         std::thread::sleep(Duration::from_millis(1000));
         if let Ok(config) = std::env::var("ZED_AUTO_UPDATE") {
             match config.as_str() {
-                "err" => Err(std::io::Error::new(
-                    std::io::ErrorKind::Other,
-                    "Simulated error",
-                ))
-                .context("Anyhow!"),
+                "err" => Err(std::io::Error::other("Simulated error")).context("Anyhow!"),
                 _ => panic!("Unknown ZED_AUTO_UPDATE value: {}", config),
             }
         } else {
@@ -118,7 +110,7 @@ pub(crate) const JOBS: [Job; 2] = [
     },
 ];
 
-pub(crate) fn perform_update(app_dir: &Path, hwnd: Option<isize>) -> Result<()> {
+pub(crate) fn perform_update(app_dir: &Path, hwnd: Option<isize>, launch: bool) -> Result<()> {
     let hwnd = hwnd.map(|ptr| HWND(ptr as _));
 
     for job in JOBS.iter() {
@@ -145,9 +137,11 @@ pub(crate) fn perform_update(app_dir: &Path, hwnd: Option<isize>) -> Result<()>
             }
         }
     }
-    let _ = std::process::Command::new(app_dir.join("Zed.exe"))
-        .creation_flags(CREATE_NEW_PROCESS_GROUP.0)
-        .spawn();
+    if launch {
+        let _ = std::process::Command::new(app_dir.join("Zed.exe"))
+            .creation_flags(CREATE_NEW_PROCESS_GROUP.0)
+            .spawn();
+    }
     log::info!("Update completed successfully");
     Ok(())
 }
@@ -159,11 +153,11 @@ mod test {
     #[test]
     fn test_perform_update() {
         let app_dir = std::path::Path::new("C:/");
-        assert!(perform_update(app_dir, None).is_ok());
+        assert!(perform_update(app_dir, None, false).is_ok());
 
         // Simulate a timeout
         unsafe { std::env::set_var("ZED_AUTO_UPDATE", "err") };
-        let ret = perform_update(app_dir, None);
+        let ret = perform_update(app_dir, None, false);
         assert!(ret.is_err_and(|e| e.to_string().as_str() == "Timed out"));
     }
 }

crates/bedrock/src/bedrock.rs 🔗

@@ -54,11 +54,7 @@ pub async fn stream_completion(
         )])));
     }
 
-    if request
-        .tools
-        .as_ref()
-        .map_or(false, |t| !t.tools.is_empty())
-    {
+    if request.tools.as_ref().is_some_and(|t| !t.tools.is_empty()) {
         response = response.set_tool_config(request.tools);
     }
 

crates/breadcrumbs/src/breadcrumbs.rs 🔗

@@ -82,11 +82,12 @@ impl Render for Breadcrumbs {
             }
             text_style.color = Color::Muted.color(cx);
 
-            if index == 0 && !TabBarSettings::get_global(cx).show && active_item.is_dirty(cx) {
-                if let Some(styled_element) = apply_dirty_filename_style(&segment, &text_style, cx)
-                {
-                    return styled_element;
-                }
+            if index == 0
+                && !TabBarSettings::get_global(cx).show
+                && active_item.is_dirty(cx)
+                && let Some(styled_element) = apply_dirty_filename_style(&segment, &text_style, cx)
+            {
+                return styled_element;
             }
 
             StyledText::new(segment.text.replace('\n', "⏎"))
@@ -231,7 +232,7 @@ fn apply_dirty_filename_style(
     let highlight = vec![(filename_position..text.len(), highlight_style)];
     Some(
         StyledText::new(text)
-            .with_default_highlights(&text_style, highlight)
+            .with_default_highlights(text_style, highlight)
             .into_any(),
     )
 }

crates/buffer_diff/src/buffer_diff.rs 🔗

@@ -175,12 +175,8 @@ impl BufferDiffSnapshot {
         if let Some(text) = &base_text {
             let base_text_rope = Rope::from(text.as_str());
             base_text_pair = Some((text.clone(), base_text_rope.clone()));
-            let snapshot = language::Buffer::build_snapshot(
-                base_text_rope,
-                language.clone(),
-                language_registry.clone(),
-                cx,
-            );
+            let snapshot =
+                language::Buffer::build_snapshot(base_text_rope, language, language_registry, cx);
             base_text_snapshot = cx.background_spawn(snapshot);
             base_text_exists = true;
         } else {
@@ -572,14 +568,14 @@ impl BufferDiffInner {
                         pending_range.end.column = 0;
                     }
 
-                    if pending_range == (start_point..end_point) {
-                        if !buffer.has_edits_since_in_range(
+                    if pending_range == (start_point..end_point)
+                        && !buffer.has_edits_since_in_range(
                             &pending_hunk.buffer_version,
                             start_anchor..end_anchor,
-                        ) {
-                            has_pending = true;
-                            secondary_status = pending_hunk.new_status;
-                        }
+                        )
+                    {
+                        has_pending = true;
+                        secondary_status = pending_hunk.new_status;
                     }
                 }
 
@@ -928,7 +924,7 @@ impl BufferDiff {
         let new_index_text = self.inner.stage_or_unstage_hunks_impl(
             &self.secondary_diff.as_ref()?.read(cx).inner,
             stage,
-            &hunks,
+            hunks,
             buffer,
             file_exists,
         );
@@ -952,12 +948,12 @@ impl BufferDiff {
         cx: &App,
     ) -> Option<Range<Anchor>> {
         let start = self
-            .hunks_intersecting_range(range.clone(), &buffer, cx)
+            .hunks_intersecting_range(range.clone(), buffer, cx)
             .next()?
             .buffer_range
             .start;
         let end = self
-            .hunks_intersecting_range_rev(range.clone(), &buffer)
+            .hunks_intersecting_range_rev(range, buffer)
             .next()?
             .buffer_range
             .end;
@@ -1031,21 +1027,20 @@ impl BufferDiff {
                         && state.base_text.syntax_update_count()
                             == new_state.base_text.syntax_update_count() =>
                 {
-                    (false, new_state.compare(&state, buffer))
+                    (false, new_state.compare(state, buffer))
                 }
                 _ => (true, Some(text::Anchor::MIN..text::Anchor::MAX)),
             };
 
-        if let Some(secondary_changed_range) = secondary_diff_change {
-            if let Some(secondary_hunk_range) =
-                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);
-                } else {
-                    changed_range = Some(secondary_hunk_range);
-                }
+        if let Some(secondary_changed_range) = secondary_diff_change
+            && let Some(secondary_hunk_range) =
+                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);
+            } else {
+                changed_range = Some(secondary_hunk_range);
             }
         }
 
@@ -1057,8 +1052,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);
                 }
@@ -1442,7 +1437,7 @@ mod tests {
         .unindent();
 
         let buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text);
-        let unstaged_diff = BufferDiffSnapshot::new_sync(buffer.clone(), index_text.clone(), cx);
+        let unstaged_diff = BufferDiffSnapshot::new_sync(buffer.clone(), index_text, cx);
         let mut uncommitted_diff =
             BufferDiffSnapshot::new_sync(buffer.clone(), head_text.clone(), cx);
         uncommitted_diff.secondary_diff = Some(Box::new(unstaged_diff));
@@ -1797,7 +1792,7 @@ mod tests {
 
             uncommitted_diff.update(cx, |diff, cx| {
                 let hunks = diff
-                    .hunks_intersecting_range(hunk_range.clone(), &buffer, &cx)
+                    .hunks_intersecting_range(hunk_range.clone(), &buffer, cx)
                     .collect::<Vec<_>>();
                 for hunk in &hunks {
                     assert_ne!(
@@ -1812,7 +1807,7 @@ mod tests {
                     .to_string();
 
                 let hunks = diff
-                    .hunks_intersecting_range(hunk_range.clone(), &buffer, &cx)
+                    .hunks_intersecting_range(hunk_range.clone(), &buffer, cx)
                     .collect::<Vec<_>>();
                 for hunk in &hunks {
                     assert_eq!(
@@ -1870,7 +1865,7 @@ mod tests {
                 .to_string();
             assert_eq!(new_index_text, buffer_text);
 
-            let hunk = diff.hunks(&buffer, &cx).next().unwrap();
+            let hunk = diff.hunks(&buffer, cx).next().unwrap();
             assert_eq!(
                 hunk.secondary_status,
                 DiffHunkSecondaryStatus::SecondaryHunkRemovalPending
@@ -1882,7 +1877,7 @@ mod tests {
                 .to_string();
             assert_eq!(index_text, head_text);
 
-            let hunk = diff.hunks(&buffer, &cx).next().unwrap();
+            let hunk = diff.hunks(&buffer, cx).next().unwrap();
             // optimistically unstaged (fine, could also be HasSecondaryHunk)
             assert_eq!(
                 hunk.secondary_status,
@@ -2029,8 +2024,8 @@ mod tests {
         fn gen_working_copy(rng: &mut StdRng, head: &str) -> String {
             let mut old_lines = {
                 let mut old_lines = Vec::new();
-                let mut old_lines_iter = head.lines();
-                while let Some(line) = old_lines_iter.next() {
+                let old_lines_iter = head.lines();
+                for line in old_lines_iter {
                     assert!(!line.ends_with("\n"));
                     old_lines.push(line.to_owned());
                 }
@@ -2134,7 +2129,7 @@ mod tests {
             diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &working_copy, cx)
                 .collect::<Vec<_>>()
         });
-        if hunks.len() == 0 {
+        if hunks.is_empty() {
             return;
         }
 

crates/call/src/call_impl/mod.rs 🔗

@@ -116,7 +116,7 @@ impl ActiveCall {
         envelope: TypedEnvelope<proto::IncomingCall>,
         mut cx: AsyncApp,
     ) -> Result<proto::Ack> {
-        let user_store = this.read_with(&mut cx, |this, _| this.user_store.clone())?;
+        let user_store = this.read_with(&cx, |this, _| this.user_store.clone())?;
         let call = IncomingCall {
             room_id: envelope.payload.room_id,
             participants: user_store
@@ -147,7 +147,7 @@ impl ActiveCall {
             let mut incoming_call = this.incoming_call.0.borrow_mut();
             if incoming_call
                 .as_ref()
-                .map_or(false, |call| call.room_id == envelope.payload.room_id)
+                .is_some_and(|call| call.room_id == envelope.payload.room_id)
             {
                 incoming_call.take();
             }

crates/call/src/call_impl/participant.rs 🔗

@@ -64,7 +64,7 @@ pub struct RemoteParticipant {
 
 impl RemoteParticipant {
     pub fn has_video_tracks(&self) -> bool {
-        return !self.video_tracks.is_empty();
+        !self.video_tracks.is_empty()
     }
 
     pub fn can_write(&self) -> bool {

crates/call/src/call_impl/room.rs 🔗

@@ -10,10 +10,10 @@ use client::{
 };
 use collections::{BTreeMap, HashMap, HashSet};
 use fs::Fs;
-use futures::{FutureExt, StreamExt};
+use futures::StreamExt;
 use gpui::{
-    App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, ScreenCaptureSource,
-    ScreenCaptureStream, Task, WeakEntity,
+    App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, FutureExt as _,
+    ScreenCaptureSource, ScreenCaptureStream, Task, Timeout, WeakEntity,
 };
 use gpui_tokio::Tokio;
 use language::LanguageRegistry;
@@ -370,57 +370,53 @@ impl Room {
                     })?;
 
                 // Wait for client to re-establish a connection to the server.
-                {
-                    let mut reconnection_timeout =
-                        cx.background_executor().timer(RECONNECT_TIMEOUT).fuse();
-                    let client_reconnection = async {
-                        let mut remaining_attempts = 3;
-                        while remaining_attempts > 0 {
-                            if client_status.borrow().is_connected() {
-                                log::info!("client reconnected, attempting to rejoin room");
-
-                                let Some(this) = this.upgrade() else { break };
-                                match this.update(cx, |this, cx| this.rejoin(cx)) {
-                                    Ok(task) => {
-                                        if task.await.log_err().is_some() {
-                                            return true;
-                                        } else {
-                                            remaining_attempts -= 1;
-                                        }
+                let executor = cx.background_executor().clone();
+                let client_reconnection = async {
+                    let mut remaining_attempts = 3;
+                    while remaining_attempts > 0 {
+                        if client_status.borrow().is_connected() {
+                            log::info!("client reconnected, attempting to rejoin room");
+
+                            let Some(this) = this.upgrade() else { break };
+                            match this.update(cx, |this, cx| this.rejoin(cx)) {
+                                Ok(task) => {
+                                    if task.await.log_err().is_some() {
+                                        return true;
+                                    } else {
+                                        remaining_attempts -= 1;
                                     }
-                                    Err(_app_dropped) => return false,
                                 }
-                            } else if client_status.borrow().is_signed_out() {
-                                return false;
+                                Err(_app_dropped) => return false,
                             }
-
-                            log::info!(
-                                "waiting for client status change, remaining attempts {}",
-                                remaining_attempts
-                            );
-                            client_status.next().await;
+                        } else if client_status.borrow().is_signed_out() {
+                            return false;
                         }
-                        false
+
+                        log::info!(
+                            "waiting for client status change, remaining attempts {}",
+                            remaining_attempts
+                        );
+                        client_status.next().await;
                     }
-                    .fuse();
-                    futures::pin_mut!(client_reconnection);
-
-                    futures::select_biased! {
-                        reconnected = client_reconnection => {
-                            if reconnected {
-                                log::info!("successfully reconnected to room");
-                                // If we successfully joined the room, go back around the loop
-                                // waiting for future connection status changes.
-                                continue;
-                            }
-                        }
-                        _ = reconnection_timeout => {
-                            log::info!("room reconnection timeout expired");
-                        }
+                    false
+                };
+
+                match client_reconnection
+                    .with_timeout(RECONNECT_TIMEOUT, &executor)
+                    .await
+                {
+                    Ok(true) => {
+                        log::info!("successfully reconnected to room");
+                        // If we successfully joined the room, go back around the loop
+                        // waiting for future connection status changes.
+                        continue;
+                    }
+                    Ok(false) => break,
+                    Err(Timeout) => {
+                        log::info!("room reconnection timeout expired");
+                        break;
                     }
                 }
-
-                break;
             }
         }
 
@@ -831,24 +827,23 @@ impl Room {
                             );
 
                             Audio::play_sound(Sound::Joined, cx);
-                            if let Some(livekit_participants) = &livekit_participants {
-                                if let Some(livekit_participant) = livekit_participants
+                            if let Some(livekit_participants) = &livekit_participants
+                                && let Some(livekit_participant) = livekit_participants
                                     .get(&ParticipantIdentity(user.id.to_string()))
+                            {
+                                for publication in
+                                    livekit_participant.track_publications().into_values()
                                 {
-                                    for publication in
-                                        livekit_participant.track_publications().into_values()
-                                    {
-                                        if let Some(track) = publication.track() {
-                                            this.livekit_room_updated(
-                                                RoomEvent::TrackSubscribed {
-                                                    track,
-                                                    publication,
-                                                    participant: livekit_participant.clone(),
-                                                },
-                                                cx,
-                                            )
-                                            .warn_on_err();
-                                        }
+                                    if let Some(track) = publication.track() {
+                                        this.livekit_room_updated(
+                                            RoomEvent::TrackSubscribed {
+                                                track,
+                                                publication,
+                                                participant: livekit_participant.clone(),
+                                            },
+                                            cx,
+                                        )
+                                        .warn_on_err();
                                     }
                                 }
                             }
@@ -944,10 +939,8 @@ impl Room {
                                 self.client.user_id()
                             )
                         })?;
-                if self.live_kit.as_ref().map_or(true, |kit| kit.deafened) {
-                    if publication.is_audio() {
-                        publication.set_enabled(false, cx);
-                    }
+                if self.live_kit.as_ref().is_none_or(|kit| kit.deafened) && publication.is_audio() {
+                    publication.set_enabled(false, cx);
                 }
                 match track {
                     livekit_client::RemoteTrack::Audio(track) => {
@@ -1009,10 +1002,10 @@ impl Room {
                 for (sid, participant) in &mut self.remote_participants {
                     participant.speaking = speaker_ids.binary_search(sid).is_ok();
                 }
-                if let Some(id) = self.client.user_id() {
-                    if let Some(room) = &mut self.live_kit {
-                        room.speaking = speaker_ids.binary_search(&id).is_ok();
-                    }
+                if let Some(id) = self.client.user_id()
+                    && let Some(room) = &mut self.live_kit
+                {
+                    room.speaking = speaker_ids.binary_search(&id).is_ok();
                 }
             }
 
@@ -1046,18 +1039,16 @@ impl Room {
                     if let LocalTrack::Published {
                         track_publication, ..
                     } = &room.microphone_track
+                        && track_publication.sid() == publication.sid()
                     {
-                        if track_publication.sid() == publication.sid() {
-                            room.microphone_track = LocalTrack::None;
-                        }
+                        room.microphone_track = LocalTrack::None;
                     }
                     if let LocalTrack::Published {
                         track_publication, ..
                     } = &room.screen_track
+                        && track_publication.sid() == publication.sid()
                     {
-                        if track_publication.sid() == publication.sid() {
-                            room.screen_track = LocalTrack::None;
-                        }
+                        room.screen_track = LocalTrack::None;
                     }
                 }
             }
@@ -1182,7 +1173,7 @@ impl Room {
             this.update(cx, |this, cx| {
                 this.shared_projects.insert(project.downgrade());
                 let active_project = this.local_participant.active_project.as_ref();
-                if active_project.map_or(false, |location| *location == project) {
+                if active_project.is_some_and(|location| *location == project) {
                     this.set_location(Some(&project), cx)
                 } else {
                     Task::ready(Ok(()))
@@ -1255,9 +1246,9 @@ impl Room {
     }
 
     pub fn is_sharing_screen(&self) -> bool {
-        self.live_kit.as_ref().map_or(false, |live_kit| {
-            !matches!(live_kit.screen_track, LocalTrack::None)
-        })
+        self.live_kit
+            .as_ref()
+            .is_some_and(|live_kit| !matches!(live_kit.screen_track, LocalTrack::None))
     }
 
     pub fn shared_screen_id(&self) -> Option<u64> {
@@ -1270,13 +1261,13 @@ impl Room {
     }
 
     pub fn is_sharing_mic(&self) -> bool {
-        self.live_kit.as_ref().map_or(false, |live_kit| {
-            !matches!(live_kit.microphone_track, LocalTrack::None)
-        })
+        self.live_kit
+            .as_ref()
+            .is_some_and(|live_kit| !matches!(live_kit.microphone_track, LocalTrack::None))
     }
 
     pub fn is_muted(&self) -> bool {
-        self.live_kit.as_ref().map_or(false, |live_kit| {
+        self.live_kit.as_ref().is_some_and(|live_kit| {
             matches!(live_kit.microphone_track, LocalTrack::None)
                 || live_kit.muted_by_user
                 || live_kit.deafened
@@ -1286,13 +1277,13 @@ impl Room {
     pub fn muted_by_user(&self) -> bool {
         self.live_kit
             .as_ref()
-            .map_or(false, |live_kit| live_kit.muted_by_user)
+            .is_some_and(|live_kit| live_kit.muted_by_user)
     }
 
     pub fn is_speaking(&self) -> bool {
         self.live_kit
             .as_ref()
-            .map_or(false, |live_kit| live_kit.speaking)
+            .is_some_and(|live_kit| live_kit.speaking)
     }
 
     pub fn is_deafened(&self) -> Option<bool> {
@@ -1488,10 +1479,8 @@ impl Room {
 
             self.set_deafened(deafened, cx);
 
-            if should_change_mute {
-                if let Some(task) = self.set_mute(deafened, cx) {
-                    task.detach_and_log_err(cx);
-                }
+            if should_change_mute && let Some(task) = self.set_mute(deafened, cx) {
+                task.detach_and_log_err(cx);
             }
         }
     }

crates/channel/src/channel_buffer.rs 🔗

@@ -82,7 +82,7 @@ impl ChannelBuffer {
                 collaborators: Default::default(),
                 acknowledge_task: None,
                 channel_id: channel.id,
-                subscription: Some(subscription.set_entity(&cx.entity(), &mut cx.to_async())),
+                subscription: Some(subscription.set_entity(&cx.entity(), &cx.to_async())),
                 user_store,
                 channel_store,
             };
@@ -110,7 +110,7 @@ impl ChannelBuffer {
             let Ok(subscription) = self.client.subscribe_to_entity(self.channel_id.0) else {
                 return;
             };
-            self.subscription = Some(subscription.set_entity(&cx.entity(), &mut cx.to_async()));
+            self.subscription = Some(subscription.set_entity(&cx.entity(), &cx.to_async()));
             cx.emit(ChannelBufferEvent::Connected);
         }
     }
@@ -135,7 +135,7 @@ impl ChannelBuffer {
             }
         }
 
-        for (_, old_collaborator) in &self.collaborators {
+        for old_collaborator in self.collaborators.values() {
             if !new_collaborators.contains_key(&old_collaborator.peer_id) {
                 self.buffer.update(cx, |buffer, cx| {
                     buffer.remove_peer(old_collaborator.replica_id, cx)
@@ -191,12 +191,11 @@ impl ChannelBuffer {
                 operation,
                 is_local: true,
             } => {
-                if *ZED_ALWAYS_ACTIVE {
-                    if let language::Operation::UpdateSelections { selections, .. } = operation {
-                        if selections.is_empty() {
-                            return;
-                        }
-                    }
+                if *ZED_ALWAYS_ACTIVE
+                    && let language::Operation::UpdateSelections { selections, .. } = operation
+                    && selections.is_empty()
+                {
+                    return;
                 }
                 let operation = language::proto::serialize_operation(operation);
                 self.client

crates/channel/src/channel_chat.rs 🔗

@@ -329,24 +329,24 @@ impl ChannelChat {
         loop {
             let step = chat
                 .update(&mut cx, |chat, cx| {
-                    if let Some(first_id) = chat.first_loaded_message_id() {
-                        if first_id <= message_id {
-                            let mut cursor = chat
-                                .messages
-                                .cursor::<Dimensions<ChannelMessageId, Count>>(&());
-                            let message_id = ChannelMessageId::Saved(message_id);
-                            cursor.seek(&message_id, Bias::Left);
-                            return ControlFlow::Break(
-                                if cursor
-                                    .item()
-                                    .map_or(false, |message| message.id == message_id)
-                                {
-                                    Some(cursor.start().1.0)
-                                } else {
-                                    None
-                                },
-                            );
-                        }
+                    if let Some(first_id) = chat.first_loaded_message_id()
+                        && first_id <= message_id
+                    {
+                        let mut cursor = chat
+                            .messages
+                            .cursor::<Dimensions<ChannelMessageId, Count>>(&());
+                        let message_id = ChannelMessageId::Saved(message_id);
+                        cursor.seek(&message_id, Bias::Left);
+                        return ControlFlow::Break(
+                            if cursor
+                                .item()
+                                .is_some_and(|message| message.id == message_id)
+                            {
+                                Some(cursor.start().1.0)
+                            } else {
+                                None
+                            },
+                        );
                     }
                     ControlFlow::Continue(chat.load_more_messages(cx))
                 })
@@ -359,22 +359,21 @@ impl ChannelChat {
     }
 
     pub fn acknowledge_last_message(&mut self, cx: &mut Context<Self>) {
-        if let ChannelMessageId::Saved(latest_message_id) = self.messages.summary().max_id {
-            if self
+        if let ChannelMessageId::Saved(latest_message_id) = self.messages.summary().max_id
+            && self
                 .last_acknowledged_id
-                .map_or(true, |acknowledged_id| acknowledged_id < latest_message_id)
-            {
-                self.rpc
-                    .send(proto::AckChannelMessage {
-                        channel_id: self.channel_id.0,
-                        message_id: latest_message_id,
-                    })
-                    .ok();
-                self.last_acknowledged_id = Some(latest_message_id);
-                self.channel_store.update(cx, |store, cx| {
-                    store.acknowledge_message_id(self.channel_id, latest_message_id, cx);
-                });
-            }
+                .is_none_or(|acknowledged_id| acknowledged_id < latest_message_id)
+        {
+            self.rpc
+                .send(proto::AckChannelMessage {
+                    channel_id: self.channel_id.0,
+                    message_id: latest_message_id,
+                })
+                .ok();
+            self.last_acknowledged_id = Some(latest_message_id);
+            self.channel_store.update(cx, |store, cx| {
+                store.acknowledge_message_id(self.channel_id, latest_message_id, cx);
+            });
         }
     }
 
@@ -407,10 +406,10 @@ impl ChannelChat {
         let missing_ancestors = loaded_messages
             .iter()
             .filter_map(|message| {
-                if let Some(ancestor_id) = message.reply_to_message_id {
-                    if !loaded_message_ids.contains(&ancestor_id) {
-                        return Some(ancestor_id);
-                    }
+                if let Some(ancestor_id) = message.reply_to_message_id
+                    && !loaded_message_ids.contains(&ancestor_id)
+                {
+                    return Some(ancestor_id);
                 }
                 None
             })
@@ -533,7 +532,7 @@ impl ChannelChat {
         message: TypedEnvelope<proto::ChannelMessageSent>,
         mut cx: AsyncApp,
     ) -> Result<()> {
-        let user_store = this.read_with(&mut cx, |this, _| this.user_store.clone())?;
+        let user_store = this.read_with(&cx, |this, _| this.user_store.clone())?;
         let message = message.payload.message.context("empty message")?;
         let message_id = message.id;
 
@@ -565,7 +564,7 @@ impl ChannelChat {
         message: TypedEnvelope<proto::ChannelMessageUpdate>,
         mut cx: AsyncApp,
     ) -> Result<()> {
-        let user_store = this.read_with(&mut cx, |this, _| this.user_store.clone())?;
+        let user_store = this.read_with(&cx, |this, _| this.user_store.clone())?;
         let message = message.payload.message.context("empty message")?;
 
         let message = ChannelMessage::from_proto(message, &user_store, &mut cx).await?;
@@ -613,7 +612,7 @@ impl ChannelChat {
                 while let Some(message) = old_cursor.item() {
                     let message_ix = old_cursor.start().1.0;
                     if nonces.contains(&message.nonce) {
-                        if ranges.last().map_or(false, |r| r.end == message_ix) {
+                        if ranges.last().is_some_and(|r| r.end == message_ix) {
                             ranges.last_mut().unwrap().end += 1;
                         } else {
                             ranges.push(message_ix..message_ix + 1);
@@ -646,32 +645,32 @@ impl ChannelChat {
     fn message_removed(&mut self, id: u64, cx: &mut Context<Self>) {
         let mut cursor = self.messages.cursor::<ChannelMessageId>(&());
         let mut messages = cursor.slice(&ChannelMessageId::Saved(id), Bias::Left);
-        if let Some(item) = cursor.item() {
-            if item.id == ChannelMessageId::Saved(id) {
-                let deleted_message_ix = messages.summary().count;
-                cursor.next();
-                messages.append(cursor.suffix(), &());
-                drop(cursor);
-                self.messages = messages;
-
-                // If the message that was deleted was the last acknowledged message,
-                // replace the acknowledged message with an earlier one.
-                self.channel_store.update(cx, |store, _| {
-                    let summary = self.messages.summary();
-                    if summary.count == 0 {
-                        store.set_acknowledged_message_id(self.channel_id, None);
-                    } else if deleted_message_ix == summary.count {
-                        if let ChannelMessageId::Saved(id) = summary.max_id {
-                            store.set_acknowledged_message_id(self.channel_id, Some(id));
-                        }
-                    }
-                });
+        if let Some(item) = cursor.item()
+            && item.id == ChannelMessageId::Saved(id)
+        {
+            let deleted_message_ix = messages.summary().count;
+            cursor.next();
+            messages.append(cursor.suffix(), &());
+            drop(cursor);
+            self.messages = messages;
+
+            // If the message that was deleted was the last acknowledged message,
+            // replace the acknowledged message with an earlier one.
+            self.channel_store.update(cx, |store, _| {
+                let summary = self.messages.summary();
+                if summary.count == 0 {
+                    store.set_acknowledged_message_id(self.channel_id, None);
+                } else if deleted_message_ix == summary.count
+                    && let ChannelMessageId::Saved(id) = summary.max_id
+                {
+                    store.set_acknowledged_message_id(self.channel_id, Some(id));
+                }
+            });
 
-                cx.emit(ChannelChatEvent::MessagesUpdated {
-                    old_range: deleted_message_ix..deleted_message_ix + 1,
-                    new_count: 0,
-                });
-            }
+            cx.emit(ChannelChatEvent::MessagesUpdated {
+                old_range: deleted_message_ix..deleted_message_ix + 1,
+                new_count: 0,
+            });
         }
     }
 

crates/channel/src/channel_store.rs 🔗

@@ -262,13 +262,12 @@ impl ChannelStore {
                         }
                     }
                     status = status_receiver.next().fuse() => {
-                        if let Some(status) = status {
-                            if status.is_connected() {
+                        if let Some(status) = status
+                            && status.is_connected() {
                                 this.update(cx, |this, _cx| {
                                     this.initialize();
                                 }).ok();
                             }
-                        }
                         continue;
                     }
                     _ = timer => {
@@ -336,10 +335,10 @@ impl ChannelStore {
     }
 
     pub fn has_open_channel_buffer(&self, channel_id: ChannelId, _cx: &App) -> bool {
-        if let Some(buffer) = self.opened_buffers.get(&channel_id) {
-            if let OpenEntityHandle::Open(buffer) = buffer {
-                return buffer.upgrade().is_some();
-            }
+        if let Some(buffer) = self.opened_buffers.get(&channel_id)
+            && let OpenEntityHandle::Open(buffer) = buffer
+        {
+            return buffer.upgrade().is_some();
         }
         false
     }
@@ -408,13 +407,12 @@ impl ChannelStore {
 
     pub fn last_acknowledge_message_id(&self, channel_id: ChannelId) -> Option<u64> {
         self.channel_states.get(&channel_id).and_then(|state| {
-            if let Some(last_message_id) = state.latest_chat_message {
-                if state
+            if let Some(last_message_id) = state.latest_chat_message
+                && state
                     .last_acknowledged_message_id()
                     .is_some_and(|id| id < last_message_id)
-                {
-                    return state.last_acknowledged_message_id();
-                }
+            {
+                return state.last_acknowledged_message_id();
             }
 
             None
@@ -570,16 +568,14 @@ impl ChannelStore {
         self.channel_index
             .by_id()
             .get(&channel_id)
-            .map_or(false, |channel| channel.is_root_channel())
+            .is_some_and(|channel| channel.is_root_channel())
     }
 
     pub fn is_public_channel(&self, channel_id: ChannelId) -> bool {
         self.channel_index
             .by_id()
             .get(&channel_id)
-            .map_or(false, |channel| {
-                channel.visibility == ChannelVisibility::Public
-            })
+            .is_some_and(|channel| channel.visibility == ChannelVisibility::Public)
     }
 
     pub fn channel_capability(&self, channel_id: ChannelId) -> Capability {
@@ -910,9 +906,9 @@ impl ChannelStore {
     async fn handle_update_channels(
         this: Entity<Self>,
         message: TypedEnvelope<proto::UpdateChannels>,
-        mut cx: AsyncApp,
+        cx: AsyncApp,
     ) -> Result<()> {
-        this.read_with(&mut cx, |this, _| {
+        this.read_with(&cx, |this, _| {
             this.update_channels_tx
                 .unbounded_send(message.payload)
                 .unwrap();
@@ -962,27 +958,27 @@ impl ChannelStore {
         self.disconnect_channel_buffers_task.take();
 
         for chat in self.opened_chats.values() {
-            if let OpenEntityHandle::Open(chat) = chat {
-                if let Some(chat) = chat.upgrade() {
-                    chat.update(cx, |chat, cx| {
-                        chat.rejoin(cx);
-                    });
-                }
+            if let OpenEntityHandle::Open(chat) = chat
+                && let Some(chat) = chat.upgrade()
+            {
+                chat.update(cx, |chat, cx| {
+                    chat.rejoin(cx);
+                });
             }
         }
 
         let mut buffer_versions = Vec::new();
         for buffer in self.opened_buffers.values() {
-            if let OpenEntityHandle::Open(buffer) = buffer {
-                if let Some(buffer) = buffer.upgrade() {
-                    let channel_buffer = buffer.read(cx);
-                    let buffer = channel_buffer.buffer().read(cx);
-                    buffer_versions.push(proto::ChannelBufferVersion {
-                        channel_id: channel_buffer.channel_id.0,
-                        epoch: channel_buffer.epoch(),
-                        version: language::proto::serialize_version(&buffer.version()),
-                    });
-                }
+            if let OpenEntityHandle::Open(buffer) = buffer
+                && let Some(buffer) = buffer.upgrade()
+            {
+                let channel_buffer = buffer.read(cx);
+                let buffer = channel_buffer.buffer().read(cx);
+                buffer_versions.push(proto::ChannelBufferVersion {
+                    channel_id: channel_buffer.channel_id.0,
+                    epoch: channel_buffer.epoch(),
+                    version: language::proto::serialize_version(&buffer.version()),
+                });
             }
         }
 
@@ -1077,11 +1073,11 @@ impl ChannelStore {
 
                 if let Some(this) = this.upgrade() {
                     this.update(cx, |this, cx| {
-                        for (_, buffer) in &this.opened_buffers {
-                            if let OpenEntityHandle::Open(buffer) = &buffer {
-                                if let Some(buffer) = buffer.upgrade() {
-                                    buffer.update(cx, |buffer, cx| buffer.disconnect(cx));
-                                }
+                        for buffer in this.opened_buffers.values() {
+                            if let OpenEntityHandle::Open(buffer) = &buffer
+                                && let Some(buffer) = buffer.upgrade()
+                            {
+                                buffer.update(cx, |buffer, cx| buffer.disconnect(cx));
                             }
                         }
                     })
@@ -1157,10 +1153,9 @@ impl ChannelStore {
                     }
                     if let Some(OpenEntityHandle::Open(buffer)) =
                         self.opened_buffers.remove(&channel_id)
+                        && let Some(buffer) = buffer.upgrade()
                     {
-                        if let Some(buffer) = buffer.upgrade() {
-                            buffer.update(cx, ChannelBuffer::disconnect);
-                        }
+                        buffer.update(cx, ChannelBuffer::disconnect);
                     }
                 }
             }
@@ -1170,12 +1165,11 @@ impl ChannelStore {
                 let id = ChannelId(channel.id);
                 let channel_changed = index.insert(channel);
 
-                if channel_changed {
-                    if let Some(OpenEntityHandle::Open(buffer)) = self.opened_buffers.get(&id) {
-                        if let Some(buffer) = buffer.upgrade() {
-                            buffer.update(cx, ChannelBuffer::channel_changed);
-                        }
-                    }
+                if channel_changed
+                    && let Some(OpenEntityHandle::Open(buffer)) = self.opened_buffers.get(&id)
+                    && let Some(buffer) = buffer.upgrade()
+                {
+                    buffer.update(cx, ChannelBuffer::channel_changed);
                 }
             }
 

crates/channel/src/channel_store_tests.rs 🔗

@@ -438,7 +438,7 @@ fn init_test(cx: &mut App) -> Entity<ChannelStore> {
 
     let clock = Arc::new(FakeSystemClock::new());
     let http = FakeHttpClient::with_404_response();
-    let client = Client::new(clock, http.clone(), cx);
+    let client = Client::new(clock, http, cx);
     let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
 
     client::init(&client, cx);

crates/cli/src/main.rs 🔗

@@ -363,7 +363,7 @@ fn anonymous_fd(path: &str) -> Option<fs::File> {
 
         let fd: fd::RawFd = fd_str.parse().ok()?;
         let file = unsafe { fs::File::from_raw_fd(fd) };
-        return Some(file);
+        Some(file)
     }
     #[cfg(any(target_os = "macos", target_os = "freebsd"))]
     {
@@ -381,13 +381,13 @@ fn anonymous_fd(path: &str) -> Option<fs::File> {
         }
         let fd: fd::RawFd = fd_str.parse().ok()?;
         let file = unsafe { fs::File::from_raw_fd(fd) };
-        return Some(file);
+        Some(file)
     }
     #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "freebsd")))]
     {
         _ = path;
         // not implemented for bsd, windows. Could be, but isn't yet
-        return None;
+        None
     }
 }
 
@@ -494,11 +494,11 @@ mod linux {
                 Ok(Fork::Parent(_)) => Ok(()),
                 Ok(Fork::Child) => {
                     unsafe { std::env::set_var(FORCE_CLI_MODE_ENV_VAR_NAME, "") };
-                    if let Err(_) = fork::setsid() {
+                    if fork::setsid().is_err() {
                         eprintln!("failed to setsid: {}", std::io::Error::last_os_error());
                         process::exit(1);
                     }
-                    if let Err(_) = fork::close_fd() {
+                    if fork::close_fd().is_err() {
                         eprintln!("failed to close_fd: {}", std::io::Error::last_os_error());
                     }
                     let error =
@@ -518,11 +518,11 @@ mod linux {
         ) -> Result<(), std::io::Error> {
             for _ in 0..100 {
                 thread::sleep(Duration::from_millis(10));
-                if sock.connect_addr(&sock_addr).is_ok() {
+                if sock.connect_addr(sock_addr).is_ok() {
                     return Ok(());
                 }
             }
-            sock.connect_addr(&sock_addr)
+            sock.connect_addr(sock_addr)
         }
     }
 }
@@ -534,8 +534,8 @@ mod flatpak {
     use std::process::Command;
     use std::{env, process};
 
-    const EXTRA_LIB_ENV_NAME: &'static str = "ZED_FLATPAK_LIB_PATH";
-    const NO_ESCAPE_ENV_NAME: &'static str = "ZED_FLATPAK_NO_ESCAPE";
+    const EXTRA_LIB_ENV_NAME: &str = "ZED_FLATPAK_LIB_PATH";
+    const NO_ESCAPE_ENV_NAME: &str = "ZED_FLATPAK_NO_ESCAPE";
 
     /// Adds bundled libraries to LD_LIBRARY_PATH if running under flatpak
     pub fn ld_extra_libs() {
@@ -586,14 +586,11 @@ mod flatpak {
 
     pub fn set_bin_if_no_escape(mut args: super::Args) -> super::Args {
         if env::var(NO_ESCAPE_ENV_NAME).is_ok()
-            && env::var("FLATPAK_ID").map_or(false, |id| id.starts_with("dev.zed.Zed"))
+            && env::var("FLATPAK_ID").is_ok_and(|id| id.starts_with("dev.zed.Zed"))
+            && args.zed.is_none()
         {
-            if args.zed.is_none() {
-                args.zed = Some("/app/libexec/zed-editor".into());
-                unsafe {
-                    env::set_var("ZED_UPDATE_EXPLANATION", "Please use flatpak to update zed")
-                };
-            }
+            args.zed = Some("/app/libexec/zed-editor".into());
+            unsafe { env::set_var("ZED_UPDATE_EXPLANATION", "Please use flatpak to update zed") };
         }
         args
     }
@@ -929,7 +926,7 @@ mod mac_os {
 
         fn path(&self) -> PathBuf {
             match self {
-                Bundle::App { app_bundle, .. } => app_bundle.join("Contents/MacOS/zed").clone(),
+                Bundle::App { app_bundle, .. } => app_bundle.join("Contents/MacOS/zed"),
                 Bundle::LocalPath { executable, .. } => executable.clone(),
             }
         }
@@ -957,17 +954,14 @@ mod mac_os {
     ) -> Result<()> {
         use anyhow::bail;
 
-        let app_id_prompt = format!("id of app \"{}\"", channel.display_name());
-        let app_id_output = Command::new("osascript")
+        let app_path_prompt = format!(
+            "POSIX path of (path to application \"{}\")",
+            channel.display_name()
+        );
+        let app_path_output = Command::new("osascript")
             .arg("-e")
-            .arg(&app_id_prompt)
+            .arg(&app_path_prompt)
             .output()?;
-        if !app_id_output.status.success() {
-            bail!("Could not determine app id for {}", channel.display_name());
-        }
-        let app_name = String::from_utf8(app_id_output.stdout)?.trim().to_owned();
-        let app_path_prompt = format!("kMDItemCFBundleIdentifier == '{app_name}'");
-        let app_path_output = Command::new("mdfind").arg(app_path_prompt).output()?;
         if !app_path_output.status.success() {
             bail!(
                 "Could not determine app path for {}",

crates/client/Cargo.toml 🔗

@@ -44,6 +44,7 @@ rpc = { workspace = true, features = ["gpui"] }
 schemars.workspace = true
 serde.workspace = true
 serde_json.workspace = true
+serde_urlencoded.workspace = true
 settings.workspace = true
 sha2.workspace = true
 smol.workspace = true

crates/client/src/client.rs 🔗

@@ -76,7 +76,7 @@ pub static ZED_APP_PATH: LazyLock<Option<PathBuf>> =
     LazyLock::new(|| std::env::var("ZED_APP_PATH").ok().map(PathBuf::from));
 
 pub static ZED_ALWAYS_ACTIVE: LazyLock<bool> =
-    LazyLock::new(|| std::env::var("ZED_ALWAYS_ACTIVE").map_or(false, |e| !e.is_empty()));
+    LazyLock::new(|| std::env::var("ZED_ALWAYS_ACTIVE").is_ok_and(|e| !e.is_empty()));
 
 pub const INITIAL_RECONNECTION_DELAY: Duration = Duration::from_millis(500);
 pub const MAX_RECONNECTION_DELAY: Duration = Duration::from_secs(30);
@@ -162,7 +162,7 @@ pub fn init(client: &Arc<Client>, cx: &mut App) {
         let client = client.clone();
         move |_: &SignIn, cx| {
             if let Some(client) = client.upgrade() {
-                cx.spawn(async move |cx| client.sign_in_with_optional_connect(true, &cx).await)
+                cx.spawn(async move |cx| client.sign_in_with_optional_connect(true, cx).await)
                     .detach_and_log_err(cx);
             }
         }
@@ -173,7 +173,7 @@ pub fn init(client: &Arc<Client>, cx: &mut App) {
         move |_: &SignOut, cx| {
             if let Some(client) = client.upgrade() {
                 cx.spawn(async move |cx| {
-                    client.sign_out(&cx).await;
+                    client.sign_out(cx).await;
                 })
                 .detach();
             }
@@ -181,11 +181,11 @@ pub fn init(client: &Arc<Client>, cx: &mut App) {
     });
 
     cx.on_action({
-        let client = client.clone();
+        let client = client;
         move |_: &Reconnect, cx| {
             if let Some(client) = client.upgrade() {
                 cx.spawn(async move |cx| {
-                    client.reconnect(&cx);
+                    client.reconnect(cx);
                 })
                 .detach();
             }
@@ -677,7 +677,7 @@ impl Client {
 
                     let mut delay = INITIAL_RECONNECTION_DELAY;
                     loop {
-                        match client.connect(true, &cx).await {
+                        match client.connect(true, cx).await {
                             ConnectionResult::Timeout => {
                                 log::error!("client connect attempt timed out")
                             }
@@ -701,7 +701,7 @@ impl Client {
                                 Status::ReconnectionError {
                                     next_reconnection: Instant::now() + delay,
                                 },
-                                &cx,
+                                cx,
                             );
                             let jitter =
                                 Duration::from_millis(rng.gen_range(0..delay.as_millis() as u64));
@@ -791,7 +791,7 @@ impl Client {
             Arc::new(move |subscriber, envelope, client, cx| {
                 let subscriber = subscriber.downcast::<E>().unwrap();
                 let envelope = envelope.into_any().downcast::<TypedEnvelope<M>>().unwrap();
-                handler(subscriber, *envelope, client.clone(), cx).boxed_local()
+                handler(subscriber, *envelope, client, cx).boxed_local()
             }),
         );
         if prev_handler.is_some() {
@@ -864,22 +864,23 @@ impl Client {
         let mut credentials = None;
 
         let old_credentials = self.state.read().credentials.clone();
-        if let Some(old_credentials) = old_credentials {
-            if self.validate_credentials(&old_credentials, cx).await? {
-                credentials = Some(old_credentials);
-            }
+        if let Some(old_credentials) = old_credentials
+            && self.validate_credentials(&old_credentials, cx).await?
+        {
+            credentials = Some(old_credentials);
         }
 
-        if credentials.is_none() && try_provider {
-            if let Some(stored_credentials) = self.credentials_provider.read_credentials(cx).await {
-                if self.validate_credentials(&stored_credentials, cx).await? {
-                    credentials = Some(stored_credentials);
-                } else {
-                    self.credentials_provider
-                        .delete_credentials(cx)
-                        .await
-                        .log_err();
-                }
+        if credentials.is_none()
+            && try_provider
+            && let Some(stored_credentials) = self.credentials_provider.read_credentials(cx).await
+        {
+            if self.validate_credentials(&stored_credentials, cx).await? {
+                credentials = Some(stored_credentials);
+            } else {
+                self.credentials_provider
+                    .delete_credentials(cx)
+                    .await
+                    .log_err();
             }
         }
 
@@ -973,6 +974,11 @@ impl Client {
         try_provider: bool,
         cx: &AsyncApp,
     ) -> Result<()> {
+        // Don't try to sign in again if we're already connected to Collab, as it will temporarily disconnect us.
+        if self.status().borrow().is_connected() {
+            return Ok(());
+        }
+
         let (is_staff_tx, is_staff_rx) = oneshot::channel::<bool>();
         let mut is_staff_tx = Some(is_staff_tx);
         cx.update(|cx| {
@@ -1023,11 +1029,11 @@ impl Client {
             Status::SignedOut | Status::Authenticated => true,
             Status::ConnectionError
             | Status::ConnectionLost
-            | Status::Authenticating { .. }
+            | Status::Authenticating
             | Status::AuthenticationError
-            | Status::Reauthenticating { .. }
+            | Status::Reauthenticating
             | Status::ReconnectionError { .. } => false,
-            Status::Connected { .. } | Status::Connecting { .. } | Status::Reconnecting { .. } => {
+            Status::Connected { .. } | Status::Connecting | Status::Reconnecting => {
                 return ConnectionResult::Result(Ok(()));
             }
             Status::UpgradeRequired => {
@@ -1151,7 +1157,7 @@ impl Client {
             let this = self.clone();
             async move |cx| {
                 while let Some(message) = incoming.next().await {
-                    this.handle_message(message, &cx);
+                    this.handle_message(message, cx);
                     // Don't starve the main thread when receiving lots of messages at once.
                     smol::future::yield_now().await;
                 }
@@ -1169,12 +1175,12 @@ impl Client {
                             peer_id,
                         })
                     {
-                        this.set_status(Status::SignedOut, &cx);
+                        this.set_status(Status::SignedOut, cx);
                     }
                 }
                 Err(err) => {
                     log::error!("connection error: {:?}", err);
-                    this.set_status(Status::ConnectionLost, &cx);
+                    this.set_status(Status::ConnectionLost, cx);
                 }
             }
         })
@@ -1410,6 +1416,12 @@ impl Client {
 
                     open_url_tx.send(url).log_err();
 
+                    #[derive(Deserialize)]
+                    struct CallbackParams {
+                        pub user_id: String,
+                        pub access_token: String,
+                    }
+
                     // Receive the HTTP request from the user's browser. Retrieve the user id and encrypted
                     // access token from the query params.
                     //
@@ -1420,17 +1432,13 @@ impl Client {
                             for _ in 0..100 {
                                 if let Some(req) = server.recv_timeout(Duration::from_secs(1))? {
                                     let path = req.url();
-                                    let mut user_id = None;
-                                    let mut access_token = None;
                                     let url = Url::parse(&format!("http://example.com{}", path))
                                         .context("failed to parse login notification url")?;
-                                    for (key, value) in url.query_pairs() {
-                                        if key == "access_token" {
-                                            access_token = Some(value.to_string());
-                                        } else if key == "user_id" {
-                                            user_id = Some(value.to_string());
-                                        }
-                                    }
+                                    let callback_params: CallbackParams =
+                                        serde_urlencoded::from_str(url.query().unwrap_or_default())
+                                            .context(
+                                                "failed to parse sign-in callback query parameters",
+                                            )?;
 
                                     let post_auth_url =
                                         http.build_url("/native_app_signin_succeeded");
@@ -1445,8 +1453,8 @@ impl Client {
                                     )
                                     .context("failed to respond to login http request")?;
                                     return Ok((
-                                        user_id.context("missing user_id parameter")?,
-                                        access_token.context("missing access_token parameter")?,
+                                        callback_params.user_id,
+                                        callback_params.access_token,
                                     ));
                                 }
                             }
@@ -1894,10 +1902,7 @@ mod tests {
         assert!(matches!(status.next().await, Some(Status::Connecting)));
 
         executor.advance_clock(CONNECTION_TIMEOUT);
-        assert!(matches!(
-            status.next().await,
-            Some(Status::ConnectionError { .. })
-        ));
+        assert!(matches!(status.next().await, Some(Status::ConnectionError)));
         auth_and_connect.await.into_response().unwrap_err();
 
         // Allow the connection to be established.
@@ -1921,10 +1926,7 @@ mod tests {
             })
         });
         executor.advance_clock(2 * INITIAL_RECONNECTION_DELAY);
-        assert!(matches!(
-            status.next().await,
-            Some(Status::Reconnecting { .. })
-        ));
+        assert!(matches!(status.next().await, Some(Status::Reconnecting)));
 
         executor.advance_clock(CONNECTION_TIMEOUT);
         assert!(matches!(
@@ -2040,10 +2042,7 @@ mod tests {
         assert_eq!(*auth_count.lock(), 1);
         assert_eq!(*dropped_auth_count.lock(), 0);
 
-        let _authenticate = cx.spawn({
-            let client = client.clone();
-            |cx| async move { client.connect(false, &cx).await }
-        });
+        let _authenticate = cx.spawn(|cx| async move { client.connect(false, &cx).await });
         executor.run_until_parked();
         assert_eq!(*auth_count.lock(), 2);
         assert_eq!(*dropped_auth_count.lock(), 1);
@@ -2065,8 +2064,8 @@ mod tests {
         let (done_tx1, done_rx1) = smol::channel::unbounded();
         let (done_tx2, done_rx2) = smol::channel::unbounded();
         AnyProtoClient::from(client.clone()).add_entity_message_handler(
-            move |entity: Entity<TestEntity>, _: TypedEnvelope<proto::JoinProject>, mut cx| {
-                match entity.read_with(&mut cx, |entity, _| entity.id).unwrap() {
+            move |entity: Entity<TestEntity>, _: TypedEnvelope<proto::JoinProject>, cx| {
+                match entity.read_with(&cx, |entity, _| entity.id).unwrap() {
                     1 => done_tx1.try_send(()).unwrap(),
                     2 => done_tx2.try_send(()).unwrap(),
                     _ => unreachable!(),
@@ -2090,17 +2089,17 @@ mod tests {
         let _subscription1 = client
             .subscribe_to_entity(1)
             .unwrap()
-            .set_entity(&entity1, &mut cx.to_async());
+            .set_entity(&entity1, &cx.to_async());
         let _subscription2 = client
             .subscribe_to_entity(2)
             .unwrap()
-            .set_entity(&entity2, &mut cx.to_async());
+            .set_entity(&entity2, &cx.to_async());
         // Ensure dropping a subscription for the same entity type still allows receiving of
         // messages for other entity IDs of the same type.
         let subscription3 = client
             .subscribe_to_entity(3)
             .unwrap()
-            .set_entity(&entity3, &mut cx.to_async());
+            .set_entity(&entity3, &cx.to_async());
         drop(subscription3);
 
         server.send(proto::JoinProject {

crates/client/src/telemetry.rs 🔗

@@ -340,22 +340,35 @@ impl Telemetry {
     }
 
     pub fn log_edit_event(self: &Arc<Self>, environment: &'static str, is_via_ssh: bool) {
+        static LAST_EVENT_TIME: Mutex<Option<Instant>> = Mutex::new(None);
+
         let mut state = self.state.lock();
         let period_data = state.event_coalescer.log_event(environment);
         drop(state);
 
-        if let Some((start, end, environment)) = period_data {
-            let duration = end
-                .saturating_duration_since(start)
-                .min(Duration::from_secs(60 * 60 * 24))
-                .as_millis() as i64;
+        if let Some(mut last_event) = LAST_EVENT_TIME.try_lock() {
+            let current_time = std::time::Instant::now();
+            let last_time = last_event.get_or_insert(current_time);
 
-            telemetry::event!(
-                "Editor Edited",
-                duration = duration,
-                environment = environment,
-                is_via_ssh = is_via_ssh
-            );
+            if current_time.duration_since(*last_time) > Duration::from_secs(60 * 10) {
+                *last_time = current_time;
+            } else {
+                return;
+            }
+
+            if let Some((start, end, environment)) = period_data {
+                let duration = end
+                    .saturating_duration_since(start)
+                    .min(Duration::from_secs(60 * 60 * 24))
+                    .as_millis() as i64;
+
+                telemetry::event!(
+                    "Editor Edited",
+                    duration = duration,
+                    environment = environment,
+                    is_via_ssh = is_via_ssh
+                );
+            }
         }
     }
 
@@ -726,7 +739,7 @@ mod tests {
         );
 
         // Third scan of worktree does not double report, as we already reported
-        test_project_discovery_helper(telemetry.clone(), vec!["package.json"], None, worktree_id);
+        test_project_discovery_helper(telemetry, vec!["package.json"], None, worktree_id);
     }
 
     #[gpui::test]
@@ -738,7 +751,7 @@ mod tests {
         let telemetry = cx.update(|cx| Telemetry::new(clock.clone(), http, cx));
 
         test_project_discovery_helper(
-            telemetry.clone(),
+            telemetry,
             vec!["package.json", "pnpm-lock.yaml"],
             Some(vec!["node", "pnpm"]),
             1,
@@ -754,7 +767,7 @@ mod tests {
         let telemetry = cx.update(|cx| Telemetry::new(clock.clone(), http, cx));
 
         test_project_discovery_helper(
-            telemetry.clone(),
+            telemetry,
             vec!["package.json", "yarn.lock"],
             Some(vec!["node", "yarn"]),
             1,
@@ -773,7 +786,7 @@ mod tests {
         // project type for the same worktree multiple times
 
         test_project_discovery_helper(
-            telemetry.clone().clone(),
+            telemetry.clone(),
             vec!["global.json"],
             Some(vec!["dotnet"]),
             1,

crates/client/src/test.rs 🔗

@@ -1,16 +1,12 @@
 use crate::{Client, Connection, Credentials, EstablishConnectionError, UserStore};
 use anyhow::{Context as _, Result, anyhow};
-use chrono::Duration;
 use cloud_api_client::{AuthenticatedUser, GetAuthenticatedUserResponse, PlanInfo};
 use cloud_llm_client::{CurrentUsage, Plan, UsageData, UsageLimit};
 use futures::{StreamExt, stream::BoxStream};
 use gpui::{AppContext as _, BackgroundExecutor, Entity, TestAppContext};
 use http_client::{AsyncBody, Method, Request, http};
 use parking_lot::Mutex;
-use rpc::{
-    ConnectionId, Peer, Receipt, TypedEnvelope,
-    proto::{self, GetPrivateUserInfo, GetPrivateUserInfoResponse},
-};
+use rpc::{ConnectionId, Peer, Receipt, TypedEnvelope, proto};
 use std::sync::Arc;
 
 pub struct FakeServer {
@@ -187,50 +183,27 @@ impl FakeServer {
     pub async fn receive<M: proto::EnvelopedMessage>(&self) -> Result<TypedEnvelope<M>> {
         self.executor.start_waiting();
 
-        loop {
-            let message = self
-                .state
-                .lock()
-                .incoming
-                .as_mut()
-                .expect("not connected")
-                .next()
-                .await
-                .context("other half hung up")?;
-            self.executor.finish_waiting();
-            let type_name = message.payload_type_name();
-            let message = message.into_any();
-
-            if message.is::<TypedEnvelope<M>>() {
-                return Ok(*message.downcast().unwrap());
-            }
-
-            let accepted_tos_at = chrono::Utc::now()
-                .checked_sub_signed(Duration::hours(5))
-                .expect("failed to build accepted_tos_at")
-                .timestamp() as u64;
-
-            if message.is::<TypedEnvelope<GetPrivateUserInfo>>() {
-                self.respond(
-                    message
-                        .downcast::<TypedEnvelope<GetPrivateUserInfo>>()
-                        .unwrap()
-                        .receipt(),
-                    GetPrivateUserInfoResponse {
-                        metrics_id: "the-metrics-id".into(),
-                        staff: false,
-                        flags: Default::default(),
-                        accepted_tos_at: Some(accepted_tos_at),
-                    },
-                );
-                continue;
-            }
+        let message = self
+            .state
+            .lock()
+            .incoming
+            .as_mut()
+            .expect("not connected")
+            .next()
+            .await
+            .context("other half hung up")?;
+        self.executor.finish_waiting();
+        let type_name = message.payload_type_name();
+        let message = message.into_any();
 
-            panic!(
-                "fake server received unexpected message type: {:?}",
-                type_name
-            );
+        if message.is::<TypedEnvelope<M>>() {
+            return Ok(*message.downcast().unwrap());
         }
+
+        panic!(
+            "fake server received unexpected message type: {:?}",
+            type_name
+        );
     }
 
     pub fn respond<T: proto::RequestMessage>(&self, receipt: Receipt<T>, response: T::Response) {

crates/client/src/user.rs 🔗

@@ -41,7 +41,7 @@ impl std::fmt::Display for ChannelId {
 pub struct ProjectId(pub u64);
 
 impl ProjectId {
-    pub fn to_proto(&self) -> u64 {
+    pub fn to_proto(self) -> u64 {
         self.0
     }
 }
@@ -177,7 +177,6 @@ impl UserStore {
         let (mut current_user_tx, current_user_rx) = watch::channel();
         let (update_contacts_tx, mut update_contacts_rx) = mpsc::unbounded();
         let rpc_subscriptions = vec![
-            client.add_message_handler(cx.weak_entity(), Self::handle_update_plan),
             client.add_message_handler(cx.weak_entity(), Self::handle_update_contacts),
             client.add_message_handler(cx.weak_entity(), Self::handle_update_invite_info),
             client.add_message_handler(cx.weak_entity(), Self::handle_show_contacts),
@@ -226,17 +225,35 @@ impl UserStore {
                     match status {
                         Status::Authenticated | Status::Connected { .. } => {
                             if let Some(user_id) = client.user_id() {
-                                let response = client.cloud_client().get_authenticated_user().await;
-                                let mut current_user = None;
+                                let response = client
+                                    .cloud_client()
+                                    .get_authenticated_user()
+                                    .await
+                                    .log_err();
+
+                                let current_user_and_response = if let Some(response) = response {
+                                    let user = Arc::new(User {
+                                        id: user_id,
+                                        github_login: response.user.github_login.clone().into(),
+                                        avatar_uri: response.user.avatar_url.clone().into(),
+                                        name: response.user.name.clone(),
+                                    });
+
+                                    Some((user, response))
+                                } else {
+                                    None
+                                };
+                                current_user_tx
+                                    .send(
+                                        current_user_and_response
+                                            .as_ref()
+                                            .map(|(user, _)| user.clone()),
+                                    )
+                                    .await
+                                    .ok();
+
                                 cx.update(|cx| {
-                                    if let Some(response) = response.log_err() {
-                                        let user = Arc::new(User {
-                                            id: user_id,
-                                            github_login: response.user.github_login.clone().into(),
-                                            avatar_uri: response.user.avatar_url.clone().into(),
-                                            name: response.user.name.clone(),
-                                        });
-                                        current_user = Some(user.clone());
+                                    if let Some((user, response)) = current_user_and_response {
                                         this.update(cx, |this, cx| {
                                             this.by_github_login
                                                 .insert(user.github_login.clone(), user_id);
@@ -247,7 +264,6 @@ impl UserStore {
                                         anyhow::Ok(())
                                     }
                                 })??;
-                                current_user_tx.send(current_user).await.ok();
 
                                 this.update(cx, |_, cx| cx.notify())?;
                             }
@@ -316,9 +332,9 @@ impl UserStore {
     async fn handle_update_contacts(
         this: Entity<Self>,
         message: TypedEnvelope<proto::UpdateContacts>,
-        mut cx: AsyncApp,
+        cx: AsyncApp,
     ) -> Result<()> {
-        this.read_with(&mut cx, |this, _| {
+        this.read_with(&cx, |this, _| {
             this.update_contacts_tx
                 .unbounded_send(UpdateContacts::Update(message.payload))
                 .unwrap();
@@ -326,26 +342,6 @@ impl UserStore {
         Ok(())
     }
 
-    async fn handle_update_plan(
-        this: Entity<Self>,
-        _message: TypedEnvelope<proto::UpdateUserPlan>,
-        mut cx: AsyncApp,
-    ) -> Result<()> {
-        let client = this
-            .read_with(&cx, |this, _| this.client.upgrade())?
-            .context("client was dropped")?;
-
-        let response = client
-            .cloud_client()
-            .get_authenticated_user()
-            .await
-            .context("failed to fetch authenticated user")?;
-
-        this.update(&mut cx, |this, cx| {
-            this.update_authenticated_user(response, cx);
-        })
-    }
-
     fn update_contacts(&mut self, message: UpdateContacts, cx: &Context<Self>) -> Task<Result<()>> {
         match message {
             UpdateContacts::Wait(barrier) => {
@@ -852,7 +848,7 @@ impl UserStore {
 
     pub fn has_accepted_terms_of_service(&self) -> bool {
         self.accepted_tos_at
-            .map_or(false, |accepted_tos_at| accepted_tos_at.is_some())
+            .is_some_and(|accepted_tos_at| accepted_tos_at.is_some())
     }
 
     pub fn accept_terms_of_service(&self, cx: &Context<Self>) -> Task<Result<()>> {
@@ -898,10 +894,10 @@ impl UserStore {
         let mut ret = Vec::with_capacity(users.len());
         for user in users {
             let user = User::new(user);
-            if let Some(old) = self.users.insert(user.id, user.clone()) {
-                if old.github_login != user.github_login {
-                    self.by_github_login.remove(&old.github_login);
-                }
+            if let Some(old) = self.users.insert(user.id, user.clone())
+                && old.github_login != user.github_login
+            {
+                self.by_github_login.remove(&old.github_login);
             }
             self.by_github_login
                 .insert(user.github_login.clone(), user.id);
@@ -1002,19 +998,6 @@ impl RequestUsage {
         }
     }
 
-    pub fn from_proto(amount: u32, limit: proto::UsageLimit) -> Option<Self> {
-        let limit = match limit.variant? {
-            proto::usage_limit::Variant::Limited(limited) => {
-                UsageLimit::Limited(limited.limit as i32)
-            }
-            proto::usage_limit::Variant::Unlimited(_) => UsageLimit::Unlimited,
-        };
-        Some(RequestUsage {
-            limit,
-            amount: amount as i32,
-        })
-    }
-
     fn from_headers(
         limit_name: &str,
         amount_name: &str,

crates/client/src/zed_urls.rs 🔗

@@ -35,3 +35,11 @@ pub fn upgrade_to_zed_pro_url(cx: &App) -> String {
 pub fn terms_of_service(cx: &App) -> String {
     format!("{server_url}/terms-of-service", server_url = server_url(cx))
 }
+
+/// Returns the URL to Zed AI's privacy and security docs.
+pub fn ai_privacy_and_security(cx: &App) -> String {
+    format!(
+        "{server_url}/docs/ai/privacy-and-security",
+        server_url = server_url(cx)
+    )
+}

crates/cloud_api_client/src/cloud_api_client.rs 🔗

@@ -205,12 +205,12 @@ impl CloudApiClient {
             let mut body = String::new();
             response.body_mut().read_to_string(&mut body).await?;
             if response.status() == StatusCode::UNAUTHORIZED {
-                return Ok(false);
+                Ok(false)
             } else {
-                return Err(anyhow!(
+                Err(anyhow!(
                     "Failed to get authenticated user.\nStatus: {:?}\nBody: {body}",
                     response.status()
-                ));
+                ))
             }
         }
     }

crates/cloud_llm_client/src/cloud_llm_client.rs 🔗

@@ -263,12 +263,12 @@ pub struct WebSearchBody {
     pub query: String,
 }
 
-#[derive(Serialize, Deserialize, Clone)]
+#[derive(Debug, Serialize, Deserialize, Clone)]
 pub struct WebSearchResponse {
     pub results: Vec<WebSearchResult>,
 }
 
-#[derive(Serialize, Deserialize, Clone)]
+#[derive(Debug, Serialize, Deserialize, Clone)]
 pub struct WebSearchResult {
     pub title: String,
     pub url: String,

crates/collab/Cargo.toml 🔗

@@ -19,7 +19,6 @@ test-support = ["sqlite"]
 
 [dependencies]
 anyhow.workspace = true
-async-stripe.workspace = true
 async-trait.workspace = true
 async-tungstenite.workspace = true
 aws-config = { version = "1.1.5" }
@@ -30,16 +29,13 @@ axum-extra = { version = "0.4", features = ["erased-json"] }
 base64.workspace = true
 chrono.workspace = true
 clock.workspace = true
-cloud_llm_client.workspace = true
 collections.workspace = true
 dashmap.workspace = true
-derive_more.workspace = true
 envy = "0.4.2"
 futures.workspace = true
 gpui.workspace = true
 hex.workspace = true
 http_client.workspace = true
-jsonwebtoken.workspace = true
 livekit_api.workspace = true
 log.workspace = true
 nanoid.workspace = true
@@ -65,7 +61,6 @@ subtle.workspace = true
 supermaven_api.workspace = true
 telemetry_events.workspace = true
 text.workspace = true
-thiserror.workspace = true
 time.workspace = true
 tokio = { workspace = true, features = ["full"] }
 toml.workspace = true
@@ -136,6 +131,3 @@ util.workspace = true
 workspace = { workspace = true, features = ["test-support"] }
 worktree = { workspace = true, features = ["test-support"] }
 zlog.workspace = true
-
-[package.metadata.cargo-machete]
-ignored = ["async-stripe"]

crates/collab/k8s/collab.template.yml 🔗

@@ -219,12 +219,6 @@ spec:
                 secretKeyRef:
                   name: slack
                   key: panics_webhook
-            - name: STRIPE_API_KEY
-              valueFrom:
-                secretKeyRef:
-                  name: stripe
-                  key: api_key
-                  optional: true
             - name: COMPLETE_WITH_LANGUAGE_MODEL_RATE_LIMIT_PER_HOUR
               value: "1000"
             - name: SUPERMAVEN_ADMIN_API_KEY

crates/collab/migrations.sqlite/20221109000000_test_schema.sql 🔗

@@ -116,6 +116,7 @@ CREATE TABLE "project_repositories" (
     "scan_id" INTEGER NOT NULL,
     "is_deleted" BOOL NOT NULL,
     "current_merge_conflicts" VARCHAR,
+    "merge_message" VARCHAR,
     "branch_summary" VARCHAR,
     "head_commit_details" VARCHAR,
     PRIMARY KEY (project_id, id)
@@ -474,67 +475,6 @@ CREATE UNIQUE INDEX "index_extensions_external_id" ON "extensions" ("external_id
 
 CREATE INDEX "index_extensions_total_download_count" ON "extensions" ("total_download_count");
 
-CREATE TABLE rate_buckets (
-    user_id INT NOT NULL,
-    rate_limit_name VARCHAR(255) NOT NULL,
-    token_count INT NOT NULL,
-    last_refill TIMESTAMP WITHOUT TIME ZONE NOT NULL,
-    PRIMARY KEY (user_id, rate_limit_name),
-    FOREIGN KEY (user_id) REFERENCES users (id)
-);
-
-CREATE INDEX idx_user_id_rate_limit ON rate_buckets (user_id, rate_limit_name);
-
-CREATE TABLE IF NOT EXISTS billing_preferences (
-    id INTEGER PRIMARY KEY AUTOINCREMENT,
-    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    user_id INTEGER NOT NULL REFERENCES users (id),
-    max_monthly_llm_usage_spending_in_cents INTEGER NOT NULL,
-    model_request_overages_enabled bool NOT NULL DEFAULT FALSE,
-    model_request_overages_spend_limit_in_cents integer NOT NULL DEFAULT 0
-);
-
-CREATE UNIQUE INDEX "uix_billing_preferences_on_user_id" ON billing_preferences (user_id);
-
-CREATE TABLE IF NOT EXISTS billing_customers (
-    id INTEGER PRIMARY KEY AUTOINCREMENT,
-    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    user_id INTEGER NOT NULL REFERENCES users (id),
-    has_overdue_invoices BOOLEAN NOT NULL DEFAULT FALSE,
-    stripe_customer_id TEXT NOT NULL,
-    trial_started_at TIMESTAMP
-);
-
-CREATE UNIQUE INDEX "uix_billing_customers_on_user_id" ON billing_customers (user_id);
-
-CREATE UNIQUE INDEX "uix_billing_customers_on_stripe_customer_id" ON billing_customers (stripe_customer_id);
-
-CREATE TABLE IF NOT EXISTS billing_subscriptions (
-    id INTEGER PRIMARY KEY AUTOINCREMENT,
-    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    billing_customer_id INTEGER NOT NULL REFERENCES billing_customers (id),
-    stripe_subscription_id TEXT NOT NULL,
-    stripe_subscription_status TEXT NOT NULL,
-    stripe_cancel_at TIMESTAMP,
-    stripe_cancellation_reason TEXT,
-    kind TEXT,
-    stripe_current_period_start BIGINT,
-    stripe_current_period_end BIGINT
-);
-
-CREATE INDEX "ix_billing_subscriptions_on_billing_customer_id" ON billing_subscriptions (billing_customer_id);
-
-CREATE UNIQUE INDEX "uix_billing_subscriptions_on_stripe_subscription_id" ON billing_subscriptions (stripe_subscription_id);
-
-CREATE TABLE IF NOT EXISTS processed_stripe_events (
-    stripe_event_id TEXT PRIMARY KEY,
-    stripe_event_type TEXT NOT NULL,
-    stripe_event_created_timestamp INTEGER NOT NULL,
-    processed_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
-);
-
-CREATE INDEX "ix_processed_stripe_events_on_stripe_event_created_timestamp" ON processed_stripe_events (stripe_event_created_timestamp);
-
 CREATE TABLE IF NOT EXISTS "breakpoints" (
     "id" INTEGER PRIMARY KEY AUTOINCREMENT,
     "project_id" INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE,

crates/collab/src/api.rs 🔗

@@ -1,19 +1,11 @@
-pub mod billing;
 pub mod contributors;
 pub mod events;
 pub mod extensions;
 pub mod ips_file;
 pub mod slack;
 
-use crate::db::Database;
-use crate::{
-    AppState, Error, Result, auth,
-    db::{User, UserId},
-    rpc,
-};
-use ::rpc::proto;
+use crate::{AppState, Error, Result, auth, db::UserId, rpc};
 use anyhow::Context as _;
-use axum::extract;
 use axum::{
     Extension, Json, Router,
     body::Body,
@@ -25,7 +17,6 @@ use axum::{
     routing::{get, post},
 };
 use axum_extra::response::ErasedJson;
-use chrono::{DateTime, Utc};
 use serde::{Deserialize, Serialize};
 use std::sync::{Arc, OnceLock};
 use tower::ServiceBuilder;
@@ -100,10 +91,7 @@ impl std::fmt::Display for SystemIdHeader {
 
 pub fn routes(rpc_server: Arc<rpc::Server>) -> Router<(), Body> {
     Router::new()
-        .route("/users/look_up", get(look_up_user))
         .route("/users/:id/access_tokens", post(create_access_token))
-        .route("/users/:id/refresh_llm_tokens", post(refresh_llm_tokens))
-        .route("/users/:id/update_plan", post(update_plan))
         .route("/rpc_server_snapshot", get(get_rpc_server_snapshot))
         .merge(contributors::router())
         .layer(
@@ -144,99 +132,6 @@ pub async fn validate_api_token<B>(req: Request<B>, next: Next<B>) -> impl IntoR
     Ok::<_, Error>(next.run(req).await)
 }
 
-#[derive(Debug, Deserialize)]
-struct LookUpUserParams {
-    identifier: String,
-}
-
-#[derive(Debug, Serialize)]
-struct LookUpUserResponse {
-    user: Option<User>,
-}
-
-async fn look_up_user(
-    Query(params): Query<LookUpUserParams>,
-    Extension(app): Extension<Arc<AppState>>,
-) -> Result<Json<LookUpUserResponse>> {
-    let user = resolve_identifier_to_user(&app.db, &params.identifier).await?;
-    let user = if let Some(user) = user {
-        match user {
-            UserOrId::User(user) => Some(user),
-            UserOrId::Id(id) => app.db.get_user_by_id(id).await?,
-        }
-    } else {
-        None
-    };
-
-    Ok(Json(LookUpUserResponse { user }))
-}
-
-enum UserOrId {
-    User(User),
-    Id(UserId),
-}
-
-async fn resolve_identifier_to_user(
-    db: &Arc<Database>,
-    identifier: &str,
-) -> Result<Option<UserOrId>> {
-    if let Some(identifier) = identifier.parse::<i32>().ok() {
-        let user = db.get_user_by_id(UserId(identifier)).await?;
-
-        return Ok(user.map(UserOrId::User));
-    }
-
-    if identifier.starts_with("cus_") {
-        let billing_customer = db
-            .get_billing_customer_by_stripe_customer_id(&identifier)
-            .await?;
-
-        return Ok(billing_customer.map(|billing_customer| UserOrId::Id(billing_customer.user_id)));
-    }
-
-    if identifier.starts_with("sub_") {
-        let billing_subscription = db
-            .get_billing_subscription_by_stripe_subscription_id(&identifier)
-            .await?;
-
-        if let Some(billing_subscription) = billing_subscription {
-            let billing_customer = db
-                .get_billing_customer_by_id(billing_subscription.billing_customer_id)
-                .await?;
-
-            return Ok(
-                billing_customer.map(|billing_customer| UserOrId::Id(billing_customer.user_id))
-            );
-        } else {
-            return Ok(None);
-        }
-    }
-
-    if identifier.contains('@') {
-        let user = db.get_user_by_email(identifier).await?;
-
-        return Ok(user.map(UserOrId::User));
-    }
-
-    if let Some(user) = db.get_user_by_github_login(identifier).await? {
-        return Ok(Some(UserOrId::User(user)));
-    }
-
-    Ok(None)
-}
-
-#[derive(Deserialize, Debug)]
-struct CreateUserParams {
-    github_user_id: i32,
-    github_login: String,
-    email_address: String,
-    email_confirmation_code: Option<String>,
-    #[serde(default)]
-    admin: bool,
-    #[serde(default)]
-    invite_count: i32,
-}
-
 async fn get_rpc_server_snapshot(
     Extension(rpc_server): Extension<Arc<rpc::Server>>,
 ) -> Result<ErasedJson> {
@@ -295,90 +190,3 @@ async fn create_access_token(
         encrypted_access_token,
     }))
 }
-
-#[derive(Serialize)]
-struct RefreshLlmTokensResponse {}
-
-async fn refresh_llm_tokens(
-    Path(user_id): Path<UserId>,
-    Extension(rpc_server): Extension<Arc<rpc::Server>>,
-) -> Result<Json<RefreshLlmTokensResponse>> {
-    rpc_server.refresh_llm_tokens_for_user(user_id).await;
-
-    Ok(Json(RefreshLlmTokensResponse {}))
-}
-
-#[derive(Debug, Serialize, Deserialize)]
-struct UpdatePlanBody {
-    pub plan: cloud_llm_client::Plan,
-    pub subscription_period: SubscriptionPeriod,
-    pub usage: cloud_llm_client::CurrentUsage,
-    pub trial_started_at: Option<DateTime<Utc>>,
-    pub is_usage_based_billing_enabled: bool,
-    pub is_account_too_young: bool,
-    pub has_overdue_invoices: bool,
-}
-
-#[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize)]
-struct SubscriptionPeriod {
-    pub started_at: DateTime<Utc>,
-    pub ended_at: DateTime<Utc>,
-}
-
-#[derive(Serialize)]
-struct UpdatePlanResponse {}
-
-async fn update_plan(
-    Path(user_id): Path<UserId>,
-    Extension(rpc_server): Extension<Arc<rpc::Server>>,
-    extract::Json(body): extract::Json<UpdatePlanBody>,
-) -> Result<Json<UpdatePlanResponse>> {
-    let plan = match body.plan {
-        cloud_llm_client::Plan::ZedFree => proto::Plan::Free,
-        cloud_llm_client::Plan::ZedPro => proto::Plan::ZedPro,
-        cloud_llm_client::Plan::ZedProTrial => proto::Plan::ZedProTrial,
-    };
-
-    let update_user_plan = proto::UpdateUserPlan {
-        plan: plan.into(),
-        trial_started_at: body
-            .trial_started_at
-            .map(|trial_started_at| trial_started_at.timestamp() as u64),
-        is_usage_based_billing_enabled: Some(body.is_usage_based_billing_enabled),
-        usage: Some(proto::SubscriptionUsage {
-            model_requests_usage_amount: body.usage.model_requests.used,
-            model_requests_usage_limit: Some(usage_limit_to_proto(body.usage.model_requests.limit)),
-            edit_predictions_usage_amount: body.usage.edit_predictions.used,
-            edit_predictions_usage_limit: Some(usage_limit_to_proto(
-                body.usage.edit_predictions.limit,
-            )),
-        }),
-        subscription_period: Some(proto::SubscriptionPeriod {
-            started_at: body.subscription_period.started_at.timestamp() as u64,
-            ended_at: body.subscription_period.ended_at.timestamp() as u64,
-        }),
-        account_too_young: Some(body.is_account_too_young),
-        has_overdue_invoices: Some(body.has_overdue_invoices),
-    };
-
-    rpc_server
-        .update_plan_for_user(user_id, update_user_plan)
-        .await?;
-
-    Ok(Json(UpdatePlanResponse {}))
-}
-
-fn usage_limit_to_proto(limit: cloud_llm_client::UsageLimit) -> proto::UsageLimit {
-    proto::UsageLimit {
-        variant: Some(match limit {
-            cloud_llm_client::UsageLimit::Limited(limit) => {
-                proto::usage_limit::Variant::Limited(proto::usage_limit::Limited {
-                    limit: limit as u32,
-                })
-            }
-            cloud_llm_client::UsageLimit::Unlimited => {
-                proto::usage_limit::Variant::Unlimited(proto::usage_limit::Unlimited {})
-            }
-        }),
-    }
-}

crates/collab/src/api/billing.rs 🔗

@@ -1,59 +0,0 @@
-use std::sync::Arc;
-use stripe::SubscriptionStatus;
-
-use crate::AppState;
-use crate::db::billing_subscription::StripeSubscriptionStatus;
-use crate::db::{CreateBillingCustomerParams, billing_customer};
-use crate::stripe_client::{StripeClient, StripeCustomerId};
-
-impl From<SubscriptionStatus> for StripeSubscriptionStatus {
-    fn from(value: SubscriptionStatus) -> Self {
-        match value {
-            SubscriptionStatus::Incomplete => Self::Incomplete,
-            SubscriptionStatus::IncompleteExpired => Self::IncompleteExpired,
-            SubscriptionStatus::Trialing => Self::Trialing,
-            SubscriptionStatus::Active => Self::Active,
-            SubscriptionStatus::PastDue => Self::PastDue,
-            SubscriptionStatus::Canceled => Self::Canceled,
-            SubscriptionStatus::Unpaid => Self::Unpaid,
-            SubscriptionStatus::Paused => Self::Paused,
-        }
-    }
-}
-
-/// Finds or creates a billing customer using the provided customer.
-pub async fn find_or_create_billing_customer(
-    app: &Arc<AppState>,
-    stripe_client: &dyn StripeClient,
-    customer_id: &StripeCustomerId,
-) -> anyhow::Result<Option<billing_customer::Model>> {
-    // If we already have a billing customer record associated with the Stripe customer,
-    // there's nothing more we need to do.
-    if let Some(billing_customer) = app
-        .db
-        .get_billing_customer_by_stripe_customer_id(customer_id.0.as_ref())
-        .await?
-    {
-        return Ok(Some(billing_customer));
-    }
-
-    let customer = stripe_client.get_customer(customer_id).await?;
-
-    let Some(email) = customer.email else {
-        return Ok(None);
-    };
-
-    let Some(user) = app.db.get_user_by_email(&email).await? else {
-        return Ok(None);
-    };
-
-    let billing_customer = app
-        .db
-        .create_billing_customer(&CreateBillingCustomerParams {
-            user_id: user.id,
-            stripe_customer_id: customer.id.to_string(),
-        })
-        .await?;
-
-    Ok(Some(billing_customer))
-}

crates/collab/src/api/events.rs 🔗

@@ -149,35 +149,35 @@ pub async fn post_crash(
         "crash report"
     );
 
-    if let Some(kinesis_client) = app.kinesis_client.clone() {
-        if let Some(stream) = app.config.kinesis_stream.clone() {
-            let properties = json!({
-                "app_version": report.header.app_version,
-                "os_version": report.header.os_version,
-                "os_name": "macOS",
-                "bundle_id": report.header.bundle_id,
-                "incident_id": report.header.incident_id,
-                "installation_id": installation_id,
-                "description": description,
-                "backtrace": summary,
-            });
-            let row = SnowflakeRow::new(
-                "Crash Reported",
-                None,
-                false,
-                Some(installation_id),
-                properties,
-            );
-            let data = serde_json::to_vec(&row)?;
-            kinesis_client
-                .put_record()
-                .stream_name(stream)
-                .partition_key(row.insert_id.unwrap_or_default())
-                .data(data.into())
-                .send()
-                .await
-                .log_err();
-        }
+    if let Some(kinesis_client) = app.kinesis_client.clone()
+        && let Some(stream) = app.config.kinesis_stream.clone()
+    {
+        let properties = json!({
+            "app_version": report.header.app_version,
+            "os_version": report.header.os_version,
+            "os_name": "macOS",
+            "bundle_id": report.header.bundle_id,
+            "incident_id": report.header.incident_id,
+            "installation_id": installation_id,
+            "description": description,
+            "backtrace": summary,
+        });
+        let row = SnowflakeRow::new(
+            "Crash Reported",
+            None,
+            false,
+            Some(installation_id),
+            properties,
+        );
+        let data = serde_json::to_vec(&row)?;
+        kinesis_client
+            .put_record()
+            .stream_name(stream)
+            .partition_key(row.insert_id.unwrap_or_default())
+            .data(data.into())
+            .send()
+            .await
+            .log_err();
     }
 
     if let Some(slack_panics_webhook) = app.config.slack_panics_webhook.clone() {
@@ -280,7 +280,7 @@ pub async fn post_hang(
         service = "client",
         version = %report.app_version.unwrap_or_default().to_string(),
         os_name = %report.os_name,
-        os_version = report.os_version.unwrap_or_default().to_string(),
+        os_version = report.os_version.unwrap_or_default(),
         incident_id = %incident_id,
         installation_id = %report.installation_id.unwrap_or_default(),
         backtrace = %backtrace,
@@ -359,34 +359,34 @@ pub async fn post_panic(
         "panic report"
     );
 
-    if let Some(kinesis_client) = app.kinesis_client.clone() {
-        if let Some(stream) = app.config.kinesis_stream.clone() {
-            let properties = json!({
-                "app_version": panic.app_version,
-                "os_name": panic.os_name,
-                "os_version": panic.os_version,
-                "incident_id": incident_id,
-                "installation_id": panic.installation_id,
-                "description": panic.payload,
-                "backtrace": backtrace,
-            });
-            let row = SnowflakeRow::new(
-                "Panic Reported",
-                None,
-                false,
-                panic.installation_id.clone(),
-                properties,
-            );
-            let data = serde_json::to_vec(&row)?;
-            kinesis_client
-                .put_record()
-                .stream_name(stream)
-                .partition_key(row.insert_id.unwrap_or_default())
-                .data(data.into())
-                .send()
-                .await
-                .log_err();
-        }
+    if let Some(kinesis_client) = app.kinesis_client.clone()
+        && let Some(stream) = app.config.kinesis_stream.clone()
+    {
+        let properties = json!({
+            "app_version": panic.app_version,
+            "os_name": panic.os_name,
+            "os_version": panic.os_version,
+            "incident_id": incident_id,
+            "installation_id": panic.installation_id,
+            "description": panic.payload,
+            "backtrace": backtrace,
+        });
+        let row = SnowflakeRow::new(
+            "Panic Reported",
+            None,
+            false,
+            panic.installation_id.clone(),
+            properties,
+        );
+        let data = serde_json::to_vec(&row)?;
+        kinesis_client
+            .put_record()
+            .stream_name(stream)
+            .partition_key(row.insert_id.unwrap_or_default())
+            .data(data.into())
+            .send()
+            .await
+            .log_err();
     }
 
     if !report_to_slack(&panic) {
@@ -518,31 +518,31 @@ pub async fn post_events(
     let first_event_at = chrono::Utc::now()
         - chrono::Duration::milliseconds(last_event.milliseconds_since_first_event);
 
-    if let Some(kinesis_client) = app.kinesis_client.clone() {
-        if let Some(stream) = app.config.kinesis_stream.clone() {
-            let mut request = kinesis_client.put_records().stream_name(stream);
-            let mut has_records = false;
-            for row in for_snowflake(
-                request_body.clone(),
-                first_event_at,
-                country_code.clone(),
-                checksum_matched,
-            ) {
-                if let Some(data) = serde_json::to_vec(&row).log_err() {
-                    request = request.records(
-                        aws_sdk_kinesis::types::PutRecordsRequestEntry::builder()
-                            .partition_key(request_body.system_id.clone().unwrap_or_default())
-                            .data(data.into())
-                            .build()
-                            .unwrap(),
-                    );
-                    has_records = true;
-                }
-            }
-            if has_records {
-                request.send().await.log_err();
+    if let Some(kinesis_client) = app.kinesis_client.clone()
+        && let Some(stream) = app.config.kinesis_stream.clone()
+    {
+        let mut request = kinesis_client.put_records().stream_name(stream);
+        let mut has_records = false;
+        for row in for_snowflake(
+            request_body.clone(),
+            first_event_at,
+            country_code.clone(),
+            checksum_matched,
+        ) {
+            if let Some(data) = serde_json::to_vec(&row).log_err() {
+                request = request.records(
+                    aws_sdk_kinesis::types::PutRecordsRequestEntry::builder()
+                        .partition_key(request_body.system_id.clone().unwrap_or_default())
+                        .data(data.into())
+                        .build()
+                        .unwrap(),
+                );
+                has_records = true;
             }
         }
+        if has_records {
+            request.send().await.log_err();
+        }
     };
 
     Ok(())
@@ -564,170 +564,10 @@ fn for_snowflake(
     country_code: Option<String>,
     checksum_matched: bool,
 ) -> impl Iterator<Item = SnowflakeRow> {
-    body.events.into_iter().filter_map(move |event| {
+    body.events.into_iter().map(move |event| {
         let timestamp =
             first_event_at + Duration::milliseconds(event.milliseconds_since_first_event);
-        // We will need to double check, but I believe all of the events that
-        // are being transformed here are now migrated over to use the
-        // telemetry::event! macro, as of this commit so this code can go away
-        // when we feel enough users have upgraded past this point.
         let (event_type, mut event_properties) = match &event.event {
-            Event::Editor(e) => (
-                match e.operation.as_str() {
-                    "open" => "Editor Opened".to_string(),
-                    "save" => "Editor Saved".to_string(),
-                    _ => format!("Unknown Editor Event: {}", e.operation),
-                },
-                serde_json::to_value(e).unwrap(),
-            ),
-            Event::EditPrediction(e) => (
-                format!(
-                    "Edit Prediction {}",
-                    if e.suggestion_accepted {
-                        "Accepted"
-                    } else {
-                        "Discarded"
-                    }
-                ),
-                serde_json::to_value(e).unwrap(),
-            ),
-            Event::EditPredictionRating(e) => (
-                "Edit Prediction Rated".to_string(),
-                serde_json::to_value(e).unwrap(),
-            ),
-            Event::Call(e) => {
-                let event_type = match e.operation.trim() {
-                    "unshare project" => "Project Unshared".to_string(),
-                    "open channel notes" => "Channel Notes Opened".to_string(),
-                    "share project" => "Project Shared".to_string(),
-                    "join channel" => "Channel Joined".to_string(),
-                    "hang up" => "Call Ended".to_string(),
-                    "accept incoming" => "Incoming Call Accepted".to_string(),
-                    "invite" => "Participant Invited".to_string(),
-                    "disable microphone" => "Microphone Disabled".to_string(),
-                    "enable microphone" => "Microphone Enabled".to_string(),
-                    "enable screen share" => "Screen Share Enabled".to_string(),
-                    "disable screen share" => "Screen Share Disabled".to_string(),
-                    "decline incoming" => "Incoming Call Declined".to_string(),
-                    _ => format!("Unknown Call Event: {}", e.operation),
-                };
-
-                (event_type, serde_json::to_value(e).unwrap())
-            }
-            Event::Assistant(e) => (
-                match e.phase {
-                    telemetry_events::AssistantPhase::Response => "Assistant Responded".to_string(),
-                    telemetry_events::AssistantPhase::Invoked => "Assistant Invoked".to_string(),
-                    telemetry_events::AssistantPhase::Accepted => {
-                        "Assistant Response Accepted".to_string()
-                    }
-                    telemetry_events::AssistantPhase::Rejected => {
-                        "Assistant Response Rejected".to_string()
-                    }
-                },
-                serde_json::to_value(e).unwrap(),
-            ),
-            Event::Cpu(_) | Event::Memory(_) => return None,
-            Event::App(e) => {
-                let mut properties = json!({});
-                let event_type = match e.operation.trim() {
-                    // App
-                    "open" => "App Opened".to_string(),
-                    "first open" => "App First Opened".to_string(),
-                    "first open for release channel" => {
-                        "App First Opened For Release Channel".to_string()
-                    }
-                    "close" => "App Closed".to_string(),
-
-                    // Project
-                    "open project" => "Project Opened".to_string(),
-                    "open node project" => {
-                        properties["project_type"] = json!("node");
-                        "Project Opened".to_string()
-                    }
-                    "open pnpm project" => {
-                        properties["project_type"] = json!("pnpm");
-                        "Project Opened".to_string()
-                    }
-                    "open yarn project" => {
-                        properties["project_type"] = json!("yarn");
-                        "Project Opened".to_string()
-                    }
-
-                    // SSH
-                    "create ssh server" => "SSH Server Created".to_string(),
-                    "create ssh project" => "SSH Project Created".to_string(),
-                    "open ssh project" => "SSH Project Opened".to_string(),
-
-                    // Welcome Page
-                    "welcome page: change keymap" => "Welcome Keymap Changed".to_string(),
-                    "welcome page: change theme" => "Welcome Theme Changed".to_string(),
-                    "welcome page: close" => "Welcome Page Closed".to_string(),
-                    "welcome page: edit settings" => "Welcome Settings Edited".to_string(),
-                    "welcome page: install cli" => "Welcome CLI Installed".to_string(),
-                    "welcome page: open" => "Welcome Page Opened".to_string(),
-                    "welcome page: open extensions" => "Welcome Extensions Page Opened".to_string(),
-                    "welcome page: sign in to copilot" => "Welcome Copilot Signed In".to_string(),
-                    "welcome page: toggle diagnostic telemetry" => {
-                        "Welcome Diagnostic Telemetry Toggled".to_string()
-                    }
-                    "welcome page: toggle metric telemetry" => {
-                        "Welcome Metric Telemetry Toggled".to_string()
-                    }
-                    "welcome page: toggle vim" => "Welcome Vim Mode Toggled".to_string(),
-                    "welcome page: view docs" => "Welcome Documentation Viewed".to_string(),
-
-                    // Extensions
-                    "extensions page: open" => "Extensions Page Opened".to_string(),
-                    "extensions: install extension" => "Extension Installed".to_string(),
-                    "extensions: uninstall extension" => "Extension Uninstalled".to_string(),
-
-                    // Misc
-                    "markdown preview: open" => "Markdown Preview Opened".to_string(),
-                    "project diagnostics: open" => "Project Diagnostics Opened".to_string(),
-                    "project search: open" => "Project Search Opened".to_string(),
-                    "repl sessions: open" => "REPL Session Started".to_string(),
-
-                    // Feature Upsell
-                    "feature upsell: toggle vim" => {
-                        properties["source"] = json!("Feature Upsell");
-                        "Vim Mode Toggled".to_string()
-                    }
-                    _ => e
-                        .operation
-                        .strip_prefix("feature upsell: viewed docs (")
-                        .and_then(|s| s.strip_suffix(')'))
-                        .map_or_else(
-                            || format!("Unknown App Event: {}", e.operation),
-                            |docs_url| {
-                                properties["url"] = json!(docs_url);
-                                properties["source"] = json!("Feature Upsell");
-                                "Documentation Viewed".to_string()
-                            },
-                        ),
-                };
-                (event_type, properties)
-            }
-            Event::Setting(e) => (
-                "Settings Changed".to_string(),
-                serde_json::to_value(e).unwrap(),
-            ),
-            Event::Extension(e) => (
-                "Extension Loaded".to_string(),
-                serde_json::to_value(e).unwrap(),
-            ),
-            Event::Edit(e) => (
-                "Editor Edited".to_string(),
-                serde_json::to_value(e).unwrap(),
-            ),
-            Event::Action(e) => (
-                "Action Invoked".to_string(),
-                serde_json::to_value(e).unwrap(),
-            ),
-            Event::Repl(e) => (
-                "Kernel Status Changed".to_string(),
-                serde_json::to_value(e).unwrap(),
-            ),
             Event::Flexible(e) => (
                 e.event_type.clone(),
                 serde_json::to_value(&e.event_properties).unwrap(),
@@ -759,7 +599,7 @@ fn for_snowflake(
             })
         });
 
-        Some(SnowflakeRow {
+        SnowflakeRow {
             time: timestamp,
             user_id: body.metrics_id.clone(),
             device_id: body.system_id.clone(),
@@ -767,7 +607,7 @@ fn for_snowflake(
             event_properties,
             user_properties,
             insert_id: Some(Uuid::new_v4().to_string()),
-        })
+        }
     })
 }
 

crates/collab/src/api/extensions.rs 🔗

@@ -337,8 +337,7 @@ async fn fetch_extensions_from_blob_store(
             if known_versions
                 .binary_search_by_key(&published_version, |known_version| known_version)
                 .is_err()
-            {
-                if let Some(extension) = fetch_extension_manifest(
+                && let Some(extension) = fetch_extension_manifest(
                     blob_store_client,
                     blob_store_bucket,
                     extension_id,
@@ -346,12 +345,11 @@ async fn fetch_extensions_from_blob_store(
                 )
                 .await
                 .log_err()
-                {
-                    new_versions
-                        .entry(extension_id)
-                        .or_default()
-                        .push(extension);
-                }
+            {
+                new_versions
+                    .entry(extension_id)
+                    .or_default()
+                    .push(extension);
             }
         }
     }

crates/collab/src/auth.rs 🔗

@@ -79,27 +79,27 @@ pub async fn validate_header<B>(mut req: Request<B>, next: Next<B>) -> impl Into
         verify_access_token(access_token, user_id, &state.db).await
     };
 
-    if let Ok(validate_result) = validate_result {
-        if validate_result.is_valid {
-            let user = state
+    if let Ok(validate_result) = validate_result
+        && validate_result.is_valid
+    {
+        let user = state
+            .db
+            .get_user_by_id(user_id)
+            .await?
+            .with_context(|| format!("user {user_id} not found"))?;
+
+        if let Some(impersonator_id) = validate_result.impersonator_id {
+            let admin = state
                 .db
-                .get_user_by_id(user_id)
+                .get_user_by_id(impersonator_id)
                 .await?
-                .with_context(|| format!("user {user_id} not found"))?;
-
-            if let Some(impersonator_id) = validate_result.impersonator_id {
-                let admin = state
-                    .db
-                    .get_user_by_id(impersonator_id)
-                    .await?
-                    .with_context(|| format!("user {impersonator_id} not found"))?;
-                req.extensions_mut()
-                    .insert(Principal::Impersonated { user, admin });
-            } else {
-                req.extensions_mut().insert(Principal::User(user));
-            };
-            return Ok::<_, Error>(next.run(req).await);
-        }
+                .with_context(|| format!("user {impersonator_id} not found"))?;
+            req.extensions_mut()
+                .insert(Principal::Impersonated { user, admin });
+        } else {
+            req.extensions_mut().insert(Principal::User(user));
+        };
+        return Ok::<_, Error>(next.run(req).await);
     }
 
     Err(Error::http(
@@ -236,7 +236,7 @@ mod test {
 
     #[gpui::test]
     async fn test_verify_access_token(cx: &mut gpui::TestAppContext) {
-        let test_db = crate::db::TestDb::sqlite(cx.executor().clone());
+        let test_db = crate::db::TestDb::sqlite(cx.executor());
         let db = test_db.db();
 
         let user = db

crates/collab/src/db.rs 🔗

@@ -41,12 +41,7 @@ use worktree_settings_file::LocalSettingsKind;
 pub use tests::TestDb;
 
 pub use ids::*;
-pub use queries::billing_customers::{CreateBillingCustomerParams, UpdateBillingCustomerParams};
-pub use queries::billing_subscriptions::{
-    CreateBillingSubscriptionParams, UpdateBillingSubscriptionParams,
-};
 pub use queries::contributors::ContributorSelector;
-pub use queries::processed_stripe_events::CreateProcessedStripeEventParams;
 pub use sea_orm::ConnectOptions;
 pub use tables::user::Model as User;
 pub use tables::*;
@@ -690,7 +685,7 @@ impl LocalSettingsKind {
         }
     }
 
-    pub fn to_proto(&self) -> proto::LocalSettingsKind {
+    pub fn to_proto(self) -> proto::LocalSettingsKind {
         match self {
             Self::Settings => proto::LocalSettingsKind::Settings,
             Self::Tasks => proto::LocalSettingsKind::Tasks,

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

@@ -70,9 +70,6 @@ macro_rules! id_type {
 }
 
 id_type!(AccessTokenId);
-id_type!(BillingCustomerId);
-id_type!(BillingSubscriptionId);
-id_type!(BillingPreferencesId);
 id_type!(BufferId);
 id_type!(ChannelBufferCollaboratorId);
 id_type!(ChannelChatParticipantId);

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

@@ -1,9 +1,6 @@
 use super::*;
 
 pub mod access_tokens;
-pub mod billing_customers;
-pub mod billing_preferences;
-pub mod billing_subscriptions;
 pub mod buffers;
 pub mod channels;
 pub mod contacts;
@@ -12,7 +9,6 @@ pub mod embeddings;
 pub mod extensions;
 pub mod messages;
 pub mod notifications;
-pub mod processed_stripe_events;
 pub mod projects;
 pub mod rooms;
 pub mod servers;

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

@@ -1,100 +0,0 @@
-use super::*;
-
-#[derive(Debug)]
-pub struct CreateBillingCustomerParams {
-    pub user_id: UserId,
-    pub stripe_customer_id: String,
-}
-
-#[derive(Debug, Default)]
-pub struct UpdateBillingCustomerParams {
-    pub user_id: ActiveValue<UserId>,
-    pub stripe_customer_id: ActiveValue<String>,
-    pub has_overdue_invoices: ActiveValue<bool>,
-    pub trial_started_at: ActiveValue<Option<DateTime>>,
-}
-
-impl Database {
-    /// Creates a new billing customer.
-    pub async fn create_billing_customer(
-        &self,
-        params: &CreateBillingCustomerParams,
-    ) -> Result<billing_customer::Model> {
-        self.transaction(|tx| async move {
-            let customer = billing_customer::Entity::insert(billing_customer::ActiveModel {
-                user_id: ActiveValue::set(params.user_id),
-                stripe_customer_id: ActiveValue::set(params.stripe_customer_id.clone()),
-                ..Default::default()
-            })
-            .exec_with_returning(&*tx)
-            .await?;
-
-            Ok(customer)
-        })
-        .await
-    }
-
-    /// Updates the specified billing customer.
-    pub async fn update_billing_customer(
-        &self,
-        id: BillingCustomerId,
-        params: &UpdateBillingCustomerParams,
-    ) -> Result<()> {
-        self.transaction(|tx| async move {
-            billing_customer::Entity::update(billing_customer::ActiveModel {
-                id: ActiveValue::set(id),
-                user_id: params.user_id.clone(),
-                stripe_customer_id: params.stripe_customer_id.clone(),
-                has_overdue_invoices: params.has_overdue_invoices.clone(),
-                trial_started_at: params.trial_started_at.clone(),
-                created_at: ActiveValue::not_set(),
-            })
-            .exec(&*tx)
-            .await?;
-
-            Ok(())
-        })
-        .await
-    }
-
-    pub async fn get_billing_customer_by_id(
-        &self,
-        id: BillingCustomerId,
-    ) -> Result<Option<billing_customer::Model>> {
-        self.transaction(|tx| async move {
-            Ok(billing_customer::Entity::find()
-                .filter(billing_customer::Column::Id.eq(id))
-                .one(&*tx)
-                .await?)
-        })
-        .await
-    }
-
-    /// Returns the billing customer for the user with the specified ID.
-    pub async fn get_billing_customer_by_user_id(
-        &self,
-        user_id: UserId,
-    ) -> Result<Option<billing_customer::Model>> {
-        self.transaction(|tx| async move {
-            Ok(billing_customer::Entity::find()
-                .filter(billing_customer::Column::UserId.eq(user_id))
-                .one(&*tx)
-                .await?)
-        })
-        .await
-    }
-
-    /// Returns the billing customer for the user with the specified Stripe customer ID.
-    pub async fn get_billing_customer_by_stripe_customer_id(
-        &self,
-        stripe_customer_id: &str,
-    ) -> Result<Option<billing_customer::Model>> {
-        self.transaction(|tx| async move {
-            Ok(billing_customer::Entity::find()
-                .filter(billing_customer::Column::StripeCustomerId.eq(stripe_customer_id))
-                .one(&*tx)
-                .await?)
-        })
-        .await
-    }
-}

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

@@ -1,17 +0,0 @@
-use super::*;
-
-impl Database {
-    /// Returns the billing preferences for the given user, if they exist.
-    pub async fn get_billing_preferences(
-        &self,
-        user_id: UserId,
-    ) -> Result<Option<billing_preference::Model>> {
-        self.transaction(|tx| async move {
-            Ok(billing_preference::Entity::find()
-                .filter(billing_preference::Column::UserId.eq(user_id))
-                .one(&*tx)
-                .await?)
-        })
-        .await
-    }
-}

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

@@ -1,158 +0,0 @@
-use anyhow::Context as _;
-
-use crate::db::billing_subscription::{
-    StripeCancellationReason, StripeSubscriptionStatus, SubscriptionKind,
-};
-
-use super::*;
-
-#[derive(Debug)]
-pub struct CreateBillingSubscriptionParams {
-    pub billing_customer_id: BillingCustomerId,
-    pub kind: Option<SubscriptionKind>,
-    pub stripe_subscription_id: String,
-    pub stripe_subscription_status: StripeSubscriptionStatus,
-    pub stripe_cancellation_reason: Option<StripeCancellationReason>,
-    pub stripe_current_period_start: Option<i64>,
-    pub stripe_current_period_end: Option<i64>,
-}
-
-#[derive(Debug, Default)]
-pub struct UpdateBillingSubscriptionParams {
-    pub billing_customer_id: ActiveValue<BillingCustomerId>,
-    pub kind: ActiveValue<Option<SubscriptionKind>>,
-    pub stripe_subscription_id: ActiveValue<String>,
-    pub stripe_subscription_status: ActiveValue<StripeSubscriptionStatus>,
-    pub stripe_cancel_at: ActiveValue<Option<DateTime>>,
-    pub stripe_cancellation_reason: ActiveValue<Option<StripeCancellationReason>>,
-    pub stripe_current_period_start: ActiveValue<Option<i64>>,
-    pub stripe_current_period_end: ActiveValue<Option<i64>>,
-}
-
-impl Database {
-    /// Creates a new billing subscription.
-    pub async fn create_billing_subscription(
-        &self,
-        params: &CreateBillingSubscriptionParams,
-    ) -> Result<billing_subscription::Model> {
-        self.transaction(|tx| async move {
-            let id = billing_subscription::Entity::insert(billing_subscription::ActiveModel {
-                billing_customer_id: ActiveValue::set(params.billing_customer_id),
-                kind: ActiveValue::set(params.kind),
-                stripe_subscription_id: ActiveValue::set(params.stripe_subscription_id.clone()),
-                stripe_subscription_status: ActiveValue::set(params.stripe_subscription_status),
-                stripe_cancellation_reason: ActiveValue::set(params.stripe_cancellation_reason),
-                stripe_current_period_start: ActiveValue::set(params.stripe_current_period_start),
-                stripe_current_period_end: ActiveValue::set(params.stripe_current_period_end),
-                ..Default::default()
-            })
-            .exec(&*tx)
-            .await?
-            .last_insert_id;
-
-            Ok(billing_subscription::Entity::find_by_id(id)
-                .one(&*tx)
-                .await?
-                .context("failed to retrieve inserted billing subscription")?)
-        })
-        .await
-    }
-
-    /// Updates the specified billing subscription.
-    pub async fn update_billing_subscription(
-        &self,
-        id: BillingSubscriptionId,
-        params: &UpdateBillingSubscriptionParams,
-    ) -> Result<()> {
-        self.transaction(|tx| async move {
-            billing_subscription::Entity::update(billing_subscription::ActiveModel {
-                id: ActiveValue::set(id),
-                billing_customer_id: params.billing_customer_id.clone(),
-                kind: params.kind.clone(),
-                stripe_subscription_id: params.stripe_subscription_id.clone(),
-                stripe_subscription_status: params.stripe_subscription_status.clone(),
-                stripe_cancel_at: params.stripe_cancel_at.clone(),
-                stripe_cancellation_reason: params.stripe_cancellation_reason.clone(),
-                stripe_current_period_start: params.stripe_current_period_start.clone(),
-                stripe_current_period_end: params.stripe_current_period_end.clone(),
-                created_at: ActiveValue::not_set(),
-            })
-            .exec(&*tx)
-            .await?;
-
-            Ok(())
-        })
-        .await
-    }
-
-    /// Returns the billing subscription with the specified Stripe subscription ID.
-    pub async fn get_billing_subscription_by_stripe_subscription_id(
-        &self,
-        stripe_subscription_id: &str,
-    ) -> Result<Option<billing_subscription::Model>> {
-        self.transaction(|tx| async move {
-            Ok(billing_subscription::Entity::find()
-                .filter(
-                    billing_subscription::Column::StripeSubscriptionId.eq(stripe_subscription_id),
-                )
-                .one(&*tx)
-                .await?)
-        })
-        .await
-    }
-
-    pub async fn get_active_billing_subscription(
-        &self,
-        user_id: UserId,
-    ) -> Result<Option<billing_subscription::Model>> {
-        self.transaction(|tx| async move {
-            Ok(billing_subscription::Entity::find()
-                .inner_join(billing_customer::Entity)
-                .filter(billing_customer::Column::UserId.eq(user_id))
-                .filter(
-                    Condition::all()
-                        .add(
-                            Condition::any()
-                                .add(
-                                    billing_subscription::Column::StripeSubscriptionStatus
-                                        .eq(StripeSubscriptionStatus::Active),
-                                )
-                                .add(
-                                    billing_subscription::Column::StripeSubscriptionStatus
-                                        .eq(StripeSubscriptionStatus::Trialing),
-                                ),
-                        )
-                        .add(billing_subscription::Column::Kind.is_not_null()),
-                )
-                .one(&*tx)
-                .await?)
-        })
-        .await
-    }
-
-    /// Returns whether the user has an active billing subscription.
-    pub async fn has_active_billing_subscription(&self, user_id: UserId) -> Result<bool> {
-        Ok(self.count_active_billing_subscriptions(user_id).await? > 0)
-    }
-
-    /// Returns the count of the active billing subscriptions for the user with the specified ID.
-    pub async fn count_active_billing_subscriptions(&self, user_id: UserId) -> Result<usize> {
-        self.transaction(|tx| async move {
-            let count = billing_subscription::Entity::find()
-                .inner_join(billing_customer::Entity)
-                .filter(
-                    billing_customer::Column::UserId.eq(user_id).and(
-                        billing_subscription::Column::StripeSubscriptionStatus
-                            .eq(StripeSubscriptionStatus::Active)
-                            .or(billing_subscription::Column::StripeSubscriptionStatus
-                                .eq(StripeSubscriptionStatus::Trialing)),
-                    ),
-                )
-                .count(&*tx)
-                .await?;
-
-            Ok(count as usize)
-        })
-        .await
-    }
-}

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

@@ -87,10 +87,10 @@ impl Database {
                 continue;
             };
 
-            if let Some((_, max_extension_version)) = &max_versions.get(&version.extension_id) {
-                if max_extension_version > &extension_version {
-                    continue;
-                }
+            if let Some((_, max_extension_version)) = &max_versions.get(&version.extension_id)
+                && max_extension_version > &extension_version
+            {
+                continue;
             }
 
             if let Some(constraints) = constraints {
@@ -331,10 +331,10 @@ impl Database {
                 .exec_without_returning(&*tx)
                 .await?;
 
-                if let Ok(db_version) = semver::Version::parse(&extension.latest_version) {
-                    if db_version >= latest_version.version {
-                        continue;
-                    }
+                if let Ok(db_version) = semver::Version::parse(&extension.latest_version)
+                    && db_version >= latest_version.version
+                {
+                    continue;
                 }
 
                 let mut extension = extension.into_active_model();

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

@@ -1,69 +0,0 @@
-use super::*;
-
-#[derive(Debug)]
-pub struct CreateProcessedStripeEventParams {
-    pub stripe_event_id: String,
-    pub stripe_event_type: String,
-    pub stripe_event_created_timestamp: i64,
-}
-
-impl Database {
-    /// Creates a new processed Stripe event.
-    pub async fn create_processed_stripe_event(
-        &self,
-        params: &CreateProcessedStripeEventParams,
-    ) -> Result<()> {
-        self.transaction(|tx| async move {
-            processed_stripe_event::Entity::insert(processed_stripe_event::ActiveModel {
-                stripe_event_id: ActiveValue::set(params.stripe_event_id.clone()),
-                stripe_event_type: ActiveValue::set(params.stripe_event_type.clone()),
-                stripe_event_created_timestamp: ActiveValue::set(
-                    params.stripe_event_created_timestamp,
-                ),
-                ..Default::default()
-            })
-            .exec_without_returning(&*tx)
-            .await?;
-
-            Ok(())
-        })
-        .await
-    }
-
-    /// Returns the processed Stripe event with the specified event ID.
-    pub async fn get_processed_stripe_event_by_event_id(
-        &self,
-        event_id: &str,
-    ) -> Result<Option<processed_stripe_event::Model>> {
-        self.transaction(|tx| async move {
-            Ok(processed_stripe_event::Entity::find_by_id(event_id)
-                .one(&*tx)
-                .await?)
-        })
-        .await
-    }
-
-    /// Returns the processed Stripe events with the specified event IDs.
-    pub async fn get_processed_stripe_events_by_event_ids(
-        &self,
-        event_ids: &[&str],
-    ) -> Result<Vec<processed_stripe_event::Model>> {
-        self.transaction(|tx| async move {
-            Ok(processed_stripe_event::Entity::find()
-                .filter(
-                    processed_stripe_event::Column::StripeEventId.is_in(event_ids.iter().copied()),
-                )
-                .all(&*tx)
-                .await?)
-        })
-        .await
-    }
-
-    /// Returns whether the Stripe event with the specified ID has already been processed.
-    pub async fn already_processed_stripe_event(&self, event_id: &str) -> Result<bool> {
-        Ok(self
-            .get_processed_stripe_event_by_event_id(event_id)
-            .await?
-            .is_some())
-    }
-}

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

@@ -349,11 +349,11 @@ impl Database {
                                     serde_json::to_string(&repository.current_merge_conflicts)
                                         .unwrap(),
                                 )),
-
-                                // Old clients do not use abs path, entry ids or head_commit_details.
+                                // Old clients do not use abs path, entry ids, head_commit_details, or merge_message.
                                 abs_path: ActiveValue::set(String::new()),
                                 entry_ids: ActiveValue::set("[]".into()),
                                 head_commit_details: ActiveValue::set(None),
+                                merge_message: ActiveValue::set(None),
                             }
                         }),
                     )
@@ -502,6 +502,7 @@ impl Database {
                 current_merge_conflicts: ActiveValue::Set(Some(
                     serde_json::to_string(&update.current_merge_conflicts).unwrap(),
                 )),
+                merge_message: ActiveValue::set(update.merge_message.clone()),
             })
             .on_conflict(
                 OnConflict::columns([
@@ -515,6 +516,7 @@ impl Database {
                     project_repository::Column::AbsPath,
                     project_repository::Column::CurrentMergeConflicts,
                     project_repository::Column::HeadCommitDetails,
+                    project_repository::Column::MergeMessage,
                 ])
                 .to_owned(),
             )
@@ -943,21 +945,21 @@ impl Database {
                 let current_merge_conflicts = db_repository_entry
                     .current_merge_conflicts
                     .as_ref()
-                    .map(|conflicts| serde_json::from_str(&conflicts))
+                    .map(|conflicts| serde_json::from_str(conflicts))
                     .transpose()?
                     .unwrap_or_default();
 
                 let branch_summary = db_repository_entry
                     .branch_summary
                     .as_ref()
-                    .map(|branch_summary| serde_json::from_str(&branch_summary))
+                    .map(|branch_summary| serde_json::from_str(branch_summary))
                     .transpose()?
                     .unwrap_or_default();
 
                 let head_commit_details = db_repository_entry
                     .head_commit_details
                     .as_ref()
-                    .map(|head_commit_details| serde_json::from_str(&head_commit_details))
+                    .map(|head_commit_details| serde_json::from_str(head_commit_details))
                     .transpose()?
                     .unwrap_or_default();
 
@@ -990,6 +992,7 @@ impl Database {
                         head_commit_details,
                         scan_id: db_repository_entry.scan_id as u64,
                         is_last_update: true,
+                        merge_message: db_repository_entry.merge_message,
                     });
                 }
             }
@@ -1318,10 +1321,10 @@ impl Database {
             .await?;
 
         let mut connection_ids = HashSet::default();
-        if let Some(host_connection) = project.host_connection().log_err() {
-            if !exclude_dev_server {
-                connection_ids.insert(host_connection);
-            }
+        if let Some(host_connection) = project.host_connection().log_err()
+            && !exclude_dev_server
+        {
+            connection_ids.insert(host_connection);
         }
 
         while let Some(collaborator) = collaborators.next().await {

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

@@ -746,21 +746,21 @@ impl Database {
                     let current_merge_conflicts = db_repository
                         .current_merge_conflicts
                         .as_ref()
-                        .map(|conflicts| serde_json::from_str(&conflicts))
+                        .map(|conflicts| serde_json::from_str(conflicts))
                         .transpose()?
                         .unwrap_or_default();
 
                     let branch_summary = db_repository
                         .branch_summary
                         .as_ref()
-                        .map(|branch_summary| serde_json::from_str(&branch_summary))
+                        .map(|branch_summary| serde_json::from_str(branch_summary))
                         .transpose()?
                         .unwrap_or_default();
 
                     let head_commit_details = db_repository
                         .head_commit_details
                         .as_ref()
-                        .map(|head_commit_details| serde_json::from_str(&head_commit_details))
+                        .map(|head_commit_details| serde_json::from_str(head_commit_details))
                         .transpose()?
                         .unwrap_or_default();
 
@@ -793,6 +793,7 @@ impl Database {
                             abs_path: db_repository.abs_path,
                             scan_id: db_repository.scan_id as u64,
                             is_last_update: true,
+                            merge_message: db_repository.merge_message,
                         });
                     }
                 }

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

@@ -1,7 +1,4 @@
 pub mod access_token;
-pub mod billing_customer;
-pub mod billing_preference;
-pub mod billing_subscription;
 pub mod buffer;
 pub mod buffer_operation;
 pub mod buffer_snapshot;
@@ -23,7 +20,6 @@ pub mod notification;
 pub mod notification_kind;
 pub mod observed_buffer_edits;
 pub mod observed_channel_messages;
-pub mod processed_stripe_event;
 pub mod project;
 pub mod project_collaborator;
 pub mod project_repository;

crates/collab/src/db/tables/billing_customer.rs 🔗

@@ -1,41 +0,0 @@
-use crate::db::{BillingCustomerId, UserId};
-use sea_orm::entity::prelude::*;
-
-/// A billing customer.
-#[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel)]
-#[sea_orm(table_name = "billing_customers")]
-pub struct Model {
-    #[sea_orm(primary_key)]
-    pub id: BillingCustomerId,
-    pub user_id: UserId,
-    pub stripe_customer_id: String,
-    pub has_overdue_invoices: bool,
-    pub trial_started_at: Option<DateTime>,
-    pub created_at: DateTime,
-}
-
-#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
-pub enum Relation {
-    #[sea_orm(
-        belongs_to = "super::user::Entity",
-        from = "Column::UserId",
-        to = "super::user::Column::Id"
-    )]
-    User,
-    #[sea_orm(has_many = "super::billing_subscription::Entity")]
-    BillingSubscription,
-}
-
-impl Related<super::user::Entity> for Entity {
-    fn to() -> RelationDef {
-        Relation::User.def()
-    }
-}
-
-impl Related<super::billing_subscription::Entity> for Entity {
-    fn to() -> RelationDef {
-        Relation::BillingSubscription.def()
-    }
-}
-
-impl ActiveModelBehavior for ActiveModel {}

crates/collab/src/db/tables/billing_preference.rs 🔗

@@ -1,32 +0,0 @@
-use crate::db::{BillingPreferencesId, UserId};
-use sea_orm::entity::prelude::*;
-
-#[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel)]
-#[sea_orm(table_name = "billing_preferences")]
-pub struct Model {
-    #[sea_orm(primary_key)]
-    pub id: BillingPreferencesId,
-    pub created_at: DateTime,
-    pub user_id: UserId,
-    pub max_monthly_llm_usage_spending_in_cents: i32,
-    pub model_request_overages_enabled: bool,
-    pub model_request_overages_spend_limit_in_cents: i32,
-}
-
-#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
-pub enum Relation {
-    #[sea_orm(
-        belongs_to = "super::user::Entity",
-        from = "Column::UserId",
-        to = "super::user::Column::Id"
-    )]
-    User,
-}
-
-impl Related<super::user::Entity> for Entity {
-    fn to() -> RelationDef {
-        Relation::User.def()
-    }
-}
-
-impl ActiveModelBehavior for ActiveModel {}

crates/collab/src/db/tables/billing_subscription.rs 🔗

@@ -1,176 +0,0 @@
-use crate::db::{BillingCustomerId, BillingSubscriptionId};
-use crate::stripe_client;
-use chrono::{Datelike as _, NaiveDate, Utc};
-use sea_orm::entity::prelude::*;
-use serde::Serialize;
-
-/// A billing subscription.
-#[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel)]
-#[sea_orm(table_name = "billing_subscriptions")]
-pub struct Model {
-    #[sea_orm(primary_key)]
-    pub id: BillingSubscriptionId,
-    pub billing_customer_id: BillingCustomerId,
-    pub kind: Option<SubscriptionKind>,
-    pub stripe_subscription_id: String,
-    pub stripe_subscription_status: StripeSubscriptionStatus,
-    pub stripe_cancel_at: Option<DateTime>,
-    pub stripe_cancellation_reason: Option<StripeCancellationReason>,
-    pub stripe_current_period_start: Option<i64>,
-    pub stripe_current_period_end: Option<i64>,
-    pub created_at: DateTime,
-}
-
-impl Model {
-    pub fn current_period_start_at(&self) -> Option<DateTimeUtc> {
-        let period_start = self.stripe_current_period_start?;
-        chrono::DateTime::from_timestamp(period_start, 0)
-    }
-
-    pub fn current_period_end_at(&self) -> Option<DateTimeUtc> {
-        let period_end = self.stripe_current_period_end?;
-        chrono::DateTime::from_timestamp(period_end, 0)
-    }
-
-    pub fn current_period(
-        subscription: Option<Self>,
-        is_staff: bool,
-    ) -> Option<(DateTimeUtc, DateTimeUtc)> {
-        if is_staff {
-            let now = Utc::now();
-            let year = now.year();
-            let month = now.month();
-
-            let first_day_of_this_month =
-                NaiveDate::from_ymd_opt(year, month, 1)?.and_hms_opt(0, 0, 0)?;
-
-            let next_month = if month == 12 { 1 } else { month + 1 };
-            let next_month_year = if month == 12 { year + 1 } else { year };
-            let first_day_of_next_month =
-                NaiveDate::from_ymd_opt(next_month_year, next_month, 1)?.and_hms_opt(23, 59, 59)?;
-
-            let last_day_of_this_month = first_day_of_next_month - chrono::Days::new(1);
-
-            Some((
-                first_day_of_this_month.and_utc(),
-                last_day_of_this_month.and_utc(),
-            ))
-        } else {
-            let subscription = subscription?;
-            let period_start_at = subscription.current_period_start_at()?;
-            let period_end_at = subscription.current_period_end_at()?;
-
-            Some((period_start_at, period_end_at))
-        }
-    }
-}
-
-#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
-pub enum Relation {
-    #[sea_orm(
-        belongs_to = "super::billing_customer::Entity",
-        from = "Column::BillingCustomerId",
-        to = "super::billing_customer::Column::Id"
-    )]
-    BillingCustomer,
-}
-
-impl Related<super::billing_customer::Entity> for Entity {
-    fn to() -> RelationDef {
-        Relation::BillingCustomer.def()
-    }
-}
-
-impl ActiveModelBehavior for ActiveModel {}
-
-#[derive(Eq, PartialEq, Copy, Clone, Debug, EnumIter, DeriveActiveEnum, Hash, Serialize)]
-#[sea_orm(rs_type = "String", db_type = "String(StringLen::None)")]
-#[serde(rename_all = "snake_case")]
-pub enum SubscriptionKind {
-    #[sea_orm(string_value = "zed_pro")]
-    ZedPro,
-    #[sea_orm(string_value = "zed_pro_trial")]
-    ZedProTrial,
-    #[sea_orm(string_value = "zed_free")]
-    ZedFree,
-}
-
-impl From<SubscriptionKind> for cloud_llm_client::Plan {
-    fn from(value: SubscriptionKind) -> Self {
-        match value {
-            SubscriptionKind::ZedPro => Self::ZedPro,
-            SubscriptionKind::ZedProTrial => Self::ZedProTrial,
-            SubscriptionKind::ZedFree => Self::ZedFree,
-        }
-    }
-}
-
-/// The status of a Stripe subscription.
-///
-/// [Stripe docs](https://docs.stripe.com/api/subscriptions/object#subscription_object-status)
-#[derive(
-    Eq, PartialEq, Copy, Clone, Debug, EnumIter, DeriveActiveEnum, Default, Hash, Serialize,
-)]
-#[sea_orm(rs_type = "String", db_type = "String(StringLen::None)")]
-#[serde(rename_all = "snake_case")]
-pub enum StripeSubscriptionStatus {
-    #[default]
-    #[sea_orm(string_value = "incomplete")]
-    Incomplete,
-    #[sea_orm(string_value = "incomplete_expired")]
-    IncompleteExpired,
-    #[sea_orm(string_value = "trialing")]
-    Trialing,
-    #[sea_orm(string_value = "active")]
-    Active,
-    #[sea_orm(string_value = "past_due")]
-    PastDue,
-    #[sea_orm(string_value = "canceled")]
-    Canceled,
-    #[sea_orm(string_value = "unpaid")]
-    Unpaid,
-    #[sea_orm(string_value = "paused")]
-    Paused,
-}
-
-impl StripeSubscriptionStatus {
-    pub fn is_cancelable(&self) -> bool {
-        match self {
-            Self::Trialing | Self::Active | Self::PastDue => true,
-            Self::Incomplete
-            | Self::IncompleteExpired
-            | Self::Canceled
-            | Self::Unpaid
-            | Self::Paused => false,
-        }
-    }
-}
-
-/// The cancellation reason for a Stripe subscription.
-///
-/// [Stripe docs](https://docs.stripe.com/api/subscriptions/object#subscription_object-cancellation_details-reason)
-#[derive(Eq, PartialEq, Copy, Clone, Debug, EnumIter, DeriveActiveEnum, Hash, Serialize)]
-#[sea_orm(rs_type = "String", db_type = "String(StringLen::None)")]
-#[serde(rename_all = "snake_case")]
-pub enum StripeCancellationReason {
-    #[sea_orm(string_value = "cancellation_requested")]
-    CancellationRequested,
-    #[sea_orm(string_value = "payment_disputed")]
-    PaymentDisputed,
-    #[sea_orm(string_value = "payment_failed")]
-    PaymentFailed,
-}
-
-impl From<stripe_client::StripeCancellationDetailsReason> for StripeCancellationReason {
-    fn from(value: stripe_client::StripeCancellationDetailsReason) -> Self {
-        match value {
-            stripe_client::StripeCancellationDetailsReason::CancellationRequested => {
-                Self::CancellationRequested
-            }
-            stripe_client::StripeCancellationDetailsReason::PaymentDisputed => {
-                Self::PaymentDisputed
-            }
-            stripe_client::StripeCancellationDetailsReason::PaymentFailed => Self::PaymentFailed,
-        }
-    }
-}

crates/collab/src/db/tables/processed_stripe_event.rs 🔗

@@ -1,16 +0,0 @@
-use sea_orm::entity::prelude::*;
-
-#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
-#[sea_orm(table_name = "processed_stripe_events")]
-pub struct Model {
-    #[sea_orm(primary_key)]
-    pub stripe_event_id: String,
-    pub stripe_event_type: String,
-    pub stripe_event_created_timestamp: i64,
-    pub processed_at: DateTime,
-}
-
-#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
-pub enum Relation {}
-
-impl ActiveModelBehavior for ActiveModel {}

crates/collab/src/db/tables/project_repository.rs 🔗

@@ -16,6 +16,8 @@ pub struct Model {
     pub is_deleted: bool,
     // JSON array typed string
     pub current_merge_conflicts: Option<String>,
+    // The suggested merge commit message
+    pub merge_message: Option<String>,
     // A JSON object representing the current Branch values
     pub branch_summary: Option<String>,
     // A JSON object representing the current Head commit values

crates/collab/src/db/tables/user.rs 🔗

@@ -29,8 +29,6 @@ pub struct Model {
 pub enum Relation {
     #[sea_orm(has_many = "super::access_token::Entity")]
     AccessToken,
-    #[sea_orm(has_one = "super::billing_customer::Entity")]
-    BillingCustomer,
     #[sea_orm(has_one = "super::room_participant::Entity")]
     RoomParticipant,
     #[sea_orm(has_many = "super::project::Entity")]
@@ -68,12 +66,6 @@ impl Related<super::access_token::Entity> for Entity {
     }
 }
 
-impl Related<super::billing_customer::Entity> for Entity {
-    fn to() -> RelationDef {
-        Relation::BillingCustomer.def()
-    }
-}
-
 impl Related<super::room_participant::Entity> for Entity {
     fn to() -> RelationDef {
         Relation::RoomParticipant.def()

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

@@ -8,7 +8,6 @@ mod embedding_tests;
 mod extension_tests;
 mod feature_flag_tests;
 mod message_tests;
-mod processed_stripe_event_tests;
 mod user_tests;
 
 use crate::migrations::run_database_migrations;

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

@@ -8,7 +8,7 @@ use time::{Duration, OffsetDateTime, PrimitiveDateTime};
 // SQLite does not support array arguments, so we only test this against a real postgres instance
 #[gpui::test]
 async fn test_get_embeddings_postgres(cx: &mut gpui::TestAppContext) {
-    let test_db = TestDb::postgres(cx.executor().clone());
+    let test_db = TestDb::postgres(cx.executor());
     let db = test_db.db();
 
     let provider = "test_model";
@@ -38,7 +38,7 @@ async fn test_get_embeddings_postgres(cx: &mut gpui::TestAppContext) {
 
 #[gpui::test]
 async fn test_purge_old_embeddings(cx: &mut gpui::TestAppContext) {
-    let test_db = TestDb::postgres(cx.executor().clone());
+    let test_db = TestDb::postgres(cx.executor());
     let db = test_db.db();
 
     let model = "test_model";

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

@@ -1,38 +0,0 @@
-use std::sync::Arc;
-
-use crate::test_both_dbs;
-
-use super::{CreateProcessedStripeEventParams, Database};
-
-test_both_dbs!(
-    test_already_processed_stripe_event,
-    test_already_processed_stripe_event_postgres,
-    test_already_processed_stripe_event_sqlite
-);
-
-async fn test_already_processed_stripe_event(db: &Arc<Database>) {
-    let unprocessed_event_id = "evt_1PiJOuRxOf7d5PNaw2zzWiyO".to_string();
-    let processed_event_id = "evt_1PiIfMRxOf7d5PNakHrAUe8P".to_string();
-
-    db.create_processed_stripe_event(&CreateProcessedStripeEventParams {
-        stripe_event_id: processed_event_id.clone(),
-        stripe_event_type: "customer.created".into(),
-        stripe_event_created_timestamp: 1722355968,
-    })
-    .await
-    .unwrap();
-
-    assert!(
-        db.already_processed_stripe_event(&processed_event_id)
-            .await
-            .unwrap(),
-        "Expected {processed_event_id} to already be processed"
-    );
-
-    assert!(
-        !db.already_processed_stripe_event(&unprocessed_event_id)
-            .await
-            .unwrap(),
-        "Expected {unprocessed_event_id} to be unprocessed"
-    );
-}

crates/collab/src/lib.rs 🔗

@@ -7,8 +7,6 @@ pub mod llm;
 pub mod migrations;
 pub mod rpc;
 pub mod seed;
-pub mod stripe_billing;
-pub mod stripe_client;
 pub mod user_backfiller;
 
 #[cfg(test)]
@@ -22,21 +20,16 @@ use axum::{
 };
 use db::{ChannelId, Database};
 use executor::Executor;
-use llm::db::LlmDatabase;
 use serde::Deserialize;
 use std::{path::PathBuf, sync::Arc};
 use util::ResultExt;
 
-use crate::stripe_billing::StripeBilling;
-use crate::stripe_client::{RealStripeClient, StripeClient};
-
 pub type Result<T, E = Error> = std::result::Result<T, E>;
 
 pub enum Error {
     Http(StatusCode, String, HeaderMap),
     Database(sea_orm::error::DbErr),
     Internal(anyhow::Error),
-    Stripe(stripe::StripeError),
 }
 
 impl From<anyhow::Error> for Error {
@@ -51,12 +44,6 @@ impl From<sea_orm::error::DbErr> for Error {
     }
 }
 
-impl From<stripe::StripeError> for Error {
-    fn from(error: stripe::StripeError) -> Self {
-        Self::Stripe(error)
-    }
-}
-
 impl From<axum::Error> for Error {
     fn from(error: axum::Error) -> Self {
         Self::Internal(error.into())
@@ -104,14 +91,6 @@ impl IntoResponse for Error {
                 );
                 (StatusCode::INTERNAL_SERVER_ERROR, format!("{}", &error)).into_response()
             }
-            Error::Stripe(error) => {
-                log::error!(
-                    "HTTP error {}: {:?}",
-                    StatusCode::INTERNAL_SERVER_ERROR,
-                    &error
-                );
-                (StatusCode::INTERNAL_SERVER_ERROR, format!("{}", &error)).into_response()
-            }
         }
     }
 }
@@ -122,7 +101,6 @@ impl std::fmt::Debug for Error {
             Error::Http(code, message, _headers) => (code, message).fmt(f),
             Error::Database(error) => error.fmt(f),
             Error::Internal(error) => error.fmt(f),
-            Error::Stripe(error) => error.fmt(f),
         }
     }
 }
@@ -133,7 +111,6 @@ impl std::fmt::Display for Error {
             Error::Http(code, message, _) => write!(f, "{code}: {message}"),
             Error::Database(error) => error.fmt(f),
             Error::Internal(error) => error.fmt(f),
-            Error::Stripe(error) => error.fmt(f),
         }
     }
 }
@@ -179,7 +156,6 @@ pub struct Config {
     pub zed_client_checksum_seed: Option<String>,
     pub slack_panics_webhook: Option<String>,
     pub auto_join_channel_id: Option<ChannelId>,
-    pub stripe_api_key: Option<String>,
     pub supermaven_admin_api_key: Option<Arc<str>>,
     pub user_backfiller_github_access_token: Option<Arc<str>>,
 }
@@ -234,7 +210,6 @@ impl Config {
             auto_join_channel_id: None,
             migrations_path: None,
             seed_path: None,
-            stripe_api_key: None,
             supermaven_admin_api_key: None,
             user_backfiller_github_access_token: None,
             kinesis_region: None,
@@ -266,14 +241,8 @@ impl ServiceMode {
 
 pub struct AppState {
     pub db: Arc<Database>,
-    pub llm_db: Option<Arc<LlmDatabase>>,
     pub livekit_client: Option<Arc<dyn livekit_api::Client>>,
     pub blob_store_client: Option<aws_sdk_s3::Client>,
-    /// This is a real instance of the Stripe client; we're working to replace references to this with the
-    /// [`StripeClient`] trait.
-    pub real_stripe_client: Option<Arc<stripe::Client>>,
-    pub stripe_client: Option<Arc<dyn StripeClient>>,
-    pub stripe_billing: Option<Arc<StripeBilling>>,
     pub executor: Executor,
     pub kinesis_client: Option<::aws_sdk_kinesis::Client>,
     pub config: Config,
@@ -286,20 +255,6 @@ impl AppState {
         let mut db = Database::new(db_options).await?;
         db.initialize_notification_kinds().await?;
 
-        let llm_db = if let Some((llm_database_url, llm_database_max_connections)) = config
-            .llm_database_url
-            .clone()
-            .zip(config.llm_database_max_connections)
-        {
-            let mut llm_db_options = db::ConnectOptions::new(llm_database_url);
-            llm_db_options.max_connections(llm_database_max_connections);
-            let mut llm_db = LlmDatabase::new(llm_db_options, executor.clone()).await?;
-            llm_db.initialize().await?;
-            Some(Arc::new(llm_db))
-        } else {
-            None
-        };
-
         let livekit_client = if let Some(((server, key), secret)) = config
             .livekit_server
             .as_ref()
@@ -316,18 +271,10 @@ impl AppState {
         };
 
         let db = Arc::new(db);
-        let stripe_client = build_stripe_client(&config).map(Arc::new).log_err();
         let this = Self {
             db: db.clone(),
-            llm_db,
             livekit_client,
             blob_store_client: build_blob_store_client(&config).await.log_err(),
-            stripe_billing: stripe_client
-                .clone()
-                .map(|stripe_client| Arc::new(StripeBilling::new(stripe_client))),
-            real_stripe_client: stripe_client.clone(),
-            stripe_client: stripe_client
-                .map(|stripe_client| Arc::new(RealStripeClient::new(stripe_client)) as _),
             executor,
             kinesis_client: if config.kinesis_access_key.is_some() {
                 build_kinesis_client(&config).await.log_err()
@@ -340,14 +287,6 @@ impl AppState {
     }
 }
 
-fn build_stripe_client(config: &Config) -> anyhow::Result<stripe::Client> {
-    let api_key = config
-        .stripe_api_key
-        .as_ref()
-        .context("missing stripe_api_key")?;
-    Ok(stripe::Client::new(api_key))
-}
-
 async fn build_blob_store_client(config: &Config) -> anyhow::Result<aws_sdk_s3::Client> {
     let keys = aws_sdk_s3::config::Credentials::new(
         config

crates/collab/src/llm.rs 🔗

@@ -1,12 +1 @@
 pub mod db;
-mod token;
-
-pub use token::*;
-
-pub const AGENT_EXTENDED_TRIAL_FEATURE_FLAG: &str = "agent-extended-trial";
-
-/// The name of the feature flag that bypasses the account age check.
-pub const BYPASS_ACCOUNT_AGE_CHECK_FEATURE_FLAG: &str = "bypass-account-age-check";
-
-/// The minimum account age an account must have in order to use the LLM service.
-pub const MIN_ACCOUNT_AGE_FOR_LLM_USE: chrono::Duration = chrono::Duration::days(30);

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

@@ -1,30 +1,9 @@
-mod ids;
-mod queries;
-mod seed;
-mod tables;
-
-#[cfg(test)]
-mod tests;
-
-use cloud_llm_client::LanguageModelProvider;
-use collections::HashMap;
-pub use ids::*;
-pub use seed::*;
-pub use tables::*;
-
-#[cfg(test)]
-pub use tests::TestLlmDb;
-use usage_measure::UsageMeasure;
-
 use std::future::Future;
 use std::sync::Arc;
 
 use anyhow::Context;
 pub use sea_orm::ConnectOptions;
-use sea_orm::prelude::*;
-use sea_orm::{
-    ActiveValue, DatabaseConnection, DatabaseTransaction, IsolationLevel, TransactionTrait,
-};
+use sea_orm::{DatabaseConnection, DatabaseTransaction, IsolationLevel, TransactionTrait};
 
 use crate::Result;
 use crate::db::TransactionHandle;
@@ -36,9 +15,6 @@ pub struct LlmDatabase {
     pool: DatabaseConnection,
     #[allow(unused)]
     executor: Executor,
-    provider_ids: HashMap<LanguageModelProvider, ProviderId>,
-    models: HashMap<(LanguageModelProvider, String), model::Model>,
-    usage_measure_ids: HashMap<UsageMeasure, UsageMeasureId>,
     #[cfg(test)]
     runtime: Option<tokio::runtime::Runtime>,
 }
@@ -51,59 +27,11 @@ impl LlmDatabase {
             options: options.clone(),
             pool: sea_orm::Database::connect(options).await?,
             executor,
-            provider_ids: HashMap::default(),
-            models: HashMap::default(),
-            usage_measure_ids: HashMap::default(),
             #[cfg(test)]
             runtime: None,
         })
     }
 
-    pub async fn initialize(&mut self) -> Result<()> {
-        self.initialize_providers().await?;
-        self.initialize_models().await?;
-        self.initialize_usage_measures().await?;
-        Ok(())
-    }
-
-    /// Returns the list of all known models, with their [`LanguageModelProvider`].
-    pub fn all_models(&self) -> Vec<(LanguageModelProvider, model::Model)> {
-        self.models
-            .iter()
-            .map(|((model_provider, _model_name), model)| (*model_provider, model.clone()))
-            .collect::<Vec<_>>()
-    }
-
-    /// Returns the names of the known models for the given [`LanguageModelProvider`].
-    pub fn model_names_for_provider(&self, provider: LanguageModelProvider) -> Vec<String> {
-        self.models
-            .keys()
-            .filter_map(|(model_provider, model_name)| {
-                if model_provider == &provider {
-                    Some(model_name)
-                } else {
-                    None
-                }
-            })
-            .cloned()
-            .collect::<Vec<_>>()
-    }
-
-    pub fn model(&self, provider: LanguageModelProvider, name: &str) -> Result<&model::Model> {
-        Ok(self
-            .models
-            .get(&(provider, name.to_string()))
-            .with_context(|| format!("unknown model {provider:?}:{name}"))?)
-    }
-
-    pub fn model_by_id(&self, id: ModelId) -> Result<&model::Model> {
-        Ok(self
-            .models
-            .values()
-            .find(|model| model.id == id)
-            .with_context(|| format!("no model for ID {id:?}"))?)
-    }
-
     pub fn options(&self) -> &ConnectOptions {
         &self.options
     }

crates/collab/src/llm/db/ids.rs 🔗

@@ -1,11 +0,0 @@
-use sea_orm::{DbErr, entity::prelude::*};
-use serde::{Deserialize, Serialize};
-
-use crate::id_type;
-
-id_type!(BillingEventId);
-id_type!(ModelId);
-id_type!(ProviderId);
-id_type!(RevokedAccessTokenId);
-id_type!(UsageId);
-id_type!(UsageMeasureId);

crates/collab/src/llm/db/queries/providers.rs 🔗

@@ -1,134 +0,0 @@
-use super::*;
-use sea_orm::{QueryOrder, sea_query::OnConflict};
-use std::str::FromStr;
-use strum::IntoEnumIterator as _;
-
-pub struct ModelParams {
-    pub provider: LanguageModelProvider,
-    pub name: String,
-    pub max_requests_per_minute: i64,
-    pub max_tokens_per_minute: i64,
-    pub max_tokens_per_day: i64,
-    pub price_per_million_input_tokens: i32,
-    pub price_per_million_output_tokens: i32,
-}
-
-impl LlmDatabase {
-    pub async fn initialize_providers(&mut self) -> Result<()> {
-        self.provider_ids = self
-            .transaction(|tx| async move {
-                let existing_providers = provider::Entity::find().all(&*tx).await?;
-
-                let mut new_providers = LanguageModelProvider::iter()
-                    .filter(|provider| {
-                        !existing_providers
-                            .iter()
-                            .any(|p| p.name == provider.to_string())
-                    })
-                    .map(|provider| provider::ActiveModel {
-                        name: ActiveValue::set(provider.to_string()),
-                        ..Default::default()
-                    })
-                    .peekable();
-
-                if new_providers.peek().is_some() {
-                    provider::Entity::insert_many(new_providers)
-                        .exec(&*tx)
-                        .await?;
-                }
-
-                let all_providers: HashMap<_, _> = provider::Entity::find()
-                    .all(&*tx)
-                    .await?
-                    .iter()
-                    .filter_map(|provider| {
-                        LanguageModelProvider::from_str(&provider.name)
-                            .ok()
-                            .map(|p| (p, provider.id))
-                    })
-                    .collect();
-
-                Ok(all_providers)
-            })
-            .await?;
-        Ok(())
-    }
-
-    pub async fn initialize_models(&mut self) -> Result<()> {
-        let all_provider_ids = &self.provider_ids;
-        self.models = self
-            .transaction(|tx| async move {
-                let all_models: HashMap<_, _> = model::Entity::find()
-                    .all(&*tx)
-                    .await?
-                    .into_iter()
-                    .filter_map(|model| {
-                        let provider = all_provider_ids.iter().find_map(|(provider, id)| {
-                            if *id == model.provider_id {
-                                Some(provider)
-                            } else {
-                                None
-                            }
-                        })?;
-                        Some(((*provider, model.name.clone()), model))
-                    })
-                    .collect();
-                Ok(all_models)
-            })
-            .await?;
-        Ok(())
-    }
-
-    pub async fn insert_models(&mut self, models: &[ModelParams]) -> Result<()> {
-        let all_provider_ids = &self.provider_ids;
-        self.transaction(|tx| async move {
-            model::Entity::insert_many(models.iter().map(|model_params| {
-                let provider_id = all_provider_ids[&model_params.provider];
-                model::ActiveModel {
-                    provider_id: ActiveValue::set(provider_id),
-                    name: ActiveValue::set(model_params.name.clone()),
-                    max_requests_per_minute: ActiveValue::set(model_params.max_requests_per_minute),
-                    max_tokens_per_minute: ActiveValue::set(model_params.max_tokens_per_minute),
-                    max_tokens_per_day: ActiveValue::set(model_params.max_tokens_per_day),
-                    price_per_million_input_tokens: ActiveValue::set(
-                        model_params.price_per_million_input_tokens,
-                    ),
-                    price_per_million_output_tokens: ActiveValue::set(
-                        model_params.price_per_million_output_tokens,
-                    ),
-                    ..Default::default()
-                }
-            }))
-            .on_conflict(
-                OnConflict::columns([model::Column::ProviderId, model::Column::Name])
-                    .update_columns([
-                        model::Column::MaxRequestsPerMinute,
-                        model::Column::MaxTokensPerMinute,
-                        model::Column::MaxTokensPerDay,
-                        model::Column::PricePerMillionInputTokens,
-                        model::Column::PricePerMillionOutputTokens,
-                    ])
-                    .to_owned(),
-            )
-            .exec_without_returning(&*tx)
-            .await?;
-            Ok(())
-        })
-        .await?;
-        self.initialize_models().await
-    }
-
-    /// Returns the list of LLM providers.
-    pub async fn list_providers(&self) -> Result<Vec<LanguageModelProvider>> {
-        self.transaction(|tx| async move {
-            Ok(provider::Entity::find()
-                .order_by_asc(provider::Column::Name)
-                .all(&*tx)
-                .await?
-                .into_iter()
-                .filter_map(|p| LanguageModelProvider::from_str(&p.name).ok())
-                .collect())
-        })
-        .await
-    }
-}

crates/collab/src/llm/db/queries/subscription_usages.rs 🔗

@@ -1,38 +0,0 @@
-use crate::db::UserId;
-
-use super::*;
-
-impl LlmDatabase {
-    pub async fn get_subscription_usage_for_period(
-        &self,
-        user_id: UserId,
-        period_start_at: DateTimeUtc,
-        period_end_at: DateTimeUtc,
-    ) -> Result<Option<subscription_usage::Model>> {
-        self.transaction(|tx| async move {
-            self.get_subscription_usage_for_period_in_tx(
-                user_id,
-                period_start_at,
-                period_end_at,
-                &tx,
-            )
-            .await
-        })
-        .await
-    }
-
-    async fn get_subscription_usage_for_period_in_tx(
-        &self,
-        user_id: UserId,
-        period_start_at: DateTimeUtc,
-        period_end_at: DateTimeUtc,
-        tx: &DatabaseTransaction,
-    ) -> Result<Option<subscription_usage::Model>> {
-        Ok(subscription_usage::Entity::find()
-            .filter(subscription_usage::Column::UserId.eq(user_id))
-            .filter(subscription_usage::Column::PeriodStartAt.eq(period_start_at))
-            .filter(subscription_usage::Column::PeriodEndAt.eq(period_end_at))
-            .one(tx)
-            .await?)
-    }
-}

crates/collab/src/llm/db/queries/usages.rs 🔗

@@ -1,44 +0,0 @@
-use std::str::FromStr;
-use strum::IntoEnumIterator as _;
-
-use super::*;
-
-impl LlmDatabase {
-    pub async fn initialize_usage_measures(&mut self) -> Result<()> {
-        let all_measures = self
-            .transaction(|tx| async move {
-                let existing_measures = usage_measure::Entity::find().all(&*tx).await?;
-
-                let new_measures = UsageMeasure::iter()
-                    .filter(|measure| {
-                        !existing_measures
-                            .iter()
-                            .any(|m| m.name == measure.to_string())
-                    })
-                    .map(|measure| usage_measure::ActiveModel {
-                        name: ActiveValue::set(measure.to_string()),
-                        ..Default::default()
-                    })
-                    .collect::<Vec<_>>();
-
-                if !new_measures.is_empty() {
-                    usage_measure::Entity::insert_many(new_measures)
-                        .exec(&*tx)
-                        .await?;
-                }
-
-                Ok(usage_measure::Entity::find().all(&*tx).await?)
-            })
-            .await?;
-
-        self.usage_measure_ids = all_measures
-            .into_iter()
-            .filter_map(|measure| {
-                UsageMeasure::from_str(&measure.name)
-                    .ok()
-                    .map(|um| (um, measure.id))
-            })
-            .collect();
-        Ok(())
-    }
-}

crates/collab/src/llm/db/seed.rs 🔗

@@ -1,45 +0,0 @@
-use super::*;
-use crate::{Config, Result};
-use queries::providers::ModelParams;
-
-pub async fn seed_database(_config: &Config, db: &mut LlmDatabase, _force: bool) -> Result<()> {
-    db.insert_models(&[
-        ModelParams {
-            provider: LanguageModelProvider::Anthropic,
-            name: "claude-3-5-sonnet".into(),
-            max_requests_per_minute: 5,
-            max_tokens_per_minute: 20_000,
-            max_tokens_per_day: 300_000,
-            price_per_million_input_tokens: 300,   // $3.00/MTok
-            price_per_million_output_tokens: 1500, // $15.00/MTok
-        },
-        ModelParams {
-            provider: LanguageModelProvider::Anthropic,
-            name: "claude-3-opus".into(),
-            max_requests_per_minute: 5,
-            max_tokens_per_minute: 10_000,
-            max_tokens_per_day: 300_000,
-            price_per_million_input_tokens: 1500,  // $15.00/MTok
-            price_per_million_output_tokens: 7500, // $75.00/MTok
-        },
-        ModelParams {
-            provider: LanguageModelProvider::Anthropic,
-            name: "claude-3-sonnet".into(),
-            max_requests_per_minute: 5,
-            max_tokens_per_minute: 20_000,
-            max_tokens_per_day: 300_000,
-            price_per_million_input_tokens: 1500,  // $15.00/MTok
-            price_per_million_output_tokens: 7500, // $75.00/MTok
-        },
-        ModelParams {
-            provider: LanguageModelProvider::Anthropic,
-            name: "claude-3-haiku".into(),
-            max_requests_per_minute: 5,
-            max_tokens_per_minute: 25_000,
-            max_tokens_per_day: 300_000,
-            price_per_million_input_tokens: 25,   // $0.25/MTok
-            price_per_million_output_tokens: 125, // $1.25/MTok
-        },
-    ])
-    .await
-}

crates/collab/src/llm/db/tables.rs 🔗

@@ -1,6 +0,0 @@
-pub mod model;
-pub mod provider;
-pub mod subscription_usage;
-pub mod subscription_usage_meter;
-pub mod usage;
-pub mod usage_measure;

crates/collab/src/llm/db/tables/model.rs 🔗

@@ -1,48 +0,0 @@
-use sea_orm::entity::prelude::*;
-
-use crate::llm::db::{ModelId, ProviderId};
-
-/// An LLM model.
-#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
-#[sea_orm(table_name = "models")]
-pub struct Model {
-    #[sea_orm(primary_key)]
-    pub id: ModelId,
-    pub provider_id: ProviderId,
-    pub name: String,
-    pub max_requests_per_minute: i64,
-    pub max_tokens_per_minute: i64,
-    pub max_input_tokens_per_minute: i64,
-    pub max_output_tokens_per_minute: i64,
-    pub max_tokens_per_day: i64,
-    pub price_per_million_input_tokens: i32,
-    pub price_per_million_cache_creation_input_tokens: i32,
-    pub price_per_million_cache_read_input_tokens: i32,
-    pub price_per_million_output_tokens: i32,
-}
-
-#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
-pub enum Relation {
-    #[sea_orm(
-        belongs_to = "super::provider::Entity",
-        from = "Column::ProviderId",
-        to = "super::provider::Column::Id"
-    )]
-    Provider,
-    #[sea_orm(has_many = "super::usage::Entity")]
-    Usages,
-}
-
-impl Related<super::provider::Entity> for Entity {
-    fn to() -> RelationDef {
-        Relation::Provider.def()
-    }
-}
-
-impl Related<super::usage::Entity> for Entity {
-    fn to() -> RelationDef {
-        Relation::Usages.def()
-    }
-}
-
-impl ActiveModelBehavior for ActiveModel {}

crates/collab/src/llm/db/tables/provider.rs 🔗

@@ -1,25 +0,0 @@
-use crate::llm::db::ProviderId;
-use sea_orm::entity::prelude::*;
-
-/// An LLM provider.
-#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
-#[sea_orm(table_name = "providers")]
-pub struct Model {
-    #[sea_orm(primary_key)]
-    pub id: ProviderId,
-    pub name: String,
-}
-
-#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
-pub enum Relation {
-    #[sea_orm(has_many = "super::model::Entity")]
-    Models,
-}
-
-impl Related<super::model::Entity> for Entity {
-    fn to() -> RelationDef {
-        Relation::Models.def()
-    }
-}
-
-impl ActiveModelBehavior for ActiveModel {}

crates/collab/src/llm/db/tables/subscription_usage.rs 🔗

@@ -1,22 +0,0 @@
-use crate::db::UserId;
-use crate::db::billing_subscription::SubscriptionKind;
-use sea_orm::entity::prelude::*;
-use time::PrimitiveDateTime;
-
-#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
-#[sea_orm(table_name = "subscription_usages_v2")]
-pub struct Model {
-    #[sea_orm(primary_key)]
-    pub id: Uuid,
-    pub user_id: UserId,
-    pub period_start_at: PrimitiveDateTime,
-    pub period_end_at: PrimitiveDateTime,
-    pub plan: SubscriptionKind,
-    pub model_requests: i32,
-    pub edit_predictions: i32,
-}
-
-#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
-pub enum Relation {}
-
-impl ActiveModelBehavior for ActiveModel {}

crates/collab/src/llm/db/tables/subscription_usage_meter.rs 🔗

@@ -1,55 +0,0 @@
-use sea_orm::entity::prelude::*;
-use serde::Serialize;
-
-use crate::llm::db::ModelId;
-
-#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
-#[sea_orm(table_name = "subscription_usage_meters_v2")]
-pub struct Model {
-    #[sea_orm(primary_key)]
-    pub id: Uuid,
-    pub subscription_usage_id: Uuid,
-    pub model_id: ModelId,
-    pub mode: CompletionMode,
-    pub requests: i32,
-}
-
-#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
-pub enum Relation {
-    #[sea_orm(
-        belongs_to = "super::subscription_usage::Entity",
-        from = "Column::SubscriptionUsageId",
-        to = "super::subscription_usage::Column::Id"
-    )]
-    SubscriptionUsage,
-    #[sea_orm(
-        belongs_to = "super::model::Entity",
-        from = "Column::ModelId",
-        to = "super::model::Column::Id"
-    )]
-    Model,
-}
-
-impl Related<super::subscription_usage::Entity> for Entity {
-    fn to() -> RelationDef {
-        Relation::SubscriptionUsage.def()
-    }
-}
-
-impl Related<super::model::Entity> for Entity {
-    fn to() -> RelationDef {
-        Relation::Model.def()
-    }
-}
-
-impl ActiveModelBehavior for ActiveModel {}
-
-#[derive(Eq, PartialEq, Copy, Clone, Debug, EnumIter, DeriveActiveEnum, Hash, Serialize)]
-#[sea_orm(rs_type = "String", db_type = "String(StringLen::None)")]
-#[serde(rename_all = "snake_case")]
-pub enum CompletionMode {
-    #[sea_orm(string_value = "normal")]
-    Normal,
-    #[sea_orm(string_value = "max")]
-    Max,
-}

crates/collab/src/llm/db/tables/usage.rs 🔗

@@ -1,52 +0,0 @@
-use crate::{
-    db::UserId,
-    llm::db::{ModelId, UsageId, UsageMeasureId},
-};
-use sea_orm::entity::prelude::*;
-
-/// An LLM usage record.
-#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
-#[sea_orm(table_name = "usages")]
-pub struct Model {
-    #[sea_orm(primary_key)]
-    pub id: UsageId,
-    /// The ID of the Zed user.
-    ///
-    /// Corresponds to the `users` table in the primary collab database.
-    pub user_id: UserId,
-    pub model_id: ModelId,
-    pub measure_id: UsageMeasureId,
-    pub timestamp: DateTime,
-    pub buckets: Vec<i64>,
-    pub is_staff: bool,
-}
-
-#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
-pub enum Relation {
-    #[sea_orm(
-        belongs_to = "super::model::Entity",
-        from = "Column::ModelId",
-        to = "super::model::Column::Id"
-    )]
-    Model,
-    #[sea_orm(
-        belongs_to = "super::usage_measure::Entity",
-        from = "Column::MeasureId",
-        to = "super::usage_measure::Column::Id"
-    )]
-    UsageMeasure,
-}
-
-impl Related<super::model::Entity> for Entity {
-    fn to() -> RelationDef {
-        Relation::Model.def()
-    }
-}
-
-impl Related<super::usage_measure::Entity> for Entity {
-    fn to() -> RelationDef {
-        Relation::UsageMeasure.def()
-    }
-}
-
-impl ActiveModelBehavior for ActiveModel {}

crates/collab/src/llm/db/tables/usage_measure.rs 🔗

@@ -1,36 +0,0 @@
-use crate::llm::db::UsageMeasureId;
-use sea_orm::entity::prelude::*;
-
-#[derive(
-    Copy, Clone, Debug, PartialEq, Eq, Hash, strum::EnumString, strum::Display, strum::EnumIter,
-)]
-#[strum(serialize_all = "snake_case")]
-pub enum UsageMeasure {
-    RequestsPerMinute,
-    TokensPerMinute,
-    InputTokensPerMinute,
-    OutputTokensPerMinute,
-    TokensPerDay,
-}
-
-#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
-#[sea_orm(table_name = "usage_measures")]
-pub struct Model {
-    #[sea_orm(primary_key)]
-    pub id: UsageMeasureId,
-    pub name: String,
-}
-
-#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
-pub enum Relation {
-    #[sea_orm(has_many = "super::usage::Entity")]
-    Usages,
-}
-
-impl Related<super::usage::Entity> for Entity {
-    fn to() -> RelationDef {
-        Relation::Usages.def()
-    }
-}
-
-impl ActiveModelBehavior for ActiveModel {}

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

@@ -1,107 +0,0 @@
-mod provider_tests;
-
-use gpui::BackgroundExecutor;
-use parking_lot::Mutex;
-use rand::prelude::*;
-use sea_orm::ConnectionTrait;
-use sqlx::migrate::MigrateDatabase;
-use std::time::Duration;
-
-use crate::migrations::run_database_migrations;
-
-use super::*;
-
-pub struct TestLlmDb {
-    pub db: Option<LlmDatabase>,
-    pub connection: Option<sqlx::AnyConnection>,
-}
-
-impl TestLlmDb {
-    pub fn postgres(background: BackgroundExecutor) -> Self {
-        static LOCK: Mutex<()> = Mutex::new(());
-
-        let _guard = LOCK.lock();
-        let mut rng = StdRng::from_entropy();
-        let url = format!(
-            "postgres://postgres@localhost/zed-llm-test-{}",
-            rng.r#gen::<u128>()
-        );
-        let runtime = tokio::runtime::Builder::new_current_thread()
-            .enable_io()
-            .enable_time()
-            .build()
-            .unwrap();
-
-        let mut db = runtime.block_on(async {
-            sqlx::Postgres::create_database(&url)
-                .await
-                .expect("failed to create test db");
-            let mut options = ConnectOptions::new(url);
-            options
-                .max_connections(5)
-                .idle_timeout(Duration::from_secs(0));
-            let db = LlmDatabase::new(options, Executor::Deterministic(background))
-                .await
-                .unwrap();
-            let migrations_path = concat!(env!("CARGO_MANIFEST_DIR"), "/migrations_llm");
-            run_database_migrations(db.options(), migrations_path)
-                .await
-                .unwrap();
-            db
-        });
-
-        db.runtime = Some(runtime);
-
-        Self {
-            db: Some(db),
-            connection: None,
-        }
-    }
-
-    pub fn db(&mut self) -> &mut LlmDatabase {
-        self.db.as_mut().unwrap()
-    }
-}
-
-#[macro_export]
-macro_rules! test_llm_db {
-    ($test_name:ident, $postgres_test_name:ident) => {
-        #[gpui::test]
-        async fn $postgres_test_name(cx: &mut gpui::TestAppContext) {
-            if !cfg!(target_os = "macos") {
-                return;
-            }
-
-            let mut test_db = $crate::llm::db::TestLlmDb::postgres(cx.executor().clone());
-            $test_name(test_db.db()).await;
-        }
-    };
-}
-
-impl Drop for TestLlmDb {
-    fn drop(&mut self) {
-        let db = self.db.take().unwrap();
-        if let sea_orm::DatabaseBackend::Postgres = db.pool.get_database_backend() {
-            db.runtime.as_ref().unwrap().block_on(async {
-                use util::ResultExt;
-                let query = "
-                        SELECT pg_terminate_backend(pg_stat_activity.pid)
-                        FROM pg_stat_activity
-                        WHERE
-                            pg_stat_activity.datname = current_database() AND
-                            pid <> pg_backend_pid();
-                    ";
-                db.pool
-                    .execute(sea_orm::Statement::from_string(
-                        db.pool.get_database_backend(),
-                        query,
-                    ))
-                    .await
-                    .log_err();
-                sqlx::Postgres::drop_database(db.options.get_url())
-                    .await
-                    .log_err();
-            })
-        }
-    }
-}

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

@@ -1,31 +0,0 @@
-use cloud_llm_client::LanguageModelProvider;
-use pretty_assertions::assert_eq;
-
-use crate::llm::db::LlmDatabase;
-use crate::test_llm_db;
-
-test_llm_db!(
-    test_initialize_providers,
-    test_initialize_providers_postgres
-);
-
-async fn test_initialize_providers(db: &mut LlmDatabase) {
-    let initial_providers = db.list_providers().await.unwrap();
-    assert_eq!(initial_providers, vec![]);
-
-    db.initialize_providers().await.unwrap();
-
-    // Do it twice, to make sure the operation is idempotent.
-    db.initialize_providers().await.unwrap();
-
-    let providers = db.list_providers().await.unwrap();
-
-    assert_eq!(
-        providers,
-        &[
-            LanguageModelProvider::Anthropic,
-            LanguageModelProvider::Google,
-            LanguageModelProvider::OpenAi,
-        ]
-    )
-}

crates/collab/src/llm/token.rs 🔗

@@ -1,146 +0,0 @@
-use crate::db::billing_subscription::SubscriptionKind;
-use crate::db::{billing_customer, billing_subscription, user};
-use crate::llm::{AGENT_EXTENDED_TRIAL_FEATURE_FLAG, BYPASS_ACCOUNT_AGE_CHECK_FEATURE_FLAG};
-use crate::{Config, db::billing_preference};
-use anyhow::{Context as _, Result};
-use chrono::{NaiveDateTime, Utc};
-use cloud_llm_client::Plan;
-use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation};
-use serde::{Deserialize, Serialize};
-use std::time::Duration;
-use thiserror::Error;
-use uuid::Uuid;
-
-#[derive(Clone, Debug, Default, Serialize, Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub struct LlmTokenClaims {
-    pub iat: u64,
-    pub exp: u64,
-    pub jti: String,
-    pub user_id: u64,
-    pub system_id: Option<String>,
-    pub metrics_id: Uuid,
-    pub github_user_login: String,
-    pub account_created_at: NaiveDateTime,
-    pub is_staff: bool,
-    pub has_llm_closed_beta_feature_flag: bool,
-    pub bypass_account_age_check: bool,
-    pub use_llm_request_queue: bool,
-    pub plan: Plan,
-    pub has_extended_trial: bool,
-    pub subscription_period: (NaiveDateTime, NaiveDateTime),
-    pub enable_model_request_overages: bool,
-    pub model_request_overages_spend_limit_in_cents: u32,
-    pub can_use_web_search_tool: bool,
-    #[serde(default)]
-    pub has_overdue_invoices: bool,
-}
-
-const LLM_TOKEN_LIFETIME: Duration = Duration::from_secs(60 * 60);
-
-impl LlmTokenClaims {
-    pub fn create(
-        user: &user::Model,
-        is_staff: bool,
-        billing_customer: billing_customer::Model,
-        billing_preferences: Option<billing_preference::Model>,
-        feature_flags: &Vec<String>,
-        subscription: billing_subscription::Model,
-        system_id: Option<String>,
-        config: &Config,
-    ) -> Result<String> {
-        let secret = config
-            .llm_api_secret
-            .as_ref()
-            .context("no LLM API secret")?;
-
-        let plan = if is_staff {
-            Plan::ZedPro
-        } else {
-            subscription.kind.map_or(Plan::ZedFree, |kind| match kind {
-                SubscriptionKind::ZedFree => Plan::ZedFree,
-                SubscriptionKind::ZedPro => Plan::ZedPro,
-                SubscriptionKind::ZedProTrial => Plan::ZedProTrial,
-            })
-        };
-        let subscription_period =
-            billing_subscription::Model::current_period(Some(subscription), is_staff)
-                .map(|(start, end)| (start.naive_utc(), end.naive_utc()))
-                .context("A plan is required to use Zed's hosted models or edit predictions. Visit https://zed.dev/account to get started.")?;
-
-        let now = Utc::now();
-        let claims = Self {
-            iat: now.timestamp() as u64,
-            exp: (now + LLM_TOKEN_LIFETIME).timestamp() as u64,
-            jti: uuid::Uuid::new_v4().to_string(),
-            user_id: user.id.to_proto(),
-            system_id,
-            metrics_id: user.metrics_id,
-            github_user_login: user.github_login.clone(),
-            account_created_at: user.account_created_at(),
-            is_staff,
-            has_llm_closed_beta_feature_flag: feature_flags
-                .iter()
-                .any(|flag| flag == "llm-closed-beta"),
-            bypass_account_age_check: feature_flags
-                .iter()
-                .any(|flag| flag == BYPASS_ACCOUNT_AGE_CHECK_FEATURE_FLAG),
-            can_use_web_search_tool: true,
-            use_llm_request_queue: feature_flags.iter().any(|flag| flag == "llm-request-queue"),
-            plan,
-            has_extended_trial: feature_flags
-                .iter()
-                .any(|flag| flag == AGENT_EXTENDED_TRIAL_FEATURE_FLAG),
-            subscription_period,
-            enable_model_request_overages: billing_preferences
-                .as_ref()
-                .map_or(false, |preferences| {
-                    preferences.model_request_overages_enabled
-                }),
-            model_request_overages_spend_limit_in_cents: billing_preferences
-                .as_ref()
-                .map_or(0, |preferences| {
-                    preferences.model_request_overages_spend_limit_in_cents as u32
-                }),
-            has_overdue_invoices: billing_customer.has_overdue_invoices,
-        };
-
-        Ok(jsonwebtoken::encode(
-            &Header::default(),
-            &claims,
-            &EncodingKey::from_secret(secret.as_ref()),
-        )?)
-    }
-
-    pub fn validate(token: &str, config: &Config) -> Result<LlmTokenClaims, ValidateLlmTokenError> {
-        let secret = config
-            .llm_api_secret
-            .as_ref()
-            .context("no LLM API secret")?;
-
-        match jsonwebtoken::decode::<Self>(
-            token,
-            &DecodingKey::from_secret(secret.as_ref()),
-            &Validation::default(),
-        ) {
-            Ok(token) => Ok(token.claims),
-            Err(e) => {
-                if e.kind() == &jsonwebtoken::errors::ErrorKind::ExpiredSignature {
-                    Err(ValidateLlmTokenError::Expired)
-                } else {
-                    Err(ValidateLlmTokenError::JwtError(e))
-                }
-            }
-        }
-    }
-}
-
-#[derive(Error, Debug)]
-pub enum ValidateLlmTokenError {
-    #[error("access token is expired")]
-    Expired,
-    #[error("access token validation error: {0}")]
-    JwtError(#[from] jsonwebtoken::errors::Error),
-    #[error("{0}")]
-    Other(#[from] anyhow::Error),
-}

crates/collab/src/main.rs 🔗

@@ -62,13 +62,6 @@ async fn main() -> Result<()> {
             db.initialize_notification_kinds().await?;
 
             collab::seed::seed(&config, &db, false).await?;
-
-            if let Some(llm_database_url) = config.llm_database_url.clone() {
-                let db_options = db::ConnectOptions::new(llm_database_url);
-                let mut db = LlmDatabase::new(db_options.clone(), Executor::Production).await?;
-                db.initialize().await?;
-                collab::llm::db::seed_database(&config, &mut db, true).await?;
-            }
         }
         Some("serve") => {
             let mode = match args.next().as_deref() {
@@ -102,13 +95,6 @@ async fn main() -> Result<()> {
 
                 let state = AppState::new(config, Executor::Production).await?;
 
-                if let Some(stripe_billing) = state.stripe_billing.clone() {
-                    let executor = state.executor.clone();
-                    executor.spawn_detached(async move {
-                        stripe_billing.initialize().await.trace_err();
-                    });
-                }
-
                 if mode.is_collab() {
                     state.db.purge_old_embeddings().await.trace_err();
 
@@ -270,9 +256,6 @@ async fn setup_llm_database(config: &Config) -> Result<()> {
         .llm_database_migrations_path
         .as_deref()
         .unwrap_or_else(|| {
-            #[cfg(feature = "sqlite")]
-            let default_migrations = concat!(env!("CARGO_MANIFEST_DIR"), "/migrations_llm.sqlite");
-            #[cfg(not(feature = "sqlite"))]
             let default_migrations = concat!(env!("CARGO_MANIFEST_DIR"), "/migrations_llm");
 
             Path::new(default_migrations)

crates/collab/src/rpc.rs 🔗

@@ -1,14 +1,6 @@
 mod connection_pool;
 
-use crate::api::billing::find_or_create_billing_customer;
 use crate::api::{CloudflareIpCountryHeader, SystemIdHeader};
-use crate::db::billing_subscription::SubscriptionKind;
-use crate::llm::db::LlmDatabase;
-use crate::llm::{
-    AGENT_EXTENDED_TRIAL_FEATURE_FLAG, BYPASS_ACCOUNT_AGE_CHECK_FEATURE_FLAG, LlmTokenClaims,
-    MIN_ACCOUNT_AGE_FOR_LLM_USE,
-};
-use crate::stripe_client::StripeCustomerId;
 use crate::{
     AppState, Error, Result, auth,
     db::{
@@ -37,7 +29,6 @@ use axum::{
     response::IntoResponse,
     routing::get,
 };
-use chrono::Utc;
 use collections::{HashMap, HashSet};
 pub use connection_pool::{ConnectionPool, ZedVersion};
 use core::fmt::{self, Debug, Formatter};
@@ -148,13 +139,6 @@ pub enum Principal {
 }
 
 impl Principal {
-    fn user(&self) -> &User {
-        match self {
-            Principal::User(user) => user,
-            Principal::Impersonated { user, .. } => user,
-        }
-    }
-
     fn update_span(&self, span: &tracing::Span) {
         match &self {
             Principal::User(user) => {
@@ -218,6 +202,7 @@ struct Session {
     /// The GeoIP country code for the user.
     #[allow(unused)]
     geoip_country_code: Option<String>,
+    #[allow(unused)]
     system_id: Option<String>,
     _executor: Executor,
 }
@@ -325,7 +310,7 @@ impl Server {
         let mut server = Self {
             id: parking_lot::Mutex::new(id),
             peer: Peer::new(id.0 as u32),
-            app_state: app_state.clone(),
+            app_state,
             connection_pool: Default::default(),
             handlers: Default::default(),
             teardown: watch::channel(false).0,
@@ -415,6 +400,8 @@ impl Server {
             .add_request_handler(forward_mutating_project_request::<proto::SaveBuffer>)
             .add_request_handler(forward_mutating_project_request::<proto::BlameBuffer>)
             .add_request_handler(multi_lsp_query)
+            .add_request_handler(lsp_query)
+            .add_message_handler(broadcast_project_message_from_host::<proto::LspQueryResponse>)
             .add_request_handler(forward_mutating_project_request::<proto::RestartLanguageServers>)
             .add_request_handler(forward_mutating_project_request::<proto::StopLanguageServers>)
             .add_request_handler(forward_mutating_project_request::<proto::LinkedEditingRange>)
@@ -463,9 +450,6 @@ impl Server {
             .add_request_handler(follow)
             .add_message_handler(unfollow)
             .add_message_handler(update_followers)
-            .add_request_handler(get_private_user_info)
-            .add_request_handler(get_llm_api_token)
-            .add_request_handler(accept_terms_of_service)
             .add_message_handler(acknowledge_channel_message)
             .add_message_handler(acknowledge_buffer_version)
             .add_request_handler(get_supermaven_api_key)
@@ -634,10 +618,10 @@ impl Server {
                             }
                         }
 
-                        if let Some(live_kit) = livekit_client.as_ref() {
-                            if delete_livekit_room {
-                                live_kit.delete_room(livekit_room).await.trace_err();
-                            }
+                        if let Some(live_kit) = livekit_client.as_ref()
+                            && delete_livekit_room
+                        {
+                            live_kit.delete_room(livekit_room).await.trace_err();
                         }
                     }
                 }
@@ -928,7 +912,10 @@ impl Server {
                                 user_id=field::Empty,
                                 login=field::Empty,
                                 impersonator=field::Empty,
+                                // todo(lsp) remove after Zed Stable hits v0.204.x
                                 multi_lsp_query_request=field::Empty,
+                                lsp_query_request=field::Empty,
+                                release_channel=field::Empty,
                                 { TOTAL_DURATION_MS }=field::Empty,
                                 { PROCESSING_DURATION_MS }=field::Empty,
                                 { QUEUE_DURATION_MS }=field::Empty,
@@ -999,8 +986,6 @@ impl Server {
                         .await?;
                 }
 
-                update_user_plan(session).await?;
-
                 let contacts = self.app_state.db.get_contacts(user.id).await?;
 
                 {
@@ -1034,99 +1019,52 @@ impl Server {
         inviter_id: UserId,
         invitee_id: UserId,
     ) -> Result<()> {
-        if let Some(user) = self.app_state.db.get_user_by_id(inviter_id).await? {
-            if let Some(code) = &user.invite_code {
-                let pool = self.connection_pool.lock();
-                let invitee_contact = contact_for_user(invitee_id, false, &pool);
-                for connection_id in pool.user_connection_ids(inviter_id) {
-                    self.peer.send(
-                        connection_id,
-                        proto::UpdateContacts {
-                            contacts: vec![invitee_contact.clone()],
-                            ..Default::default()
-                        },
-                    )?;
-                    self.peer.send(
-                        connection_id,
-                        proto::UpdateInviteInfo {
-                            url: format!("{}{}", self.app_state.config.invite_link_prefix, &code),
-                            count: user.invite_count as u32,
-                        },
-                    )?;
-                }
+        if let Some(user) = self.app_state.db.get_user_by_id(inviter_id).await?
+            && let Some(code) = &user.invite_code
+        {
+            let pool = self.connection_pool.lock();
+            let invitee_contact = contact_for_user(invitee_id, false, &pool);
+            for connection_id in pool.user_connection_ids(inviter_id) {
+                self.peer.send(
+                    connection_id,
+                    proto::UpdateContacts {
+                        contacts: vec![invitee_contact.clone()],
+                        ..Default::default()
+                    },
+                )?;
+                self.peer.send(
+                    connection_id,
+                    proto::UpdateInviteInfo {
+                        url: format!("{}{}", self.app_state.config.invite_link_prefix, &code),
+                        count: user.invite_count as u32,
+                    },
+                )?;
             }
         }
         Ok(())
     }
 
     pub async fn invite_count_updated(self: &Arc<Self>, user_id: UserId) -> Result<()> {
-        if let Some(user) = self.app_state.db.get_user_by_id(user_id).await? {
-            if let Some(invite_code) = &user.invite_code {
-                let pool = self.connection_pool.lock();
-                for connection_id in pool.user_connection_ids(user_id) {
-                    self.peer.send(
-                        connection_id,
-                        proto::UpdateInviteInfo {
-                            url: format!(
-                                "{}{}",
-                                self.app_state.config.invite_link_prefix, invite_code
-                            ),
-                            count: user.invite_count as u32,
-                        },
-                    )?;
-                }
+        if let Some(user) = self.app_state.db.get_user_by_id(user_id).await?
+            && let Some(invite_code) = &user.invite_code
+        {
+            let pool = self.connection_pool.lock();
+            for connection_id in pool.user_connection_ids(user_id) {
+                self.peer.send(
+                    connection_id,
+                    proto::UpdateInviteInfo {
+                        url: format!(
+                            "{}{}",
+                            self.app_state.config.invite_link_prefix, invite_code
+                        ),
+                        count: user.invite_count as u32,
+                    },
+                )?;
             }
         }
         Ok(())
     }
 
-    pub async fn update_plan_for_user(
-        self: &Arc<Self>,
-        user_id: UserId,
-        update_user_plan: proto::UpdateUserPlan,
-    ) -> Result<()> {
-        let pool = self.connection_pool.lock();
-        for connection_id in pool.user_connection_ids(user_id) {
-            self.peer
-                .send(connection_id, update_user_plan.clone())
-                .trace_err();
-        }
-
-        Ok(())
-    }
-
-    /// This is the legacy way of updating the user's plan, where we fetch the data to construct the `UpdateUserPlan`
-    /// message on the Collab server.
-    ///
-    /// The new way is to receive the data from Cloud via the `POST /users/:id/update_plan` endpoint.
-    pub async fn update_plan_for_user_legacy(self: &Arc<Self>, user_id: UserId) -> Result<()> {
-        let user = self
-            .app_state
-            .db
-            .get_user_by_id(user_id)
-            .await?
-            .context("user not found")?;
-
-        let update_user_plan = make_update_user_plan_message(
-            &user,
-            user.admin,
-            &self.app_state.db,
-            self.app_state.llm_db.clone(),
-        )
-        .await?;
-
-        self.update_plan_for_user(user_id, update_user_plan).await
-    }
-
-    pub async fn refresh_llm_tokens_for_user(self: &Arc<Self>, user_id: UserId) {
-        let pool = self.connection_pool.lock();
-        for connection_id in pool.user_connection_ids(user_id) {
-            self.peer
-                .send(connection_id, proto::RefreshLlmToken {})
-                .trace_err();
-        }
-    }
-
     pub async fn snapshot(self: &Arc<Self>) -> ServerSnapshot<'_> {
         ServerSnapshot {
             connection_pool: ConnectionPoolGuard {
@@ -1167,10 +1105,10 @@ fn broadcast<F>(
     F: FnMut(ConnectionId) -> anyhow::Result<()>,
 {
     for receiver_id in receiver_ids {
-        if Some(receiver_id) != sender_id {
-            if let Err(error) = f(receiver_id) {
-                tracing::error!("failed to send to {:?} {}", receiver_id, error);
-            }
+        if Some(receiver_id) != sender_id
+            && let Err(error) = f(receiver_id)
+        {
+            tracing::error!("failed to send to {:?} {}", receiver_id, error);
         }
     }
 }
@@ -1452,9 +1390,7 @@ async fn create_room(
         let live_kit = live_kit?;
         let user_id = session.user_id().to_string();
 
-        let token = live_kit
-            .room_token(&livekit_room, &user_id.to_string())
-            .trace_err()?;
+        let token = live_kit.room_token(&livekit_room, &user_id).trace_err()?;
 
         Some(proto::LiveKitConnectionInfo {
             server_url: live_kit.url().into(),
@@ -2081,9 +2017,9 @@ async fn join_project(
         .unzip();
     response.send(proto::JoinProjectResponse {
         project_id: project.id.0 as u64,
-        worktrees: worktrees.clone(),
+        worktrees,
         replica_id: replica_id.0 as u32,
-        collaborators: collaborators.clone(),
+        collaborators,
         language_servers,
         language_server_capabilities,
         role: project.role.into(),
@@ -2360,11 +2296,10 @@ async fn update_language_server(
     let db = session.db().await;
 
     if let Some(proto::update_language_server::Variant::MetadataUpdated(update)) = &request.variant
+        && let Some(capabilities) = update.capabilities.clone()
     {
-        if let Some(capabilities) = update.capabilities.clone() {
-            db.update_server_capabilities(project_id, request.language_server_id, capabilities)
-                .await?;
-        }
+        db.update_server_capabilities(project_id, request.language_server_id, capabilities)
+            .await?;
     }
 
     let project_connection_ids = db
@@ -2425,6 +2360,7 @@ where
     Ok(())
 }
 
+// todo(lsp) remove after Zed Stable hits v0.204.x
 async fn multi_lsp_query(
     request: MultiLspQuery,
     response: Response<MultiLspQuery>,
@@ -2435,6 +2371,21 @@ async fn multi_lsp_query(
     forward_mutating_project_request(request, response, session).await
 }
 
+async fn lsp_query(
+    request: proto::LspQuery,
+    response: Response<proto::LspQuery>,
+    session: MessageContext,
+) -> Result<()> {
+    let (name, should_write) = request.query_name_and_write_permissions();
+    tracing::Span::current().record("lsp_query_request", name);
+    tracing::info!("lsp_query message received");
+    if should_write {
+        forward_mutating_project_request(request, response, session).await
+    } else {
+        forward_read_only_project_request(request, response, session).await
+    }
+}
+
 /// Notify other participants that a new buffer has been created
 async fn create_buffer_for_peer(
     request: proto::CreateBufferForPeer,
@@ -2881,214 +2832,6 @@ fn should_auto_subscribe_to_channels(version: ZedVersion) -> bool {
     version.0.minor() < 139
 }
 
-async fn current_plan(db: &Arc<Database>, user_id: UserId, is_staff: bool) -> Result<proto::Plan> {
-    if is_staff {
-        return Ok(proto::Plan::ZedPro);
-    }
-
-    let subscription = db.get_active_billing_subscription(user_id).await?;
-    let subscription_kind = subscription.and_then(|subscription| subscription.kind);
-
-    let plan = if let Some(subscription_kind) = subscription_kind {
-        match subscription_kind {
-            SubscriptionKind::ZedPro => proto::Plan::ZedPro,
-            SubscriptionKind::ZedProTrial => proto::Plan::ZedProTrial,
-            SubscriptionKind::ZedFree => proto::Plan::Free,
-        }
-    } else {
-        proto::Plan::Free
-    };
-
-    Ok(plan)
-}
-
-async fn make_update_user_plan_message(
-    user: &User,
-    is_staff: bool,
-    db: &Arc<Database>,
-    llm_db: Option<Arc<LlmDatabase>>,
-) -> Result<proto::UpdateUserPlan> {
-    let feature_flags = db.get_user_flags(user.id).await?;
-    let plan = current_plan(db, user.id, is_staff).await?;
-    let billing_customer = db.get_billing_customer_by_user_id(user.id).await?;
-    let billing_preferences = db.get_billing_preferences(user.id).await?;
-
-    let (subscription_period, usage) = if let Some(llm_db) = llm_db {
-        let subscription = db.get_active_billing_subscription(user.id).await?;
-
-        let subscription_period =
-            crate::db::billing_subscription::Model::current_period(subscription, is_staff);
-
-        let usage = if let Some((period_start_at, period_end_at)) = subscription_period {
-            llm_db
-                .get_subscription_usage_for_period(user.id, period_start_at, period_end_at)
-                .await?
-        } else {
-            None
-        };
-
-        (subscription_period, usage)
-    } else {
-        (None, None)
-    };
-
-    let bypass_account_age_check = feature_flags
-        .iter()
-        .any(|flag| flag == BYPASS_ACCOUNT_AGE_CHECK_FEATURE_FLAG);
-    let account_too_young = !matches!(plan, proto::Plan::ZedPro)
-        && !bypass_account_age_check
-        && user.account_age() < MIN_ACCOUNT_AGE_FOR_LLM_USE;
-
-    Ok(proto::UpdateUserPlan {
-        plan: plan.into(),
-        trial_started_at: billing_customer
-            .as_ref()
-            .and_then(|billing_customer| billing_customer.trial_started_at)
-            .map(|trial_started_at| trial_started_at.and_utc().timestamp() as u64),
-        is_usage_based_billing_enabled: if is_staff {
-            Some(true)
-        } else {
-            billing_preferences.map(|preferences| preferences.model_request_overages_enabled)
-        },
-        subscription_period: subscription_period.map(|(started_at, ended_at)| {
-            proto::SubscriptionPeriod {
-                started_at: started_at.timestamp() as u64,
-                ended_at: ended_at.timestamp() as u64,
-            }
-        }),
-        account_too_young: Some(account_too_young),
-        has_overdue_invoices: billing_customer
-            .map(|billing_customer| billing_customer.has_overdue_invoices),
-        usage: Some(
-            usage
-                .map(|usage| subscription_usage_to_proto(plan, usage, &feature_flags))
-                .unwrap_or_else(|| make_default_subscription_usage(plan, &feature_flags)),
-        ),
-    })
-}
-
-fn model_requests_limit(
-    plan: cloud_llm_client::Plan,
-    feature_flags: &Vec<String>,
-) -> cloud_llm_client::UsageLimit {
-    match plan.model_requests_limit() {
-        cloud_llm_client::UsageLimit::Limited(limit) => {
-            let limit = if plan == cloud_llm_client::Plan::ZedProTrial
-                && feature_flags
-                    .iter()
-                    .any(|flag| flag == AGENT_EXTENDED_TRIAL_FEATURE_FLAG)
-            {
-                1_000
-            } else {
-                limit
-            };
-
-            cloud_llm_client::UsageLimit::Limited(limit)
-        }
-        cloud_llm_client::UsageLimit::Unlimited => cloud_llm_client::UsageLimit::Unlimited,
-    }
-}
-
-fn subscription_usage_to_proto(
-    plan: proto::Plan,
-    usage: crate::llm::db::subscription_usage::Model,
-    feature_flags: &Vec<String>,
-) -> proto::SubscriptionUsage {
-    let plan = match plan {
-        proto::Plan::Free => cloud_llm_client::Plan::ZedFree,
-        proto::Plan::ZedPro => cloud_llm_client::Plan::ZedPro,
-        proto::Plan::ZedProTrial => cloud_llm_client::Plan::ZedProTrial,
-    };
-
-    proto::SubscriptionUsage {
-        model_requests_usage_amount: usage.model_requests as u32,
-        model_requests_usage_limit: Some(proto::UsageLimit {
-            variant: Some(match model_requests_limit(plan, feature_flags) {
-                cloud_llm_client::UsageLimit::Limited(limit) => {
-                    proto::usage_limit::Variant::Limited(proto::usage_limit::Limited {
-                        limit: limit as u32,
-                    })
-                }
-                cloud_llm_client::UsageLimit::Unlimited => {
-                    proto::usage_limit::Variant::Unlimited(proto::usage_limit::Unlimited {})
-                }
-            }),
-        }),
-        edit_predictions_usage_amount: usage.edit_predictions as u32,
-        edit_predictions_usage_limit: Some(proto::UsageLimit {
-            variant: Some(match plan.edit_predictions_limit() {
-                cloud_llm_client::UsageLimit::Limited(limit) => {
-                    proto::usage_limit::Variant::Limited(proto::usage_limit::Limited {
-                        limit: limit as u32,
-                    })
-                }
-                cloud_llm_client::UsageLimit::Unlimited => {
-                    proto::usage_limit::Variant::Unlimited(proto::usage_limit::Unlimited {})
-                }
-            }),
-        }),
-    }
-}
-
-fn make_default_subscription_usage(
-    plan: proto::Plan,
-    feature_flags: &Vec<String>,
-) -> proto::SubscriptionUsage {
-    let plan = match plan {
-        proto::Plan::Free => cloud_llm_client::Plan::ZedFree,
-        proto::Plan::ZedPro => cloud_llm_client::Plan::ZedPro,
-        proto::Plan::ZedProTrial => cloud_llm_client::Plan::ZedProTrial,
-    };
-
-    proto::SubscriptionUsage {
-        model_requests_usage_amount: 0,
-        model_requests_usage_limit: Some(proto::UsageLimit {
-            variant: Some(match model_requests_limit(plan, feature_flags) {
-                cloud_llm_client::UsageLimit::Limited(limit) => {
-                    proto::usage_limit::Variant::Limited(proto::usage_limit::Limited {
-                        limit: limit as u32,
-                    })
-                }
-                cloud_llm_client::UsageLimit::Unlimited => {
-                    proto::usage_limit::Variant::Unlimited(proto::usage_limit::Unlimited {})
-                }
-            }),
-        }),
-        edit_predictions_usage_amount: 0,
-        edit_predictions_usage_limit: Some(proto::UsageLimit {
-            variant: Some(match plan.edit_predictions_limit() {
-                cloud_llm_client::UsageLimit::Limited(limit) => {
-                    proto::usage_limit::Variant::Limited(proto::usage_limit::Limited {
-                        limit: limit as u32,
-                    })
-                }
-                cloud_llm_client::UsageLimit::Unlimited => {
-                    proto::usage_limit::Variant::Unlimited(proto::usage_limit::Unlimited {})
-                }
-            }),
-        }),
-    }
-}
-
-async fn update_user_plan(session: &Session) -> Result<()> {
-    let db = session.db().await;
-
-    let update_user_plan = make_update_user_plan_message(
-        session.principal.user(),
-        session.is_staff(),
-        &db.0,
-        session.app_state.llm_db.clone(),
-    )
-    .await?;
-
-    session
-        .peer
-        .send(session.connection_id, update_user_plan)
-        .trace_err();
-
-    Ok(())
-}
-
 async fn subscribe_to_channels(
     _: proto::SubscribeToChannels,
     session: MessageContext,
@@ -4257,139 +4000,6 @@ async fn mark_notification_as_read(
     Ok(())
 }
 
-/// Get the current users information
-async fn get_private_user_info(
-    _request: proto::GetPrivateUserInfo,
-    response: Response<proto::GetPrivateUserInfo>,
-    session: MessageContext,
-) -> Result<()> {
-    let db = session.db().await;
-
-    let metrics_id = db.get_user_metrics_id(session.user_id()).await?;
-    let user = db
-        .get_user_by_id(session.user_id())
-        .await?
-        .context("user not found")?;
-    let flags = db.get_user_flags(session.user_id()).await?;
-
-    response.send(proto::GetPrivateUserInfoResponse {
-        metrics_id,
-        staff: user.admin,
-        flags,
-        accepted_tos_at: user.accepted_tos_at.map(|t| t.and_utc().timestamp() as u64),
-    })?;
-    Ok(())
-}
-
-/// Accept the terms of service (tos) on behalf of the current user
-async fn accept_terms_of_service(
-    _request: proto::AcceptTermsOfService,
-    response: Response<proto::AcceptTermsOfService>,
-    session: MessageContext,
-) -> Result<()> {
-    let db = session.db().await;
-
-    let accepted_tos_at = Utc::now();
-    db.set_user_accepted_tos_at(session.user_id(), Some(accepted_tos_at.naive_utc()))
-        .await?;
-
-    response.send(proto::AcceptTermsOfServiceResponse {
-        accepted_tos_at: accepted_tos_at.timestamp() as u64,
-    })?;
-
-    // When the user accepts the terms of service, we want to refresh their LLM
-    // token to grant access.
-    session
-        .peer
-        .send(session.connection_id, proto::RefreshLlmToken {})?;
-
-    Ok(())
-}
-
-async fn get_llm_api_token(
-    _request: proto::GetLlmToken,
-    response: Response<proto::GetLlmToken>,
-    session: MessageContext,
-) -> Result<()> {
-    let db = session.db().await;
-
-    let flags = db.get_user_flags(session.user_id()).await?;
-
-    let user_id = session.user_id();
-    let user = db
-        .get_user_by_id(user_id)
-        .await?
-        .with_context(|| format!("user {user_id} not found"))?;
-
-    if user.accepted_tos_at.is_none() {
-        Err(anyhow!("terms of service not accepted"))?
-    }
-
-    let stripe_client = session
-        .app_state
-        .stripe_client
-        .as_ref()
-        .context("failed to retrieve Stripe client")?;
-
-    let stripe_billing = session
-        .app_state
-        .stripe_billing
-        .as_ref()
-        .context("failed to retrieve Stripe billing object")?;
-
-    let billing_customer = if let Some(billing_customer) =
-        db.get_billing_customer_by_user_id(user.id).await?
-    {
-        billing_customer
-    } else {
-        let customer_id = stripe_billing
-            .find_or_create_customer_by_email(user.email_address.as_deref())
-            .await?;
-
-        find_or_create_billing_customer(&session.app_state, stripe_client.as_ref(), &customer_id)
-            .await?
-            .context("billing customer not found")?
-    };
-
-    let billing_subscription =
-        if let Some(billing_subscription) = db.get_active_billing_subscription(user.id).await? {
-            billing_subscription
-        } else {
-            let stripe_customer_id =
-                StripeCustomerId(billing_customer.stripe_customer_id.clone().into());
-
-            let stripe_subscription = stripe_billing
-                .subscribe_to_zed_free(stripe_customer_id)
-                .await?;
-
-            db.create_billing_subscription(&db::CreateBillingSubscriptionParams {
-                billing_customer_id: billing_customer.id,
-                kind: Some(SubscriptionKind::ZedFree),
-                stripe_subscription_id: stripe_subscription.id.to_string(),
-                stripe_subscription_status: stripe_subscription.status.into(),
-                stripe_cancellation_reason: None,
-                stripe_current_period_start: Some(stripe_subscription.current_period_start),
-                stripe_current_period_end: Some(stripe_subscription.current_period_end),
-            })
-            .await?
-        };
-
-    let billing_preferences = db.get_billing_preferences(user.id).await?;
-
-    let token = LlmTokenClaims::create(
-        &user,
-        session.is_staff(),
-        billing_customer,
-        billing_preferences,
-        &flags,
-        billing_subscription,
-        session.system_id.clone(),
-        &session.app_state.config,
-    )?;
-    response.send(proto::GetLlmTokenResponse { token })?;
-    Ok(())
-}
-
 fn to_axum_message(message: TungsteniteMessage) -> anyhow::Result<AxumMessage> {
     let message = match message {
         TungsteniteMessage::Text(payload) => AxumMessage::Text(payload.as_str().to_string()),

crates/collab/src/rpc/connection_pool.rs 🔗

@@ -30,7 +30,19 @@ impl fmt::Display for ZedVersion {
 
 impl ZedVersion {
     pub fn can_collaborate(&self) -> bool {
-        self.0 >= SemanticVersion::new(0, 157, 0)
+        // v0.198.4 is the first version where we no longer connect to Collab automatically.
+        // We reject any clients older than that to prevent them from connecting to Collab just for authentication.
+        if self.0 < SemanticVersion::new(0, 198, 4) {
+            return false;
+        }
+
+        // Since we hotfixed the changes to no longer connect to Collab automatically to Preview, we also need to reject
+        // versions in the range [v0.199.0, v0.199.1].
+        if self.0 >= SemanticVersion::new(0, 199, 0) && self.0 < SemanticVersion::new(0, 199, 2) {
+            return false;
+        }
+
+        true
     }
 }
 

crates/collab/src/stripe_billing.rs 🔗

@@ -1,156 +0,0 @@
-use std::sync::Arc;
-
-use anyhow::anyhow;
-use collections::HashMap;
-use stripe::SubscriptionStatus;
-use tokio::sync::RwLock;
-
-use crate::Result;
-use crate::stripe_client::{
-    RealStripeClient, StripeAutomaticTax, StripeClient, StripeCreateSubscriptionItems,
-    StripeCreateSubscriptionParams, StripeCustomerId, StripePrice, StripePriceId,
-    StripeSubscription,
-};
-
-pub struct StripeBilling {
-    state: RwLock<StripeBillingState>,
-    client: Arc<dyn StripeClient>,
-}
-
-#[derive(Default)]
-struct StripeBillingState {
-    prices_by_lookup_key: HashMap<String, StripePrice>,
-}
-
-impl StripeBilling {
-    pub fn new(client: Arc<stripe::Client>) -> Self {
-        Self {
-            client: Arc::new(RealStripeClient::new(client.clone())),
-            state: RwLock::default(),
-        }
-    }
-
-    #[cfg(test)]
-    pub fn test(client: Arc<crate::stripe_client::FakeStripeClient>) -> Self {
-        Self {
-            client,
-            state: RwLock::default(),
-        }
-    }
-
-    pub fn client(&self) -> &Arc<dyn StripeClient> {
-        &self.client
-    }
-
-    pub async fn initialize(&self) -> Result<()> {
-        log::info!("StripeBilling: initializing");
-
-        let mut state = self.state.write().await;
-
-        let prices = self.client.list_prices().await?;
-
-        for price in prices {
-            if let Some(lookup_key) = price.lookup_key.clone() {
-                state.prices_by_lookup_key.insert(lookup_key, price);
-            }
-        }
-
-        log::info!("StripeBilling: initialized");
-
-        Ok(())
-    }
-
-    pub async fn zed_pro_price_id(&self) -> Result<StripePriceId> {
-        self.find_price_id_by_lookup_key("zed-pro").await
-    }
-
-    pub async fn zed_free_price_id(&self) -> Result<StripePriceId> {
-        self.find_price_id_by_lookup_key("zed-free").await
-    }
-
-    pub async fn find_price_id_by_lookup_key(&self, lookup_key: &str) -> Result<StripePriceId> {
-        self.state
-            .read()
-            .await
-            .prices_by_lookup_key
-            .get(lookup_key)
-            .map(|price| price.id.clone())
-            .ok_or_else(|| crate::Error::Internal(anyhow!("no price ID found for {lookup_key:?}")))
-    }
-
-    pub async fn find_price_by_lookup_key(&self, lookup_key: &str) -> Result<StripePrice> {
-        self.state
-            .read()
-            .await
-            .prices_by_lookup_key
-            .get(lookup_key)
-            .cloned()
-            .ok_or_else(|| crate::Error::Internal(anyhow!("no price found for {lookup_key:?}")))
-    }
-
-    /// Returns the Stripe customer associated with the provided email address, or creates a new customer, if one does
-    /// not already exist.
-    ///
-    /// Always returns a new Stripe customer if the email address is `None`.
-    pub async fn find_or_create_customer_by_email(
-        &self,
-        email_address: Option<&str>,
-    ) -> Result<StripeCustomerId> {
-        let existing_customer = if let Some(email) = email_address {
-            let customers = self.client.list_customers_by_email(email).await?;
-
-            customers.first().cloned()
-        } else {
-            None
-        };
-
-        let customer_id = if let Some(existing_customer) = existing_customer {
-            existing_customer.id
-        } else {
-            let customer = self
-                .client
-                .create_customer(crate::stripe_client::CreateCustomerParams {
-                    email: email_address,
-                })
-                .await?;
-
-            customer.id
-        };
-
-        Ok(customer_id)
-    }
-
-    pub async fn subscribe_to_zed_free(
-        &self,
-        customer_id: StripeCustomerId,
-    ) -> Result<StripeSubscription> {
-        let zed_free_price_id = self.zed_free_price_id().await?;
-
-        let existing_subscriptions = self
-            .client
-            .list_subscriptions_for_customer(&customer_id)
-            .await?;
-
-        let existing_active_subscription =
-            existing_subscriptions.into_iter().find(|subscription| {
-                subscription.status == SubscriptionStatus::Active
-                    || subscription.status == SubscriptionStatus::Trialing
-            });
-        if let Some(subscription) = existing_active_subscription {
-            return Ok(subscription);
-        }
-
-        let params = StripeCreateSubscriptionParams {
-            customer: customer_id,
-            items: vec![StripeCreateSubscriptionItems {
-                price: Some(zed_free_price_id),
-                quantity: Some(1),
-            }],
-            automatic_tax: Some(StripeAutomaticTax { enabled: true }),
-        };
-
-        let subscription = self.client.create_subscription(params).await?;
-
-        Ok(subscription)
-    }
-}

crates/collab/src/stripe_client.rs 🔗

@@ -1,285 +0,0 @@
-#[cfg(test)]
-mod fake_stripe_client;
-mod real_stripe_client;
-
-use std::collections::HashMap;
-use std::sync::Arc;
-
-use anyhow::Result;
-use async_trait::async_trait;
-
-#[cfg(test)]
-pub use fake_stripe_client::*;
-pub use real_stripe_client::*;
-use serde::{Deserialize, Serialize};
-
-#[derive(Debug, PartialEq, Eq, Hash, Clone, derive_more::Display, Serialize)]
-pub struct StripeCustomerId(pub Arc<str>);
-
-#[derive(Debug, Clone)]
-pub struct StripeCustomer {
-    pub id: StripeCustomerId,
-    pub email: Option<String>,
-}
-
-#[derive(Debug)]
-pub struct CreateCustomerParams<'a> {
-    pub email: Option<&'a str>,
-}
-
-#[derive(Debug)]
-pub struct UpdateCustomerParams<'a> {
-    pub email: Option<&'a str>,
-}
-
-#[derive(Debug, PartialEq, Eq, Hash, Clone, derive_more::Display)]
-pub struct StripeSubscriptionId(pub Arc<str>);
-
-#[derive(Debug, PartialEq, Clone)]
-pub struct StripeSubscription {
-    pub id: StripeSubscriptionId,
-    pub customer: StripeCustomerId,
-    // TODO: Create our own version of this enum.
-    pub status: stripe::SubscriptionStatus,
-    pub current_period_end: i64,
-    pub current_period_start: i64,
-    pub items: Vec<StripeSubscriptionItem>,
-    pub cancel_at: Option<i64>,
-    pub cancellation_details: Option<StripeCancellationDetails>,
-}
-
-#[derive(Debug, PartialEq, Eq, Hash, Clone, derive_more::Display)]
-pub struct StripeSubscriptionItemId(pub Arc<str>);
-
-#[derive(Debug, PartialEq, Clone)]
-pub struct StripeSubscriptionItem {
-    pub id: StripeSubscriptionItemId,
-    pub price: Option<StripePrice>,
-}
-
-#[derive(Debug, Clone, PartialEq)]
-pub struct StripeCancellationDetails {
-    pub reason: Option<StripeCancellationDetailsReason>,
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Copy)]
-pub enum StripeCancellationDetailsReason {
-    CancellationRequested,
-    PaymentDisputed,
-    PaymentFailed,
-}
-
-#[derive(Debug)]
-pub struct StripeCreateSubscriptionParams {
-    pub customer: StripeCustomerId,
-    pub items: Vec<StripeCreateSubscriptionItems>,
-    pub automatic_tax: Option<StripeAutomaticTax>,
-}
-
-#[derive(Debug)]
-pub struct StripeCreateSubscriptionItems {
-    pub price: Option<StripePriceId>,
-    pub quantity: Option<u64>,
-}
-
-#[derive(Debug, Clone)]
-pub struct UpdateSubscriptionParams {
-    pub items: Option<Vec<UpdateSubscriptionItems>>,
-    pub trial_settings: Option<StripeSubscriptionTrialSettings>,
-}
-
-#[derive(Debug, PartialEq, Clone)]
-pub struct UpdateSubscriptionItems {
-    pub price: Option<StripePriceId>,
-}
-
-#[derive(Debug, PartialEq, Clone)]
-pub struct StripeSubscriptionTrialSettings {
-    pub end_behavior: StripeSubscriptionTrialSettingsEndBehavior,
-}
-
-#[derive(Debug, PartialEq, Clone)]
-pub struct StripeSubscriptionTrialSettingsEndBehavior {
-    pub missing_payment_method: StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod,
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Copy)]
-pub enum StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod {
-    Cancel,
-    CreateInvoice,
-    Pause,
-}
-
-#[derive(Debug, PartialEq, Eq, Hash, Clone, derive_more::Display)]
-pub struct StripePriceId(pub Arc<str>);
-
-#[derive(Debug, PartialEq, Clone)]
-pub struct StripePrice {
-    pub id: StripePriceId,
-    pub unit_amount: Option<i64>,
-    pub lookup_key: Option<String>,
-    pub recurring: Option<StripePriceRecurring>,
-}
-
-#[derive(Debug, PartialEq, Clone)]
-pub struct StripePriceRecurring {
-    pub meter: Option<String>,
-}
-
-#[derive(Debug, PartialEq, Eq, Hash, Clone, derive_more::Display, Deserialize)]
-pub struct StripeMeterId(pub Arc<str>);
-
-#[derive(Debug, Clone, Deserialize)]
-pub struct StripeMeter {
-    pub id: StripeMeterId,
-    pub event_name: String,
-}
-
-#[derive(Debug, Serialize)]
-pub struct StripeCreateMeterEventParams<'a> {
-    pub identifier: &'a str,
-    pub event_name: &'a str,
-    pub payload: StripeCreateMeterEventPayload<'a>,
-    pub timestamp: Option<i64>,
-}
-
-#[derive(Debug, Serialize)]
-pub struct StripeCreateMeterEventPayload<'a> {
-    pub value: u64,
-    pub stripe_customer_id: &'a StripeCustomerId,
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Copy)]
-pub enum StripeBillingAddressCollection {
-    Auto,
-    Required,
-}
-
-#[derive(Debug, PartialEq, Clone)]
-pub struct StripeCustomerUpdate {
-    pub address: Option<StripeCustomerUpdateAddress>,
-    pub name: Option<StripeCustomerUpdateName>,
-    pub shipping: Option<StripeCustomerUpdateShipping>,
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Copy)]
-pub enum StripeCustomerUpdateAddress {
-    Auto,
-    Never,
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Copy)]
-pub enum StripeCustomerUpdateName {
-    Auto,
-    Never,
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Copy)]
-pub enum StripeCustomerUpdateShipping {
-    Auto,
-    Never,
-}
-
-#[derive(Debug, Default)]
-pub struct StripeCreateCheckoutSessionParams<'a> {
-    pub customer: Option<&'a StripeCustomerId>,
-    pub client_reference_id: Option<&'a str>,
-    pub mode: Option<StripeCheckoutSessionMode>,
-    pub line_items: Option<Vec<StripeCreateCheckoutSessionLineItems>>,
-    pub payment_method_collection: Option<StripeCheckoutSessionPaymentMethodCollection>,
-    pub subscription_data: Option<StripeCreateCheckoutSessionSubscriptionData>,
-    pub success_url: Option<&'a str>,
-    pub billing_address_collection: Option<StripeBillingAddressCollection>,
-    pub customer_update: Option<StripeCustomerUpdate>,
-    pub tax_id_collection: Option<StripeTaxIdCollection>,
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Copy)]
-pub enum StripeCheckoutSessionMode {
-    Payment,
-    Setup,
-    Subscription,
-}
-
-#[derive(Debug, PartialEq, Clone)]
-pub struct StripeCreateCheckoutSessionLineItems {
-    pub price: Option<String>,
-    pub quantity: Option<u64>,
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Copy)]
-pub enum StripeCheckoutSessionPaymentMethodCollection {
-    Always,
-    IfRequired,
-}
-
-#[derive(Debug, PartialEq, Clone)]
-pub struct StripeCreateCheckoutSessionSubscriptionData {
-    pub metadata: Option<HashMap<String, String>>,
-    pub trial_period_days: Option<u32>,
-    pub trial_settings: Option<StripeSubscriptionTrialSettings>,
-}
-
-#[derive(Debug, PartialEq, Clone)]
-pub struct StripeTaxIdCollection {
-    pub enabled: bool,
-}
-
-#[derive(Debug, Clone)]
-pub struct StripeAutomaticTax {
-    pub enabled: bool,
-}
-
-#[derive(Debug)]
-pub struct StripeCheckoutSession {
-    pub url: Option<String>,
-}
-
-#[async_trait]
-pub trait StripeClient: Send + Sync {
-    async fn list_customers_by_email(&self, email: &str) -> Result<Vec<StripeCustomer>>;
-
-    async fn get_customer(&self, customer_id: &StripeCustomerId) -> Result<StripeCustomer>;
-
-    async fn create_customer(&self, params: CreateCustomerParams<'_>) -> Result<StripeCustomer>;
-
-    async fn update_customer(
-        &self,
-        customer_id: &StripeCustomerId,
-        params: UpdateCustomerParams<'_>,
-    ) -> Result<StripeCustomer>;
-
-    async fn list_subscriptions_for_customer(
-        &self,
-        customer_id: &StripeCustomerId,
-    ) -> Result<Vec<StripeSubscription>>;
-
-    async fn get_subscription(
-        &self,
-        subscription_id: &StripeSubscriptionId,
-    ) -> Result<StripeSubscription>;
-
-    async fn create_subscription(
-        &self,
-        params: StripeCreateSubscriptionParams,
-    ) -> Result<StripeSubscription>;
-
-    async fn update_subscription(
-        &self,
-        subscription_id: &StripeSubscriptionId,
-        params: UpdateSubscriptionParams,
-    ) -> Result<()>;
-
-    async fn cancel_subscription(&self, subscription_id: &StripeSubscriptionId) -> Result<()>;
-
-    async fn list_prices(&self) -> Result<Vec<StripePrice>>;
-
-    async fn list_meters(&self) -> Result<Vec<StripeMeter>>;
-
-    async fn create_meter_event(&self, params: StripeCreateMeterEventParams<'_>) -> Result<()>;
-
-    async fn create_checkout_session(
-        &self,
-        params: StripeCreateCheckoutSessionParams<'_>,
-    ) -> Result<StripeCheckoutSession>;
-}

crates/collab/src/stripe_client/fake_stripe_client.rs 🔗

@@ -1,247 +0,0 @@
-use std::sync::Arc;
-
-use anyhow::{Result, anyhow};
-use async_trait::async_trait;
-use chrono::{Duration, Utc};
-use collections::HashMap;
-use parking_lot::Mutex;
-use uuid::Uuid;
-
-use crate::stripe_client::{
-    CreateCustomerParams, StripeBillingAddressCollection, StripeCheckoutSession,
-    StripeCheckoutSessionMode, StripeCheckoutSessionPaymentMethodCollection, StripeClient,
-    StripeCreateCheckoutSessionLineItems, StripeCreateCheckoutSessionParams,
-    StripeCreateCheckoutSessionSubscriptionData, StripeCreateMeterEventParams,
-    StripeCreateSubscriptionParams, StripeCustomer, StripeCustomerId, StripeCustomerUpdate,
-    StripeMeter, StripeMeterId, StripePrice, StripePriceId, StripeSubscription,
-    StripeSubscriptionId, StripeSubscriptionItem, StripeSubscriptionItemId, StripeTaxIdCollection,
-    UpdateCustomerParams, UpdateSubscriptionParams,
-};
-
-#[derive(Debug, Clone)]
-pub struct StripeCreateMeterEventCall {
-    pub identifier: Arc<str>,
-    pub event_name: Arc<str>,
-    pub value: u64,
-    pub stripe_customer_id: StripeCustomerId,
-    pub timestamp: Option<i64>,
-}
-
-#[derive(Debug, Clone)]
-pub struct StripeCreateCheckoutSessionCall {
-    pub customer: Option<StripeCustomerId>,
-    pub client_reference_id: Option<String>,
-    pub mode: Option<StripeCheckoutSessionMode>,
-    pub line_items: Option<Vec<StripeCreateCheckoutSessionLineItems>>,
-    pub payment_method_collection: Option<StripeCheckoutSessionPaymentMethodCollection>,
-    pub subscription_data: Option<StripeCreateCheckoutSessionSubscriptionData>,
-    pub success_url: Option<String>,
-    pub billing_address_collection: Option<StripeBillingAddressCollection>,
-    pub customer_update: Option<StripeCustomerUpdate>,
-    pub tax_id_collection: Option<StripeTaxIdCollection>,
-}
-
-pub struct FakeStripeClient {
-    pub customers: Arc<Mutex<HashMap<StripeCustomerId, StripeCustomer>>>,
-    pub subscriptions: Arc<Mutex<HashMap<StripeSubscriptionId, StripeSubscription>>>,
-    pub update_subscription_calls:
-        Arc<Mutex<Vec<(StripeSubscriptionId, UpdateSubscriptionParams)>>>,
-    pub prices: Arc<Mutex<HashMap<StripePriceId, StripePrice>>>,
-    pub meters: Arc<Mutex<HashMap<StripeMeterId, StripeMeter>>>,
-    pub create_meter_event_calls: Arc<Mutex<Vec<StripeCreateMeterEventCall>>>,
-    pub create_checkout_session_calls: Arc<Mutex<Vec<StripeCreateCheckoutSessionCall>>>,
-}
-
-impl FakeStripeClient {
-    pub fn new() -> Self {
-        Self {
-            customers: Arc::new(Mutex::new(HashMap::default())),
-            subscriptions: Arc::new(Mutex::new(HashMap::default())),
-            update_subscription_calls: Arc::new(Mutex::new(Vec::new())),
-            prices: Arc::new(Mutex::new(HashMap::default())),
-            meters: Arc::new(Mutex::new(HashMap::default())),
-            create_meter_event_calls: Arc::new(Mutex::new(Vec::new())),
-            create_checkout_session_calls: Arc::new(Mutex::new(Vec::new())),
-        }
-    }
-}
-
-#[async_trait]
-impl StripeClient for FakeStripeClient {
-    async fn list_customers_by_email(&self, email: &str) -> Result<Vec<StripeCustomer>> {
-        Ok(self
-            .customers
-            .lock()
-            .values()
-            .filter(|customer| customer.email.as_deref() == Some(email))
-            .cloned()
-            .collect())
-    }
-
-    async fn get_customer(&self, customer_id: &StripeCustomerId) -> Result<StripeCustomer> {
-        self.customers
-            .lock()
-            .get(customer_id)
-            .cloned()
-            .ok_or_else(|| anyhow!("no customer found for {customer_id:?}"))
-    }
-
-    async fn create_customer(&self, params: CreateCustomerParams<'_>) -> Result<StripeCustomer> {
-        let customer = StripeCustomer {
-            id: StripeCustomerId(format!("cus_{}", Uuid::new_v4()).into()),
-            email: params.email.map(|email| email.to_string()),
-        };
-
-        self.customers
-            .lock()
-            .insert(customer.id.clone(), customer.clone());
-
-        Ok(customer)
-    }
-
-    async fn update_customer(
-        &self,
-        customer_id: &StripeCustomerId,
-        params: UpdateCustomerParams<'_>,
-    ) -> Result<StripeCustomer> {
-        let mut customers = self.customers.lock();
-        if let Some(customer) = customers.get_mut(customer_id) {
-            if let Some(email) = params.email {
-                customer.email = Some(email.to_string());
-            }
-            Ok(customer.clone())
-        } else {
-            Err(anyhow!("no customer found for {customer_id:?}"))
-        }
-    }
-
-    async fn list_subscriptions_for_customer(
-        &self,
-        customer_id: &StripeCustomerId,
-    ) -> Result<Vec<StripeSubscription>> {
-        let subscriptions = self
-            .subscriptions
-            .lock()
-            .values()
-            .filter(|subscription| subscription.customer == *customer_id)
-            .cloned()
-            .collect();
-
-        Ok(subscriptions)
-    }
-
-    async fn get_subscription(
-        &self,
-        subscription_id: &StripeSubscriptionId,
-    ) -> Result<StripeSubscription> {
-        self.subscriptions
-            .lock()
-            .get(subscription_id)
-            .cloned()
-            .ok_or_else(|| anyhow!("no subscription found for {subscription_id:?}"))
-    }
-
-    async fn create_subscription(
-        &self,
-        params: StripeCreateSubscriptionParams,
-    ) -> Result<StripeSubscription> {
-        let now = Utc::now();
-
-        let subscription = StripeSubscription {
-            id: StripeSubscriptionId(format!("sub_{}", Uuid::new_v4()).into()),
-            customer: params.customer,
-            status: stripe::SubscriptionStatus::Active,
-            current_period_start: now.timestamp(),
-            current_period_end: (now + Duration::days(30)).timestamp(),
-            items: params
-                .items
-                .into_iter()
-                .map(|item| StripeSubscriptionItem {
-                    id: StripeSubscriptionItemId(format!("si_{}", Uuid::new_v4()).into()),
-                    price: item
-                        .price
-                        .and_then(|price_id| self.prices.lock().get(&price_id).cloned()),
-                })
-                .collect(),
-            cancel_at: None,
-            cancellation_details: None,
-        };
-
-        self.subscriptions
-            .lock()
-            .insert(subscription.id.clone(), subscription.clone());
-
-        Ok(subscription)
-    }
-
-    async fn update_subscription(
-        &self,
-        subscription_id: &StripeSubscriptionId,
-        params: UpdateSubscriptionParams,
-    ) -> Result<()> {
-        let subscription = self.get_subscription(subscription_id).await?;
-
-        self.update_subscription_calls
-            .lock()
-            .push((subscription.id, params));
-
-        Ok(())
-    }
-
-    async fn cancel_subscription(&self, subscription_id: &StripeSubscriptionId) -> Result<()> {
-        // TODO: Implement fake subscription cancellation.
-        let _ = subscription_id;
-
-        Ok(())
-    }
-
-    async fn list_prices(&self) -> Result<Vec<StripePrice>> {
-        let prices = self.prices.lock().values().cloned().collect();
-
-        Ok(prices)
-    }
-
-    async fn list_meters(&self) -> Result<Vec<StripeMeter>> {
-        let meters = self.meters.lock().values().cloned().collect();
-
-        Ok(meters)
-    }
-
-    async fn create_meter_event(&self, params: StripeCreateMeterEventParams<'_>) -> Result<()> {
-        self.create_meter_event_calls
-            .lock()
-            .push(StripeCreateMeterEventCall {
-                identifier: params.identifier.into(),
-                event_name: params.event_name.into(),
-                value: params.payload.value,
-                stripe_customer_id: params.payload.stripe_customer_id.clone(),
-                timestamp: params.timestamp,
-            });
-
-        Ok(())
-    }
-
-    async fn create_checkout_session(
-        &self,
-        params: StripeCreateCheckoutSessionParams<'_>,
-    ) -> Result<StripeCheckoutSession> {
-        self.create_checkout_session_calls
-            .lock()
-            .push(StripeCreateCheckoutSessionCall {
-                customer: params.customer.cloned(),
-                client_reference_id: params.client_reference_id.map(|id| id.to_string()),
-                mode: params.mode,
-                line_items: params.line_items,
-                payment_method_collection: params.payment_method_collection,
-                subscription_data: params.subscription_data,
-                success_url: params.success_url.map(|url| url.to_string()),
-                billing_address_collection: params.billing_address_collection,
-                customer_update: params.customer_update,
-                tax_id_collection: params.tax_id_collection,
-            });
-
-        Ok(StripeCheckoutSession {
-            url: Some("https://checkout.stripe.com/c/pay/cs_test_1".to_string()),
-        })
-    }
-}

crates/collab/src/stripe_client/real_stripe_client.rs 🔗

@@ -1,612 +0,0 @@
-use std::str::FromStr as _;
-use std::sync::Arc;
-
-use anyhow::{Context as _, Result, anyhow};
-use async_trait::async_trait;
-use serde::{Deserialize, Serialize};
-use stripe::{
-    CancellationDetails, CancellationDetailsReason, CheckoutSession, CheckoutSessionMode,
-    CheckoutSessionPaymentMethodCollection, CreateCheckoutSession, CreateCheckoutSessionLineItems,
-    CreateCheckoutSessionSubscriptionData, CreateCheckoutSessionSubscriptionDataTrialSettings,
-    CreateCheckoutSessionSubscriptionDataTrialSettingsEndBehavior,
-    CreateCheckoutSessionSubscriptionDataTrialSettingsEndBehaviorMissingPaymentMethod,
-    CreateCustomer, CreateSubscriptionAutomaticTax, Customer, CustomerId, ListCustomers, Price,
-    PriceId, Recurring, Subscription, SubscriptionId, SubscriptionItem, SubscriptionItemId,
-    UpdateCustomer, UpdateSubscriptionItems, UpdateSubscriptionTrialSettings,
-    UpdateSubscriptionTrialSettingsEndBehavior,
-    UpdateSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod,
-};
-
-use crate::stripe_client::{
-    CreateCustomerParams, StripeAutomaticTax, StripeBillingAddressCollection,
-    StripeCancellationDetails, StripeCancellationDetailsReason, StripeCheckoutSession,
-    StripeCheckoutSessionMode, StripeCheckoutSessionPaymentMethodCollection, StripeClient,
-    StripeCreateCheckoutSessionLineItems, StripeCreateCheckoutSessionParams,
-    StripeCreateCheckoutSessionSubscriptionData, StripeCreateMeterEventParams,
-    StripeCreateSubscriptionParams, StripeCustomer, StripeCustomerId, StripeCustomerUpdate,
-    StripeCustomerUpdateAddress, StripeCustomerUpdateName, StripeCustomerUpdateShipping,
-    StripeMeter, StripePrice, StripePriceId, StripePriceRecurring, StripeSubscription,
-    StripeSubscriptionId, StripeSubscriptionItem, StripeSubscriptionItemId,
-    StripeSubscriptionTrialSettings, StripeSubscriptionTrialSettingsEndBehavior,
-    StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod, StripeTaxIdCollection,
-    UpdateCustomerParams, UpdateSubscriptionParams,
-};
-
-pub struct RealStripeClient {
-    client: Arc<stripe::Client>,
-}
-
-impl RealStripeClient {
-    pub fn new(client: Arc<stripe::Client>) -> Self {
-        Self { client }
-    }
-}
-
-#[async_trait]
-impl StripeClient for RealStripeClient {
-    async fn list_customers_by_email(&self, email: &str) -> Result<Vec<StripeCustomer>> {
-        let response = Customer::list(
-            &self.client,
-            &ListCustomers {
-                email: Some(email),
-                ..Default::default()
-            },
-        )
-        .await?;
-
-        Ok(response
-            .data
-            .into_iter()
-            .map(StripeCustomer::from)
-            .collect())
-    }
-
-    async fn get_customer(&self, customer_id: &StripeCustomerId) -> Result<StripeCustomer> {
-        let customer_id = customer_id.try_into()?;
-
-        let customer = Customer::retrieve(&self.client, &customer_id, &[]).await?;
-
-        Ok(StripeCustomer::from(customer))
-    }
-
-    async fn create_customer(&self, params: CreateCustomerParams<'_>) -> Result<StripeCustomer> {
-        let customer = Customer::create(
-            &self.client,
-            CreateCustomer {
-                email: params.email,
-                ..Default::default()
-            },
-        )
-        .await?;
-
-        Ok(StripeCustomer::from(customer))
-    }
-
-    async fn update_customer(
-        &self,
-        customer_id: &StripeCustomerId,
-        params: UpdateCustomerParams<'_>,
-    ) -> Result<StripeCustomer> {
-        let customer = Customer::update(
-            &self.client,
-            &customer_id.try_into()?,
-            UpdateCustomer {
-                email: params.email,
-                ..Default::default()
-            },
-        )
-        .await?;
-
-        Ok(StripeCustomer::from(customer))
-    }
-
-    async fn list_subscriptions_for_customer(
-        &self,
-        customer_id: &StripeCustomerId,
-    ) -> Result<Vec<StripeSubscription>> {
-        let customer_id = customer_id.try_into()?;
-
-        let subscriptions = stripe::Subscription::list(
-            &self.client,
-            &stripe::ListSubscriptions {
-                customer: Some(customer_id),
-                status: None,
-                ..Default::default()
-            },
-        )
-        .await?;
-
-        Ok(subscriptions
-            .data
-            .into_iter()
-            .map(StripeSubscription::from)
-            .collect())
-    }
-
-    async fn get_subscription(
-        &self,
-        subscription_id: &StripeSubscriptionId,
-    ) -> Result<StripeSubscription> {
-        let subscription_id = subscription_id.try_into()?;
-
-        let subscription = Subscription::retrieve(&self.client, &subscription_id, &[]).await?;
-
-        Ok(StripeSubscription::from(subscription))
-    }
-
-    async fn create_subscription(
-        &self,
-        params: StripeCreateSubscriptionParams,
-    ) -> Result<StripeSubscription> {
-        let customer_id = params.customer.try_into()?;
-
-        let mut create_subscription = stripe::CreateSubscription::new(customer_id);
-        create_subscription.items = Some(
-            params
-                .items
-                .into_iter()
-                .map(|item| stripe::CreateSubscriptionItems {
-                    price: item.price.map(|price| price.to_string()),
-                    quantity: item.quantity,
-                    ..Default::default()
-                })
-                .collect(),
-        );
-        create_subscription.automatic_tax = params.automatic_tax.map(Into::into);
-
-        let subscription = Subscription::create(&self.client, create_subscription).await?;
-
-        Ok(StripeSubscription::from(subscription))
-    }
-
-    async fn update_subscription(
-        &self,
-        subscription_id: &StripeSubscriptionId,
-        params: UpdateSubscriptionParams,
-    ) -> Result<()> {
-        let subscription_id = subscription_id.try_into()?;
-
-        stripe::Subscription::update(
-            &self.client,
-            &subscription_id,
-            stripe::UpdateSubscription {
-                items: params.items.map(|items| {
-                    items
-                        .into_iter()
-                        .map(|item| UpdateSubscriptionItems {
-                            price: item.price.map(|price| price.to_string()),
-                            ..Default::default()
-                        })
-                        .collect()
-                }),
-                trial_settings: params.trial_settings.map(Into::into),
-                ..Default::default()
-            },
-        )
-        .await?;
-
-        Ok(())
-    }
-
-    async fn cancel_subscription(&self, subscription_id: &StripeSubscriptionId) -> Result<()> {
-        let subscription_id = subscription_id.try_into()?;
-
-        Subscription::cancel(
-            &self.client,
-            &subscription_id,
-            stripe::CancelSubscription {
-                invoice_now: None,
-                ..Default::default()
-            },
-        )
-        .await?;
-
-        Ok(())
-    }
-
-    async fn list_prices(&self) -> Result<Vec<StripePrice>> {
-        let response = stripe::Price::list(
-            &self.client,
-            &stripe::ListPrices {
-                limit: Some(100),
-                ..Default::default()
-            },
-        )
-        .await?;
-
-        Ok(response.data.into_iter().map(StripePrice::from).collect())
-    }
-
-    async fn list_meters(&self) -> Result<Vec<StripeMeter>> {
-        #[derive(Serialize)]
-        struct Params {
-            #[serde(skip_serializing_if = "Option::is_none")]
-            limit: Option<u64>,
-        }
-
-        let response = self
-            .client
-            .get_query::<stripe::List<StripeMeter>, _>(
-                "/billing/meters",
-                Params { limit: Some(100) },
-            )
-            .await?;
-
-        Ok(response.data)
-    }
-
-    async fn create_meter_event(&self, params: StripeCreateMeterEventParams<'_>) -> Result<()> {
-        #[derive(Deserialize)]
-        struct StripeMeterEvent {
-            pub identifier: String,
-        }
-
-        let identifier = params.identifier;
-        match self
-            .client
-            .post_form::<StripeMeterEvent, _>("/billing/meter_events", params)
-            .await
-        {
-            Ok(_event) => Ok(()),
-            Err(stripe::StripeError::Stripe(error)) => {
-                if error.http_status == 400
-                    && error
-                        .message
-                        .as_ref()
-                        .map_or(false, |message| message.contains(identifier))
-                {
-                    Ok(())
-                } else {
-                    Err(anyhow!(stripe::StripeError::Stripe(error)))
-                }
-            }
-            Err(error) => Err(anyhow!("failed to create meter event: {error:?}")),
-        }
-    }
-
-    async fn create_checkout_session(
-        &self,
-        params: StripeCreateCheckoutSessionParams<'_>,
-    ) -> Result<StripeCheckoutSession> {
-        let params = params.try_into()?;
-        let session = CheckoutSession::create(&self.client, params).await?;
-
-        Ok(session.into())
-    }
-}
-
-impl From<CustomerId> for StripeCustomerId {
-    fn from(value: CustomerId) -> Self {
-        Self(value.as_str().into())
-    }
-}
-
-impl TryFrom<StripeCustomerId> for CustomerId {
-    type Error = anyhow::Error;
-
-    fn try_from(value: StripeCustomerId) -> Result<Self, Self::Error> {
-        Self::from_str(value.0.as_ref()).context("failed to parse Stripe customer ID")
-    }
-}
-
-impl TryFrom<&StripeCustomerId> for CustomerId {
-    type Error = anyhow::Error;
-
-    fn try_from(value: &StripeCustomerId) -> Result<Self, Self::Error> {
-        Self::from_str(value.0.as_ref()).context("failed to parse Stripe customer ID")
-    }
-}
-
-impl From<Customer> for StripeCustomer {
-    fn from(value: Customer) -> Self {
-        StripeCustomer {
-            id: value.id.into(),
-            email: value.email,
-        }
-    }
-}
-
-impl From<SubscriptionId> for StripeSubscriptionId {
-    fn from(value: SubscriptionId) -> Self {
-        Self(value.as_str().into())
-    }
-}
-
-impl TryFrom<&StripeSubscriptionId> for SubscriptionId {
-    type Error = anyhow::Error;
-
-    fn try_from(value: &StripeSubscriptionId) -> Result<Self, Self::Error> {
-        Self::from_str(value.0.as_ref()).context("failed to parse Stripe subscription ID")
-    }
-}
-
-impl From<Subscription> for StripeSubscription {
-    fn from(value: Subscription) -> Self {
-        Self {
-            id: value.id.into(),
-            customer: value.customer.id().into(),
-            status: value.status,
-            current_period_start: value.current_period_start,
-            current_period_end: value.current_period_end,
-            items: value.items.data.into_iter().map(Into::into).collect(),
-            cancel_at: value.cancel_at,
-            cancellation_details: value.cancellation_details.map(Into::into),
-        }
-    }
-}
-
-impl From<CancellationDetails> for StripeCancellationDetails {
-    fn from(value: CancellationDetails) -> Self {
-        Self {
-            reason: value.reason.map(Into::into),
-        }
-    }
-}
-
-impl From<CancellationDetailsReason> for StripeCancellationDetailsReason {
-    fn from(value: CancellationDetailsReason) -> Self {
-        match value {
-            CancellationDetailsReason::CancellationRequested => Self::CancellationRequested,
-            CancellationDetailsReason::PaymentDisputed => Self::PaymentDisputed,
-            CancellationDetailsReason::PaymentFailed => Self::PaymentFailed,
-        }
-    }
-}
-
-impl From<SubscriptionItemId> for StripeSubscriptionItemId {
-    fn from(value: SubscriptionItemId) -> Self {
-        Self(value.as_str().into())
-    }
-}
-
-impl From<SubscriptionItem> for StripeSubscriptionItem {
-    fn from(value: SubscriptionItem) -> Self {
-        Self {
-            id: value.id.into(),
-            price: value.price.map(Into::into),
-        }
-    }
-}
-
-impl From<StripeAutomaticTax> for CreateSubscriptionAutomaticTax {
-    fn from(value: StripeAutomaticTax) -> Self {
-        Self {
-            enabled: value.enabled,
-            liability: None,
-        }
-    }
-}
-
-impl From<StripeSubscriptionTrialSettings> for UpdateSubscriptionTrialSettings {
-    fn from(value: StripeSubscriptionTrialSettings) -> Self {
-        Self {
-            end_behavior: value.end_behavior.into(),
-        }
-    }
-}
-
-impl From<StripeSubscriptionTrialSettingsEndBehavior>
-    for UpdateSubscriptionTrialSettingsEndBehavior
-{
-    fn from(value: StripeSubscriptionTrialSettingsEndBehavior) -> Self {
-        Self {
-            missing_payment_method: value.missing_payment_method.into(),
-        }
-    }
-}
-
-impl From<StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod>
-    for UpdateSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod
-{
-    fn from(value: StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod) -> Self {
-        match value {
-            StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod::Cancel => Self::Cancel,
-            StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod::CreateInvoice => {
-                Self::CreateInvoice
-            }
-            StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod::Pause => Self::Pause,
-        }
-    }
-}
-
-impl From<PriceId> for StripePriceId {
-    fn from(value: PriceId) -> Self {
-        Self(value.as_str().into())
-    }
-}
-
-impl TryFrom<StripePriceId> for PriceId {
-    type Error = anyhow::Error;
-
-    fn try_from(value: StripePriceId) -> Result<Self, Self::Error> {
-        Self::from_str(value.0.as_ref()).context("failed to parse Stripe price ID")
-    }
-}
-
-impl From<Price> for StripePrice {
-    fn from(value: Price) -> Self {
-        Self {
-            id: value.id.into(),
-            unit_amount: value.unit_amount,
-            lookup_key: value.lookup_key,
-            recurring: value.recurring.map(StripePriceRecurring::from),
-        }
-    }
-}
-
-impl From<Recurring> for StripePriceRecurring {
-    fn from(value: Recurring) -> Self {
-        Self { meter: value.meter }
-    }
-}
-
-impl<'a> TryFrom<StripeCreateCheckoutSessionParams<'a>> for CreateCheckoutSession<'a> {
-    type Error = anyhow::Error;
-
-    fn try_from(value: StripeCreateCheckoutSessionParams<'a>) -> Result<Self, Self::Error> {
-        Ok(Self {
-            customer: value
-                .customer
-                .map(|customer_id| customer_id.try_into())
-                .transpose()?,
-            client_reference_id: value.client_reference_id,
-            mode: value.mode.map(Into::into),
-            line_items: value
-                .line_items
-                .map(|line_items| line_items.into_iter().map(Into::into).collect()),
-            payment_method_collection: value.payment_method_collection.map(Into::into),
-            subscription_data: value.subscription_data.map(Into::into),
-            success_url: value.success_url,
-            billing_address_collection: value.billing_address_collection.map(Into::into),
-            customer_update: value.customer_update.map(Into::into),
-            tax_id_collection: value.tax_id_collection.map(Into::into),
-            ..Default::default()
-        })
-    }
-}
-
-impl From<StripeCheckoutSessionMode> for CheckoutSessionMode {
-    fn from(value: StripeCheckoutSessionMode) -> Self {
-        match value {
-            StripeCheckoutSessionMode::Payment => Self::Payment,
-            StripeCheckoutSessionMode::Setup => Self::Setup,
-            StripeCheckoutSessionMode::Subscription => Self::Subscription,
-        }
-    }
-}
-
-impl From<StripeCreateCheckoutSessionLineItems> for CreateCheckoutSessionLineItems {
-    fn from(value: StripeCreateCheckoutSessionLineItems) -> Self {
-        Self {
-            price: value.price,
-            quantity: value.quantity,
-            ..Default::default()
-        }
-    }
-}
-
-impl From<StripeCheckoutSessionPaymentMethodCollection> for CheckoutSessionPaymentMethodCollection {
-    fn from(value: StripeCheckoutSessionPaymentMethodCollection) -> Self {
-        match value {
-            StripeCheckoutSessionPaymentMethodCollection::Always => Self::Always,
-            StripeCheckoutSessionPaymentMethodCollection::IfRequired => Self::IfRequired,
-        }
-    }
-}
-
-impl From<StripeCreateCheckoutSessionSubscriptionData> for CreateCheckoutSessionSubscriptionData {
-    fn from(value: StripeCreateCheckoutSessionSubscriptionData) -> Self {
-        Self {
-            trial_period_days: value.trial_period_days,
-            trial_settings: value.trial_settings.map(Into::into),
-            metadata: value.metadata,
-            ..Default::default()
-        }
-    }
-}
-
-impl From<StripeSubscriptionTrialSettings> for CreateCheckoutSessionSubscriptionDataTrialSettings {
-    fn from(value: StripeSubscriptionTrialSettings) -> Self {
-        Self {
-            end_behavior: value.end_behavior.into(),
-        }
-    }
-}
-
-impl From<StripeSubscriptionTrialSettingsEndBehavior>
-    for CreateCheckoutSessionSubscriptionDataTrialSettingsEndBehavior
-{
-    fn from(value: StripeSubscriptionTrialSettingsEndBehavior) -> Self {
-        Self {
-            missing_payment_method: value.missing_payment_method.into(),
-        }
-    }
-}
-
-impl From<StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod>
-    for CreateCheckoutSessionSubscriptionDataTrialSettingsEndBehaviorMissingPaymentMethod
-{
-    fn from(value: StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod) -> Self {
-        match value {
-            StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod::Cancel => Self::Cancel,
-            StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod::CreateInvoice => {
-                Self::CreateInvoice
-            }
-            StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod::Pause => Self::Pause,
-        }
-    }
-}
-
-impl From<CheckoutSession> for StripeCheckoutSession {
-    fn from(value: CheckoutSession) -> Self {
-        Self { url: value.url }
-    }
-}
-
-impl From<StripeBillingAddressCollection> for stripe::CheckoutSessionBillingAddressCollection {
-    fn from(value: StripeBillingAddressCollection) -> Self {
-        match value {
-            StripeBillingAddressCollection::Auto => {
-                stripe::CheckoutSessionBillingAddressCollection::Auto
-            }
-            StripeBillingAddressCollection::Required => {
-                stripe::CheckoutSessionBillingAddressCollection::Required
-            }
-        }
-    }
-}
-
-impl From<StripeCustomerUpdateAddress> for stripe::CreateCheckoutSessionCustomerUpdateAddress {
-    fn from(value: StripeCustomerUpdateAddress) -> Self {
-        match value {
-            StripeCustomerUpdateAddress::Auto => {
-                stripe::CreateCheckoutSessionCustomerUpdateAddress::Auto
-            }
-            StripeCustomerUpdateAddress::Never => {
-                stripe::CreateCheckoutSessionCustomerUpdateAddress::Never
-            }
-        }
-    }
-}
-
-impl From<StripeCustomerUpdateName> for stripe::CreateCheckoutSessionCustomerUpdateName {
-    fn from(value: StripeCustomerUpdateName) -> Self {
-        match value {
-            StripeCustomerUpdateName::Auto => stripe::CreateCheckoutSessionCustomerUpdateName::Auto,
-            StripeCustomerUpdateName::Never => {
-                stripe::CreateCheckoutSessionCustomerUpdateName::Never
-            }
-        }
-    }
-}
-
-impl From<StripeCustomerUpdateShipping> for stripe::CreateCheckoutSessionCustomerUpdateShipping {
-    fn from(value: StripeCustomerUpdateShipping) -> Self {
-        match value {
-            StripeCustomerUpdateShipping::Auto => {
-                stripe::CreateCheckoutSessionCustomerUpdateShipping::Auto
-            }
-            StripeCustomerUpdateShipping::Never => {
-                stripe::CreateCheckoutSessionCustomerUpdateShipping::Never
-            }
-        }
-    }
-}
-
-impl From<StripeCustomerUpdate> for stripe::CreateCheckoutSessionCustomerUpdate {
-    fn from(value: StripeCustomerUpdate) -> Self {
-        stripe::CreateCheckoutSessionCustomerUpdate {
-            address: value.address.map(Into::into),
-            name: value.name.map(Into::into),
-            shipping: value.shipping.map(Into::into),
-        }
-    }
-}
-
-impl From<StripeTaxIdCollection> for stripe::CreateCheckoutSessionTaxIdCollection {
-    fn from(value: StripeTaxIdCollection) -> Self {
-        stripe::CreateCheckoutSessionTaxIdCollection {
-            enabled: value.enabled,
-        }
-    }
-}

crates/collab/src/tests.rs 🔗

@@ -8,7 +8,6 @@ mod channel_buffer_tests;
 mod channel_guest_tests;
 mod channel_message_tests;
 mod channel_tests;
-// mod debug_panel_tests;
 mod editor_tests;
 mod following_tests;
 mod git_tests;
@@ -18,7 +17,6 @@ mod random_channel_buffer_tests;
 mod random_project_collaboration_tests;
 mod randomized_test_helpers;
 mod remote_editing_collaboration_tests;
-mod stripe_billing_tests;
 mod test_server;
 
 use language::{Language, LanguageConfig, LanguageMatcher, tree_sitter_rust};

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

@@ -15,13 +15,14 @@ use editor::{
     },
 };
 use fs::Fs;
-use futures::{StreamExt, lock::Mutex};
+use futures::{SinkExt, StreamExt, channel::mpsc, lock::Mutex};
 use gpui::{App, Rgba, TestAppContext, UpdateGlobal, VisualContext, VisualTestContext};
 use indoc::indoc;
 use language::{
     FakeLspAdapter,
     language_settings::{AllLanguageSettings, InlayHintSettings},
 };
+use lsp::LSP_REQUEST_TIMEOUT;
 use project::{
     ProjectPath, SERVER_PROGRESS_THROTTLE_TIMEOUT,
     lsp_store::lsp_ext_command::{ExpandedMacro, LspExtExpandMacro},
@@ -1017,6 +1018,211 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T
     })
 }
 
+#[gpui::test]
+async fn test_slow_lsp_server(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
+    let mut server = TestServer::start(cx_a.executor()).await;
+    let client_a = server.create_client(cx_a, "user_a").await;
+    let client_b = server.create_client(cx_b, "user_b").await;
+    server
+        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
+        .await;
+    let active_call_a = cx_a.read(ActiveCall::global);
+    cx_b.update(editor::init);
+
+    let command_name = "test_command";
+    let capabilities = lsp::ServerCapabilities {
+        code_lens_provider: Some(lsp::CodeLensOptions {
+            resolve_provider: None,
+        }),
+        execute_command_provider: Some(lsp::ExecuteCommandOptions {
+            commands: vec![command_name.to_string()],
+            ..lsp::ExecuteCommandOptions::default()
+        }),
+        ..lsp::ServerCapabilities::default()
+    };
+    client_a.language_registry().add(rust_lang());
+    let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
+        "Rust",
+        FakeLspAdapter {
+            capabilities: capabilities.clone(),
+            ..FakeLspAdapter::default()
+        },
+    );
+    client_b.language_registry().add(rust_lang());
+    client_b.language_registry().register_fake_lsp_adapter(
+        "Rust",
+        FakeLspAdapter {
+            capabilities,
+            ..FakeLspAdapter::default()
+        },
+    );
+
+    client_a
+        .fs()
+        .insert_tree(
+            path!("/dir"),
+            json!({
+                "one.rs": "const ONE: usize = 1;"
+            }),
+        )
+        .await;
+    let (project_a, worktree_id) = client_a.build_local_project(path!("/dir"), cx_a).await;
+    let project_id = active_call_a
+        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
+        .await
+        .unwrap();
+    let project_b = client_b.join_remote_project(project_id, cx_b).await;
+
+    let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
+    let editor_b = workspace_b
+        .update_in(cx_b, |workspace, window, cx| {
+            workspace.open_path((worktree_id, "one.rs"), None, true, window, cx)
+        })
+        .await
+        .unwrap()
+        .downcast::<Editor>()
+        .unwrap();
+    let (lsp_store_b, buffer_b) = editor_b.update(cx_b, |editor, cx| {
+        let lsp_store = editor.project().unwrap().read(cx).lsp_store();
+        let buffer = editor.buffer().read(cx).as_singleton().unwrap();
+        (lsp_store, buffer)
+    });
+    let fake_language_server = fake_language_servers.next().await.unwrap();
+    cx_a.run_until_parked();
+    cx_b.run_until_parked();
+
+    let long_request_time = LSP_REQUEST_TIMEOUT / 2;
+    let (request_started_tx, mut request_started_rx) = mpsc::unbounded();
+    let requests_started = Arc::new(AtomicUsize::new(0));
+    let requests_completed = Arc::new(AtomicUsize::new(0));
+    let _lens_requests = fake_language_server
+        .set_request_handler::<lsp::request::CodeLensRequest, _, _>({
+            let request_started_tx = request_started_tx.clone();
+            let requests_started = requests_started.clone();
+            let requests_completed = requests_completed.clone();
+            move |params, cx| {
+                let mut request_started_tx = request_started_tx.clone();
+                let requests_started = requests_started.clone();
+                let requests_completed = requests_completed.clone();
+                async move {
+                    assert_eq!(
+                        params.text_document.uri.as_str(),
+                        uri!("file:///dir/one.rs")
+                    );
+                    requests_started.fetch_add(1, atomic::Ordering::Release);
+                    request_started_tx.send(()).await.unwrap();
+                    cx.background_executor().timer(long_request_time).await;
+                    let i = requests_completed.fetch_add(1, atomic::Ordering::Release) + 1;
+                    Ok(Some(vec![lsp::CodeLens {
+                        range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 9)),
+                        command: Some(lsp::Command {
+                            title: format!("LSP Command {i}"),
+                            command: command_name.to_string(),
+                            arguments: None,
+                        }),
+                        data: None,
+                    }]))
+                }
+            }
+        });
+
+    // Move cursor to a location, this should trigger the code lens call.
+    editor_b.update_in(cx_b, |editor, window, cx| {
+        editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
+            s.select_ranges([7..7])
+        });
+    });
+    let () = request_started_rx.next().await.unwrap();
+    assert_eq!(
+        requests_started.load(atomic::Ordering::Acquire),
+        1,
+        "Selection change should have initiated the first request"
+    );
+    assert_eq!(
+        requests_completed.load(atomic::Ordering::Acquire),
+        0,
+        "Slow requests should be running still"
+    );
+    let _first_task = lsp_store_b.update(cx_b, |lsp_store, cx| {
+        lsp_store
+            .forget_code_lens_task(buffer_b.read(cx).remote_id())
+            .expect("Should have the fetch task started")
+    });
+
+    editor_b.update_in(cx_b, |editor, window, cx| {
+        editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
+            s.select_ranges([1..1])
+        });
+    });
+    let () = request_started_rx.next().await.unwrap();
+    assert_eq!(
+        requests_started.load(atomic::Ordering::Acquire),
+        2,
+        "Selection change should have initiated the second request"
+    );
+    assert_eq!(
+        requests_completed.load(atomic::Ordering::Acquire),
+        0,
+        "Slow requests should be running still"
+    );
+    let _second_task = lsp_store_b.update(cx_b, |lsp_store, cx| {
+        lsp_store
+            .forget_code_lens_task(buffer_b.read(cx).remote_id())
+            .expect("Should have the fetch task started for the 2nd time")
+    });
+
+    editor_b.update_in(cx_b, |editor, window, cx| {
+        editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
+            s.select_ranges([2..2])
+        });
+    });
+    let () = request_started_rx.next().await.unwrap();
+    assert_eq!(
+        requests_started.load(atomic::Ordering::Acquire),
+        3,
+        "Selection change should have initiated the third request"
+    );
+    assert_eq!(
+        requests_completed.load(atomic::Ordering::Acquire),
+        0,
+        "Slow requests should be running still"
+    );
+
+    _first_task.await.unwrap();
+    _second_task.await.unwrap();
+    cx_b.run_until_parked();
+    assert_eq!(
+        requests_started.load(atomic::Ordering::Acquire),
+        3,
+        "No selection changes should trigger no more code lens requests"
+    );
+    assert_eq!(
+        requests_completed.load(atomic::Ordering::Acquire),
+        3,
+        "After enough time, all 3 LSP requests should have been served by the language server"
+    );
+    let resulting_lens_actions = editor_b
+        .update(cx_b, |editor, cx| {
+            let lsp_store = editor.project().unwrap().read(cx).lsp_store();
+            lsp_store.update(cx, |lsp_store, cx| {
+                lsp_store.code_lens_actions(&buffer_b, cx)
+            })
+        })
+        .await
+        .unwrap()
+        .unwrap();
+    assert_eq!(
+        resulting_lens_actions.len(),
+        1,
+        "Should have fetched one code lens action, but got: {resulting_lens_actions:?}"
+    );
+    assert_eq!(
+        resulting_lens_actions.first().unwrap().lsp_action.title(),
+        "LSP Command 3",
+        "Only the final code lens action should be in the data"
+    )
+}
+
 #[gpui::test(iterations = 10)]
 async fn test_language_server_statuses(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
     let mut server = TestServer::start(cx_a.executor()).await;
@@ -2908,7 +3114,7 @@ async fn test_lsp_pull_diagnostics(
 
     {
         assert!(
-            diagnostics_pulls_result_ids.lock().await.len() > 0,
+            !diagnostics_pulls_result_ids.lock().await.is_empty(),
             "Initial diagnostics pulls should report None at least"
         );
         assert_eq!(
@@ -3593,7 +3799,7 @@ async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte
     let abs_path = project_a.read_with(cx_a, |project, cx| {
         project
             .absolute_path(&project_path, cx)
-            .map(|path_buf| Arc::from(path_buf.to_owned()))
+            .map(Arc::from)
             .unwrap()
     });
 
@@ -3647,20 +3853,16 @@ async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte
     let breakpoints_a = editor_a.update(cx_a, |editor, cx| {
         editor
             .breakpoint_store()
-            .clone()
             .unwrap()
             .read(cx)
             .all_source_breakpoints(cx)
-            .clone()
     });
     let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
         editor
             .breakpoint_store()
-            .clone()
             .unwrap()
             .read(cx)
             .all_source_breakpoints(cx)
-            .clone()
     });
 
     assert_eq!(1, breakpoints_a.len());
@@ -3680,20 +3882,16 @@ async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte
     let breakpoints_a = editor_a.update(cx_a, |editor, cx| {
         editor
             .breakpoint_store()
-            .clone()
             .unwrap()
             .read(cx)
             .all_source_breakpoints(cx)
-            .clone()
     });
     let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
         editor
             .breakpoint_store()
-            .clone()
             .unwrap()
             .read(cx)
             .all_source_breakpoints(cx)
-            .clone()
     });
 
     assert_eq!(1, breakpoints_a.len());
@@ -3713,20 +3911,16 @@ async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte
     let breakpoints_a = editor_a.update(cx_a, |editor, cx| {
         editor
             .breakpoint_store()
-            .clone()
             .unwrap()
             .read(cx)
             .all_source_breakpoints(cx)
-            .clone()
     });
     let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
         editor
             .breakpoint_store()
-            .clone()
             .unwrap()
             .read(cx)
             .all_source_breakpoints(cx)
-            .clone()
     });
 
     assert_eq!(1, breakpoints_a.len());
@@ -3746,20 +3940,16 @@ async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte
     let breakpoints_a = editor_a.update(cx_a, |editor, cx| {
         editor
             .breakpoint_store()
-            .clone()
             .unwrap()
             .read(cx)
             .all_source_breakpoints(cx)
-            .clone()
     });
     let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
         editor
             .breakpoint_store()
-            .clone()
             .unwrap()
             .read(cx)
             .all_source_breakpoints(cx)
-            .clone()
     });
 
     assert_eq!(0, breakpoints_a.len());

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

@@ -3208,7 +3208,7 @@ async fn test_fs_operations(
         })
         .await
         .unwrap()
-        .to_included()
+        .into_included()
         .unwrap();
 
     worktree_a.read_with(cx_a, |worktree, _| {
@@ -3237,7 +3237,7 @@ async fn test_fs_operations(
         })
         .await
         .unwrap()
-        .to_included()
+        .into_included()
         .unwrap();
 
     worktree_a.read_with(cx_a, |worktree, _| {
@@ -3266,7 +3266,7 @@ async fn test_fs_operations(
         })
         .await
         .unwrap()
-        .to_included()
+        .into_included()
         .unwrap();
 
     worktree_a.read_with(cx_a, |worktree, _| {
@@ -3295,7 +3295,7 @@ async fn test_fs_operations(
         })
         .await
         .unwrap()
-        .to_included()
+        .into_included()
         .unwrap();
 
     project_b
@@ -3304,7 +3304,7 @@ async fn test_fs_operations(
         })
         .await
         .unwrap()
-        .to_included()
+        .into_included()
         .unwrap();
 
     project_b
@@ -3313,7 +3313,7 @@ async fn test_fs_operations(
         })
         .await
         .unwrap()
-        .to_included()
+        .into_included()
         .unwrap();
 
     worktree_a.read_with(cx_a, |worktree, _| {
@@ -4850,6 +4850,7 @@ async fn test_definition(
     let definitions_1 = project_b
         .update(cx_b, |p, cx| p.definitions(&buffer_b, 23, cx))
         .await
+        .unwrap()
         .unwrap();
     cx_b.read(|cx| {
         assert_eq!(
@@ -4885,6 +4886,7 @@ async fn test_definition(
     let definitions_2 = project_b
         .update(cx_b, |p, cx| p.definitions(&buffer_b, 33, cx))
         .await
+        .unwrap()
         .unwrap();
     cx_b.read(|cx| {
         assert_eq!(definitions_2.len(), 1);
@@ -4922,6 +4924,7 @@ async fn test_definition(
     let type_definitions = project_b
         .update(cx_b, |p, cx| p.type_definitions(&buffer_b, 7, cx))
         .await
+        .unwrap()
         .unwrap();
     cx_b.read(|cx| {
         assert_eq!(
@@ -4970,7 +4973,7 @@ async fn test_references(
         "Rust",
         FakeLspAdapter {
             name: "my-fake-lsp-adapter",
-            capabilities: capabilities,
+            capabilities,
             ..FakeLspAdapter::default()
         },
     );
@@ -5060,7 +5063,7 @@ async fn test_references(
         ])))
         .unwrap();
 
-    let references = references.await.unwrap();
+    let references = references.await.unwrap().unwrap();
     executor.run_until_parked();
     project_b.read_with(cx_b, |project, cx| {
         // User is informed that a request is no longer pending.
@@ -5104,7 +5107,7 @@ async fn test_references(
     lsp_response_tx
         .unbounded_send(Err(anyhow!("can't find references")))
         .unwrap();
-    assert_eq!(references.await.unwrap(), []);
+    assert_eq!(references.await.unwrap().unwrap(), []);
 
     // User is informed that the request is no longer pending.
     executor.run_until_parked();
@@ -5505,7 +5508,8 @@ async fn test_lsp_hover(
     // Request hover information as the guest.
     let mut hovers = project_b
         .update(cx_b, |p, cx| p.hover(&buffer_b, 22, cx))
-        .await;
+        .await
+        .unwrap();
     assert_eq!(
         hovers.len(),
         2,
@@ -5764,7 +5768,7 @@ async fn test_open_buffer_while_getting_definition_pointing_to_it(
         definitions = project_b.update(cx_b, |p, cx| p.definitions(&buffer_b1, 23, cx));
     }
 
-    let definitions = definitions.await.unwrap();
+    let definitions = definitions.await.unwrap().unwrap();
     assert_eq!(
         definitions.len(),
         1,

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

@@ -266,7 +266,7 @@ impl RandomizedTest for RandomChannelBufferTest {
                                 "client {user_id} has different text than client {prev_user_id} for channel {channel_name}",
                             );
                         } else {
-                            prev_text = Some((user_id, text.clone()));
+                            prev_text = Some((user_id, text));
                         }
 
                         // Assert that all clients and the server agree about who is present in the

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

@@ -304,7 +304,7 @@ impl RandomizedTest for ProjectCollaborationTest {
                                         let worktree = worktree.read(cx);
                                         worktree.is_visible()
                                             && worktree.entries(false, 0).any(|e| e.is_file())
-                                            && worktree.root_entry().map_or(false, |e| e.is_dir())
+                                            && worktree.root_entry().is_some_and(|e| e.is_dir())
                                     })
                                     .choose(rng)
                             });
@@ -643,7 +643,7 @@ impl RandomizedTest for ProjectCollaborationTest {
                 );
 
                 let project = project.await?;
-                client.dev_server_projects_mut().push(project.clone());
+                client.dev_server_projects_mut().push(project);
             }
 
             ClientOperation::CreateWorktreeEntry {
@@ -1162,8 +1162,8 @@ impl RandomizedTest for ProjectCollaborationTest {
                             Some((project, cx))
                         });
 
-                        if !guest_project.is_disconnected(cx) {
-                            if let Some((host_project, host_cx)) = host_project {
+                        if !guest_project.is_disconnected(cx)
+                            && let Some((host_project, host_cx)) = host_project {
                                 let host_worktree_snapshots =
                                     host_project.read_with(host_cx, |host_project, cx| {
                                         host_project
@@ -1235,7 +1235,6 @@ impl RandomizedTest for ProjectCollaborationTest {
                                     );
                                 }
                             }
-                        }
 
                         for buffer in guest_project.opened_buffers(cx) {
                             let buffer = buffer.read(cx);

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

@@ -198,11 +198,11 @@ pub async fn run_randomized_test<T: RandomizedTest>(
 }
 
 pub fn save_randomized_test_plan() {
-    if let Some(serialize_plan) = LAST_PLAN.lock().take() {
-        if let Some(path) = plan_save_path() {
-            eprintln!("saved test plan to path {:?}", path);
-            std::fs::write(path, serialize_plan()).unwrap();
-        }
+    if let Some(serialize_plan) = LAST_PLAN.lock().take()
+        && let Some(path) = plan_save_path()
+    {
+        eprintln!("saved test plan to path {:?}", path);
+        std::fs::write(path, serialize_plan()).unwrap();
     }
 }
 
@@ -290,10 +290,9 @@ impl<T: RandomizedTest> TestPlan<T> {
                         if let StoredOperation::Client {
                             user_id, batch_id, ..
                         } = operation
+                            && batch_id == current_batch_id
                         {
-                            if batch_id == current_batch_id {
-                                return Some(user_id);
-                            }
+                            return Some(user_id);
                         }
                         None
                     }));
@@ -366,10 +365,9 @@ impl<T: RandomizedTest> TestPlan<T> {
                     },
                     applied,
                 ) = stored_operation
+                    && user_id == &current_user_id
                 {
-                    if user_id == &current_user_id {
-                        return Some((operation.clone(), applied.clone()));
-                    }
+                    return Some((operation.clone(), applied.clone()));
                 }
             }
             None
@@ -550,11 +548,11 @@ impl<T: RandomizedTest> TestPlan<T> {
                         .unwrap();
                     let pool = server.connection_pool.lock();
                     for contact in contacts {
-                        if let db::Contact::Accepted { user_id, busy, .. } = contact {
-                            if user_id == removed_user_id {
-                                assert!(!pool.is_user_online(user_id));
-                                assert!(!busy);
-                            }
+                        if let db::Contact::Accepted { user_id, busy, .. } = contact
+                            && user_id == removed_user_id
+                        {
+                            assert!(!pool.is_user_online(user_id));
+                            assert!(!busy);
                         }
                     }
                 }

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

@@ -1,123 +0,0 @@
-use std::sync::Arc;
-
-use pretty_assertions::assert_eq;
-
-use crate::stripe_billing::StripeBilling;
-use crate::stripe_client::{FakeStripeClient, StripePrice, StripePriceId, StripePriceRecurring};
-
-fn make_stripe_billing() -> (StripeBilling, Arc<FakeStripeClient>) {
-    let stripe_client = Arc::new(FakeStripeClient::new());
-    let stripe_billing = StripeBilling::test(stripe_client.clone());
-
-    (stripe_billing, stripe_client)
-}
-
-#[gpui::test]
-async fn test_initialize() {
-    let (stripe_billing, stripe_client) = make_stripe_billing();
-
-    // Add test prices
-    let price1 = StripePrice {
-        id: StripePriceId("price_1".into()),
-        unit_amount: Some(1_000),
-        lookup_key: Some("zed-pro".to_string()),
-        recurring: None,
-    };
-    let price2 = StripePrice {
-        id: StripePriceId("price_2".into()),
-        unit_amount: Some(0),
-        lookup_key: Some("zed-free".to_string()),
-        recurring: None,
-    };
-    let price3 = StripePrice {
-        id: StripePriceId("price_3".into()),
-        unit_amount: Some(500),
-        lookup_key: None,
-        recurring: Some(StripePriceRecurring {
-            meter: Some("meter_1".to_string()),
-        }),
-    };
-    stripe_client
-        .prices
-        .lock()
-        .insert(price1.id.clone(), price1);
-    stripe_client
-        .prices
-        .lock()
-        .insert(price2.id.clone(), price2);
-    stripe_client
-        .prices
-        .lock()
-        .insert(price3.id.clone(), price3);
-
-    // Initialize the billing system
-    stripe_billing.initialize().await.unwrap();
-
-    // Verify that prices can be found by lookup key
-    let zed_pro_price_id = stripe_billing.zed_pro_price_id().await.unwrap();
-    assert_eq!(zed_pro_price_id.to_string(), "price_1");
-
-    let zed_free_price_id = stripe_billing.zed_free_price_id().await.unwrap();
-    assert_eq!(zed_free_price_id.to_string(), "price_2");
-
-    // Verify that a price can be found by lookup key
-    let zed_pro_price = stripe_billing
-        .find_price_by_lookup_key("zed-pro")
-        .await
-        .unwrap();
-    assert_eq!(zed_pro_price.id.to_string(), "price_1");
-    assert_eq!(zed_pro_price.unit_amount, Some(1_000));
-
-    // Verify that finding a non-existent lookup key returns an error
-    let result = stripe_billing
-        .find_price_by_lookup_key("non-existent")
-        .await;
-    assert!(result.is_err());
-}
-
-#[gpui::test]
-async fn test_find_or_create_customer_by_email() {
-    let (stripe_billing, stripe_client) = make_stripe_billing();
-
-    // Create a customer with an email that doesn't yet correspond to a customer.
-    {
-        let email = "user@example.com";
-
-        let customer_id = stripe_billing
-            .find_or_create_customer_by_email(Some(email))
-            .await
-            .unwrap();
-
-        let customer = stripe_client
-            .customers
-            .lock()
-            .get(&customer_id)
-            .unwrap()
-            .clone();
-        assert_eq!(customer.email.as_deref(), Some(email));
-    }
-
-    // Create a customer with an email that corresponds to an existing customer.
-    {
-        let email = "user2@example.com";
-
-        let existing_customer_id = stripe_billing
-            .find_or_create_customer_by_email(Some(email))
-            .await
-            .unwrap();
-
-        let customer_id = stripe_billing
-            .find_or_create_customer_by_email(Some(email))
-            .await
-            .unwrap();
-        assert_eq!(customer_id, existing_customer_id);
-
-        let customer = stripe_client
-            .customers
-            .lock()
-            .get(&customer_id)
-            .unwrap()
-            .clone();
-        assert_eq!(customer.email.as_deref(), Some(email));
-    }
-}

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

@@ -1,4 +1,3 @@
-use crate::stripe_client::FakeStripeClient;
 use crate::{
     AppState, Config,
     db::{NewUserParams, UserId, tests::TestDb},
@@ -371,8 +370,8 @@ impl TestServer {
         let client = TestClient {
             app_state,
             username: name.to_string(),
-            channel_store: cx.read(ChannelStore::global).clone(),
-            notification_store: cx.read(NotificationStore::global).clone(),
+            channel_store: cx.read(ChannelStore::global),
+            notification_store: cx.read(NotificationStore::global),
             state: Default::default(),
         };
         client.wait_for_current_user(cx).await;
@@ -566,12 +565,8 @@ impl TestServer {
     ) -> Arc<AppState> {
         Arc::new(AppState {
             db: test_db.db().clone(),
-            llm_db: None,
             livekit_client: Some(Arc::new(livekit_test_server.create_api_client())),
             blob_store_client: None,
-            real_stripe_client: None,
-            stripe_client: Some(Arc::new(FakeStripeClient::new())),
-            stripe_billing: None,
             executor,
             kinesis_client: None,
             config: Config {
@@ -608,7 +603,6 @@ impl TestServer {
                 auto_join_channel_id: None,
                 migrations_path: None,
                 seed_path: None,
-                stripe_api_key: None,
                 supermaven_admin_api_key: None,
                 user_backfiller_github_access_token: None,
                 kinesis_region: None,
@@ -903,7 +897,7 @@ impl TestClient {
         let window = cx.update(|cx| cx.active_window().unwrap().downcast::<Workspace>().unwrap());
 
         let entity = window.root(cx).unwrap();
-        let cx = VisualTestContext::from_window(*window.deref(), cx).as_mut();
+        let cx = VisualTestContext::from_window(*window.deref(), cx).into_mut();
         // it might be nice to try and cleanup these at the end of each test.
         (entity, cx)
     }

crates/collab/src/user_backfiller.rs 🔗

@@ -130,17 +130,17 @@ impl UserBackfiller {
             .and_then(|value| value.parse::<i64>().ok())
             .and_then(|value| DateTime::from_timestamp(value, 0));
 
-        if rate_limit_remaining == Some(0) {
-            if let Some(reset_at) = rate_limit_reset {
-                let now = Utc::now();
-                if reset_at > now {
-                    let sleep_duration = reset_at - now;
-                    log::info!(
-                        "rate limit reached. Sleeping for {} seconds",
-                        sleep_duration.num_seconds()
-                    );
-                    self.executor.sleep(sleep_duration.to_std().unwrap()).await;
-                }
+        if rate_limit_remaining == Some(0)
+            && let Some(reset_at) = rate_limit_reset
+        {
+            let now = Utc::now();
+            if reset_at > now {
+                let sleep_duration = reset_at - now;
+                log::info!(
+                    "rate limit reached. Sleeping for {} seconds",
+                    sleep_duration.num_seconds()
+                );
+                self.executor.sleep(sleep_duration.to_std().unwrap()).await;
             }
         }
 

crates/collab_ui/src/channel_view.rs 🔗

@@ -66,7 +66,7 @@ impl ChannelView {
             channel_id,
             link_position,
             pane.clone(),
-            workspace.clone(),
+            workspace,
             window,
             cx,
         );
@@ -107,43 +107,32 @@ impl ChannelView {
                     .find(|view| view.read(cx).channel_buffer.read(cx).remote_id(cx) == buffer_id);
 
                 // If this channel buffer is already open in this pane, just return it.
-                if let Some(existing_view) = existing_view.clone() {
-                    if existing_view.read(cx).channel_buffer == channel_view.read(cx).channel_buffer
-                    {
-                        if let Some(link_position) = link_position {
-                            existing_view.update(cx, |channel_view, cx| {
-                                channel_view.focus_position_from_link(
-                                    link_position,
-                                    true,
-                                    window,
-                                    cx,
-                                )
-                            });
-                        }
-                        return existing_view;
+                if let Some(existing_view) = existing_view.clone()
+                    && existing_view.read(cx).channel_buffer == channel_view.read(cx).channel_buffer
+                {
+                    if let Some(link_position) = link_position {
+                        existing_view.update(cx, |channel_view, cx| {
+                            channel_view.focus_position_from_link(link_position, true, window, cx)
+                        });
                     }
+                    return existing_view;
                 }
 
                 // If the pane contained a disconnected view for this channel buffer,
                 // replace that.
-                if let Some(existing_item) = existing_view {
-                    if let Some(ix) = pane.index_for_item(&existing_item) {
-                        pane.close_item_by_id(
-                            existing_item.entity_id(),
-                            SaveIntent::Skip,
-                            window,
-                            cx,
-                        )
+                if let Some(existing_item) = existing_view
+                    && let Some(ix) = pane.index_for_item(&existing_item)
+                {
+                    pane.close_item_by_id(existing_item.entity_id(), SaveIntent::Skip, window, cx)
                         .detach();
-                        pane.add_item(
-                            Box::new(channel_view.clone()),
-                            true,
-                            true,
-                            Some(ix),
-                            window,
-                            cx,
-                        );
-                    }
+                    pane.add_item(
+                        Box::new(channel_view.clone()),
+                        true,
+                        true,
+                        Some(ix),
+                        window,
+                        cx,
+                    );
                 }
 
                 if let Some(link_position) = link_position {
@@ -259,26 +248,21 @@ impl ChannelView {
             .editor
             .update(cx, |editor, cx| editor.snapshot(window, cx));
 
-        if let Some(outline) = snapshot.buffer_snapshot.outline(None) {
-            if let Some(item) = outline
+        if let Some(outline) = snapshot.buffer_snapshot.outline(None)
+            && let Some(item) = outline
                 .items
                 .iter()
                 .find(|item| &Channel::slug(&item.text).to_lowercase() == &position)
-            {
-                self.editor.update(cx, |editor, cx| {
-                    editor.change_selections(
-                        SelectionEffects::scroll(Autoscroll::focused()),
-                        window,
-                        cx,
-                        |s| {
-                            s.replace_cursors_with(|map| {
-                                vec![item.range.start.to_display_point(map)]
-                            })
-                        },
-                    )
-                });
-                return;
-            }
+        {
+            self.editor.update(cx, |editor, cx| {
+                editor.change_selections(
+                    SelectionEffects::scroll(Autoscroll::focused()),
+                    window,
+                    cx,
+                    |s| s.replace_cursors_with(|map| vec![item.range.start.to_display_point(map)]),
+                )
+            });
+            return;
         }
 
         if !first_attempt {

crates/collab_ui/src/chat_panel.rs 🔗

@@ -287,19 +287,20 @@ impl ChatPanel {
     }
 
     fn acknowledge_last_message(&mut self, cx: &mut Context<Self>) {
-        if self.active && self.is_scrolled_to_bottom {
-            if let Some((chat, _)) = &self.active_chat {
-                if let Some(channel_id) = self.channel_id(cx) {
-                    self.last_acknowledged_message_id = self
-                        .channel_store
-                        .read(cx)
-                        .last_acknowledge_message_id(channel_id);
-                }
-
-                chat.update(cx, |chat, cx| {
-                    chat.acknowledge_last_message(cx);
-                });
+        if self.active
+            && self.is_scrolled_to_bottom
+            && let Some((chat, _)) = &self.active_chat
+        {
+            if let Some(channel_id) = self.channel_id(cx) {
+                self.last_acknowledged_message_id = self
+                    .channel_store
+                    .read(cx)
+                    .last_acknowledge_message_id(channel_id);
             }
+
+            chat.update(cx, |chat, cx| {
+                chat.acknowledge_last_message(cx);
+            });
         }
     }
 
@@ -405,14 +406,13 @@ impl ChatPanel {
                     && last_message.id != this_message.id
                     && duration_since_last_message < Duration::from_secs(5 * 60);
 
-                if let ChannelMessageId::Saved(id) = this_message.id {
-                    if this_message
+                if let ChannelMessageId::Saved(id) = this_message.id
+                    && this_message
                         .mentions
                         .iter()
                         .any(|(_, user_id)| Some(*user_id) == self.client.user_id())
-                    {
-                        active_chat.acknowledge_message(id);
-                    }
+                {
+                    active_chat.acknowledge_message(id);
                 }
 
                 (this_message, is_continuation_from_previous, is_admin)
@@ -674,7 +674,7 @@ impl ChatPanel {
                 })
             })
             .when_some(message_id, |el, message_id| {
-                let this = cx.entity().clone();
+                let this = cx.entity();
 
                 el.child(
                     self.render_popover_button(
@@ -871,34 +871,33 @@ impl ChatPanel {
                 scroll_to_message_id.or(this.last_acknowledged_message_id)
             })?;
 
-            if let Some(message_id) = scroll_to_message_id {
-                if let Some(item_ix) =
+            if let Some(message_id) = scroll_to_message_id
+                && let Some(item_ix) =
                     ChannelChat::load_history_since_message(chat.clone(), message_id, cx.clone())
                         .await
-                {
-                    this.update(cx, |this, cx| {
-                        if let Some(highlight_message_id) = highlight_message_id {
-                            let task = cx.spawn(async move |this, cx| {
-                                cx.background_executor().timer(Duration::from_secs(2)).await;
-                                this.update(cx, |this, cx| {
-                                    this.highlighted_message.take();
-                                    cx.notify();
-                                })
-                                .ok();
-                            });
-
-                            this.highlighted_message = Some((highlight_message_id, task));
-                        }
+            {
+                this.update(cx, |this, cx| {
+                    if let Some(highlight_message_id) = highlight_message_id {
+                        let task = cx.spawn(async move |this, cx| {
+                            cx.background_executor().timer(Duration::from_secs(2)).await;
+                            this.update(cx, |this, cx| {
+                                this.highlighted_message.take();
+                                cx.notify();
+                            })
+                            .ok();
+                        });
 
-                        if this.active_chat.as_ref().map_or(false, |(c, _)| *c == chat) {
-                            this.message_list.scroll_to(ListOffset {
-                                item_ix,
-                                offset_in_item: px(0.0),
-                            });
-                            cx.notify();
-                        }
-                    })?;
-                }
+                        this.highlighted_message = Some((highlight_message_id, task));
+                    }
+
+                    if this.active_chat.as_ref().is_some_and(|(c, _)| *c == chat) {
+                        this.message_list.scroll_to(ListOffset {
+                            item_ix,
+                            offset_in_item: px(0.0),
+                        });
+                        cx.notify();
+                    }
+                })?;
             }
 
             Ok(())
@@ -1039,7 +1038,7 @@ impl Render for ChatPanel {
                     .cloned();
 
                 el.when_some(reply_message, |el, reply_message| {
-                    let user_being_replied_to = reply_message.sender.clone();
+                    let user_being_replied_to = reply_message.sender;
 
                     el.child(
                         h_flex()
@@ -1187,7 +1186,7 @@ impl Panel for ChatPanel {
                 let is_in_call = ActiveCall::global(cx)
                     .read(cx)
                     .room()
-                    .map_or(false, |room| room.read(cx).contains_guests());
+                    .is_some_and(|room| room.read(cx).contains_guests());
 
                 self.active || is_in_call
             }

crates/collab_ui/src/chat_panel/message_editor.rs 🔗

@@ -241,38 +241,36 @@ impl MessageEditor {
     ) -> Task<Result<Vec<CompletionResponse>>> {
         if let Some((start_anchor, query, candidates)) =
             self.collect_mention_candidates(buffer, end_anchor, cx)
+            && !candidates.is_empty()
         {
-            if !candidates.is_empty() {
-                return cx.spawn(async move |_, cx| {
-                    let completion_response = Self::completions_for_candidates(
-                        &cx,
-                        query.as_str(),
-                        &candidates,
-                        start_anchor..end_anchor,
-                        Self::completion_for_mention,
-                    )
-                    .await;
-                    Ok(vec![completion_response])
-                });
-            }
+            return cx.spawn(async move |_, cx| {
+                let completion_response = Self::completions_for_candidates(
+                    cx,
+                    query.as_str(),
+                    &candidates,
+                    start_anchor..end_anchor,
+                    Self::completion_for_mention,
+                )
+                .await;
+                Ok(vec![completion_response])
+            });
         }
 
         if let Some((start_anchor, query, candidates)) =
             self.collect_emoji_candidates(buffer, end_anchor, cx)
+            && !candidates.is_empty()
         {
-            if !candidates.is_empty() {
-                return cx.spawn(async move |_, cx| {
-                    let completion_response = Self::completions_for_candidates(
-                        &cx,
-                        query.as_str(),
-                        candidates,
-                        start_anchor..end_anchor,
-                        Self::completion_for_emoji,
-                    )
-                    .await;
-                    Ok(vec![completion_response])
-                });
-            }
+            return cx.spawn(async move |_, cx| {
+                let completion_response = Self::completions_for_candidates(
+                    cx,
+                    query.as_str(),
+                    candidates,
+                    start_anchor..end_anchor,
+                    Self::completion_for_emoji,
+                )
+                .await;
+                Ok(vec![completion_response])
+            });
         }
 
         Task::ready(Ok(vec![CompletionResponse {
@@ -399,11 +397,10 @@ impl MessageEditor {
     ) -> Option<(Anchor, String, &'static [StringMatchCandidate])> {
         static EMOJI_FUZZY_MATCH_CANDIDATES: LazyLock<Vec<StringMatchCandidate>> =
             LazyLock::new(|| {
-                let emojis = emojis::iter()
+                emojis::iter()
                     .flat_map(|s| s.shortcodes())
                     .map(|emoji| StringMatchCandidate::new(0, emoji))
-                    .collect::<Vec<_>>();
-                emojis
+                    .collect::<Vec<_>>()
             });
 
         let end_offset = end_anchor.to_offset(buffer.read(cx));
@@ -474,18 +471,17 @@ impl MessageEditor {
                 for range in ranges {
                     text.clear();
                     text.extend(buffer.text_for_range(range.clone()));
-                    if let Some(username) = text.strip_prefix('@') {
-                        if let Some(user) = this
+                    if let Some(username) = text.strip_prefix('@')
+                        && let Some(user) = this
                             .user_store
                             .read(cx)
                             .cached_user_by_github_login(username)
-                        {
-                            let start = multi_buffer.anchor_after(range.start);
-                            let end = multi_buffer.anchor_after(range.end);
+                    {
+                        let start = multi_buffer.anchor_after(range.start);
+                        let end = multi_buffer.anchor_after(range.end);
 
-                            mentioned_user_ids.push(user.id);
-                            anchor_ranges.push(start..end);
-                        }
+                        mentioned_user_ids.push(user.id);
+                        anchor_ranges.push(start..end);
                     }
                 }
 

crates/collab_ui/src/collab_panel.rs 🔗

@@ -95,7 +95,7 @@ pub fn init(cx: &mut App) {
                 .and_then(|room| room.read(cx).channel_id());
 
             if let Some(channel_id) = channel_id {
-                let workspace = cx.entity().clone();
+                let workspace = cx.entity();
                 window.defer(cx, move |window, cx| {
                     ChannelView::open(channel_id, None, workspace, window, cx)
                         .detach_and_log_err(cx)
@@ -311,10 +311,10 @@ impl CollabPanel {
                 window,
                 |this: &mut Self, _, event, window, cx| {
                     if let editor::EditorEvent::Blurred = event {
-                        if let Some(state) = &this.channel_editing_state {
-                            if state.pending_name().is_some() {
-                                return;
-                            }
+                        if let Some(state) = &this.channel_editing_state
+                            && state.pending_name().is_some()
+                        {
+                            return;
                         }
                         this.take_editing_state(window, cx);
                         this.update_entries(false, cx);
@@ -491,11 +491,11 @@ impl CollabPanel {
             if !self.collapsed_sections.contains(&Section::ActiveCall) {
                 let room = room.read(cx);
 
-                if query.is_empty() {
-                    if let Some(channel_id) = room.channel_id() {
-                        self.entries.push(ListEntry::ChannelNotes { channel_id });
-                        self.entries.push(ListEntry::ChannelChat { channel_id });
-                    }
+                if query.is_empty()
+                    && let Some(channel_id) = room.channel_id()
+                {
+                    self.entries.push(ListEntry::ChannelNotes { channel_id });
+                    self.entries.push(ListEntry::ChannelChat { channel_id });
                 }
 
                 // Populate the active user.
@@ -639,10 +639,10 @@ impl CollabPanel {
                 &Default::default(),
                 executor.clone(),
             ));
-            if let Some(state) = &self.channel_editing_state {
-                if matches!(state, ChannelEditingState::Create { location: None, .. }) {
-                    self.entries.push(ListEntry::ChannelEditor { depth: 0 });
-                }
+            if let Some(state) = &self.channel_editing_state
+                && matches!(state, ChannelEditingState::Create { location: None, .. })
+            {
+                self.entries.push(ListEntry::ChannelEditor { depth: 0 });
             }
             let mut collapse_depth = None;
             for mat in matches {
@@ -664,9 +664,7 @@ impl CollabPanel {
 
                 let has_children = channel_store
                     .channel_at_index(mat.candidate_id + 1)
-                    .map_or(false, |next_channel| {
-                        next_channel.parent_path.ends_with(&[channel.id])
-                    });
+                    .is_some_and(|next_channel| next_channel.parent_path.ends_with(&[channel.id]));
 
                 match &self.channel_editing_state {
                     Some(ChannelEditingState::Create {
@@ -1125,7 +1123,7 @@ impl CollabPanel {
     }
 
     fn has_subchannels(&self, ix: usize) -> bool {
-        self.entries.get(ix).map_or(false, |entry| {
+        self.entries.get(ix).is_some_and(|entry| {
             if let ListEntry::Channel { has_children, .. } = entry {
                 *has_children
             } else {
@@ -1142,7 +1140,7 @@ impl CollabPanel {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        let this = cx.entity().clone();
+        let this = cx.entity();
         if !(role == proto::ChannelRole::Guest
             || role == proto::ChannelRole::Talker
             || role == proto::ChannelRole::Member)
@@ -1272,7 +1270,7 @@ impl CollabPanel {
                 .channel_for_id(clipboard.channel_id)
                 .map(|channel| channel.name.clone())
         });
-        let this = cx.entity().clone();
+        let this = cx.entity();
 
         let context_menu = ContextMenu::build(window, cx, |mut context_menu, window, cx| {
             if self.has_subchannels(ix) {
@@ -1439,7 +1437,7 @@ impl CollabPanel {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        let this = cx.entity().clone();
+        let this = cx.entity();
         let in_room = ActiveCall::global(cx).read(cx).room().is_some();
 
         let context_menu = ContextMenu::build(window, cx, |mut context_menu, _, _| {
@@ -1552,98 +1550,93 @@ impl CollabPanel {
             return;
         }
 
-        if let Some(selection) = self.selection {
-            if let Some(entry) = self.entries.get(selection) {
-                match entry {
-                    ListEntry::Header(section) => match section {
-                        Section::ActiveCall => Self::leave_call(window, cx),
-                        Section::Channels => self.new_root_channel(window, cx),
-                        Section::Contacts => self.toggle_contact_finder(window, cx),
-                        Section::ContactRequests
-                        | Section::Online
-                        | Section::Offline
-                        | Section::ChannelInvites => {
-                            self.toggle_section_expanded(*section, cx);
-                        }
-                    },
-                    ListEntry::Contact { contact, calling } => {
-                        if contact.online && !contact.busy && !calling {
-                            self.call(contact.user.id, window, cx);
-                        }
+        if let Some(selection) = self.selection
+            && let Some(entry) = self.entries.get(selection)
+        {
+            match entry {
+                ListEntry::Header(section) => match section {
+                    Section::ActiveCall => Self::leave_call(window, cx),
+                    Section::Channels => self.new_root_channel(window, cx),
+                    Section::Contacts => self.toggle_contact_finder(window, cx),
+                    Section::ContactRequests
+                    | Section::Online
+                    | Section::Offline
+                    | Section::ChannelInvites => {
+                        self.toggle_section_expanded(*section, cx);
                     }
-                    ListEntry::ParticipantProject {
-                        project_id,
-                        host_user_id,
-                        ..
-                    } => {
-                        if let Some(workspace) = self.workspace.upgrade() {
-                            let app_state = workspace.read(cx).app_state().clone();
-                            workspace::join_in_room_project(
-                                *project_id,
-                                *host_user_id,
-                                app_state,
-                                cx,
-                            )
+                },
+                ListEntry::Contact { contact, calling } => {
+                    if contact.online && !contact.busy && !calling {
+                        self.call(contact.user.id, window, cx);
+                    }
+                }
+                ListEntry::ParticipantProject {
+                    project_id,
+                    host_user_id,
+                    ..
+                } => {
+                    if let Some(workspace) = self.workspace.upgrade() {
+                        let app_state = workspace.read(cx).app_state().clone();
+                        workspace::join_in_room_project(*project_id, *host_user_id, app_state, cx)
                             .detach_and_prompt_err(
                                 "Failed to join project",
                                 window,
                                 cx,
                                 |_, _, _| None,
                             );
-                        }
-                    }
-                    ListEntry::ParticipantScreen { peer_id, .. } => {
-                        let Some(peer_id) = peer_id else {
-                            return;
-                        };
-                        if let Some(workspace) = self.workspace.upgrade() {
-                            workspace.update(cx, |workspace, cx| {
-                                workspace.open_shared_screen(*peer_id, window, cx)
-                            });
-                        }
                     }
-                    ListEntry::Channel { channel, .. } => {
-                        let is_active = maybe!({
-                            let call_channel = ActiveCall::global(cx)
-                                .read(cx)
-                                .room()?
-                                .read(cx)
-                                .channel_id()?;
-
-                            Some(call_channel == channel.id)
-                        })
-                        .unwrap_or(false);
-                        if is_active {
-                            self.open_channel_notes(channel.id, window, cx)
-                        } else {
-                            self.join_channel(channel.id, window, cx)
-                        }
-                    }
-                    ListEntry::ContactPlaceholder => self.toggle_contact_finder(window, cx),
-                    ListEntry::CallParticipant { user, peer_id, .. } => {
-                        if Some(user) == self.user_store.read(cx).current_user().as_ref() {
-                            Self::leave_call(window, cx);
-                        } else if let Some(peer_id) = peer_id {
-                            self.workspace
-                                .update(cx, |workspace, cx| workspace.follow(*peer_id, window, cx))
-                                .ok();
-                        }
-                    }
-                    ListEntry::IncomingRequest(user) => {
-                        self.respond_to_contact_request(user.id, true, window, cx)
-                    }
-                    ListEntry::ChannelInvite(channel) => {
-                        self.respond_to_channel_invite(channel.id, true, cx)
+                }
+                ListEntry::ParticipantScreen { peer_id, .. } => {
+                    let Some(peer_id) = peer_id else {
+                        return;
+                    };
+                    if let Some(workspace) = self.workspace.upgrade() {
+                        workspace.update(cx, |workspace, cx| {
+                            workspace.open_shared_screen(*peer_id, window, cx)
+                        });
                     }
-                    ListEntry::ChannelNotes { channel_id } => {
-                        self.open_channel_notes(*channel_id, window, cx)
+                }
+                ListEntry::Channel { channel, .. } => {
+                    let is_active = maybe!({
+                        let call_channel = ActiveCall::global(cx)
+                            .read(cx)
+                            .room()?
+                            .read(cx)
+                            .channel_id()?;
+
+                        Some(call_channel == channel.id)
+                    })
+                    .unwrap_or(false);
+                    if is_active {
+                        self.open_channel_notes(channel.id, window, cx)
+                    } else {
+                        self.join_channel(channel.id, window, cx)
                     }
-                    ListEntry::ChannelChat { channel_id } => {
-                        self.join_channel_chat(*channel_id, window, cx)
+                }
+                ListEntry::ContactPlaceholder => self.toggle_contact_finder(window, cx),
+                ListEntry::CallParticipant { user, peer_id, .. } => {
+                    if Some(user) == self.user_store.read(cx).current_user().as_ref() {
+                        Self::leave_call(window, cx);
+                    } else if let Some(peer_id) = peer_id {
+                        self.workspace
+                            .update(cx, |workspace, cx| workspace.follow(*peer_id, window, cx))
+                            .ok();
                     }
-                    ListEntry::OutgoingRequest(_) => {}
-                    ListEntry::ChannelEditor { .. } => {}
                 }
+                ListEntry::IncomingRequest(user) => {
+                    self.respond_to_contact_request(user.id, true, window, cx)
+                }
+                ListEntry::ChannelInvite(channel) => {
+                    self.respond_to_channel_invite(channel.id, true, cx)
+                }
+                ListEntry::ChannelNotes { channel_id } => {
+                    self.open_channel_notes(*channel_id, window, cx)
+                }
+                ListEntry::ChannelChat { channel_id } => {
+                    self.join_channel_chat(*channel_id, window, cx)
+                }
+                ListEntry::OutgoingRequest(_) => {}
+                ListEntry::ChannelEditor { .. } => {}
             }
         }
     }
@@ -1828,10 +1821,10 @@ impl CollabPanel {
     }
 
     fn select_channel_editor(&mut self) {
-        self.selection = self.entries.iter().position(|entry| match entry {
-            ListEntry::ChannelEditor { .. } => true,
-            _ => false,
-        });
+        self.selection = self
+            .entries
+            .iter()
+            .position(|entry| matches!(entry, ListEntry::ChannelEditor { .. }));
     }
 
     fn new_subchannel(
@@ -2317,7 +2310,7 @@ impl CollabPanel {
                                 let client = this.client.clone();
                                 cx.spawn_in(window, async move |_, cx| {
                                     client
-                                        .connect(true, &cx)
+                                        .connect(true, cx)
                                         .await
                                         .into_response()
                                         .notify_async_err(cx);
@@ -2514,7 +2507,7 @@ impl CollabPanel {
 
         let button = match section {
             Section::ActiveCall => channel_link.map(|channel_link| {
-                let channel_link_copy = channel_link.clone();
+                let channel_link_copy = channel_link;
                 IconButton::new("channel-link", IconName::Copy)
                     .icon_size(IconSize::Small)
                     .size(ButtonSize::None)
@@ -2698,7 +2691,7 @@ impl CollabPanel {
                 h_flex()
                     .w_full()
                     .justify_between()
-                    .child(Label::new(github_login.clone()))
+                    .child(Label::new(github_login))
                     .child(h_flex().children(controls)),
             )
             .start_slot(Avatar::new(user.avatar_uri.clone()))
@@ -2931,7 +2924,7 @@ impl CollabPanel {
                                 .visible_on_hover(""),
                         )
                         .child(
-                            IconButton::new("channel_notes", IconName::FileText)
+                            IconButton::new("channel_notes", IconName::Reader)
                                 .style(ButtonStyle::Filled)
                                 .shape(ui::IconButtonShape::Square)
                                 .icon_size(IconSize::Small)
@@ -3132,7 +3125,7 @@ impl Panel for CollabPanel {
 
 impl Focusable for CollabPanel {
     fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
-        self.filter_editor.focus_handle(cx).clone()
+        self.filter_editor.focus_handle(cx)
     }
 }
 

crates/collab_ui/src/collab_panel/channel_modal.rs 🔗

@@ -586,7 +586,7 @@ impl ChannelModalDelegate {
             return;
         };
         let user_id = membership.user.id;
-        let picker = cx.entity().clone();
+        let picker = cx.entity();
         let context_menu = ContextMenu::build(window, cx, |mut menu, _window, _cx| {
             let role = membership.role;
 

crates/collab_ui/src/notification_panel.rs 🔗

@@ -121,13 +121,12 @@ impl NotificationPanel {
             let notification_list = ListState::new(0, ListAlignment::Top, px(1000.));
             notification_list.set_scroll_handler(cx.listener(
                 |this, event: &ListScrollEvent, _, cx| {
-                    if event.count.saturating_sub(event.visible_range.end) < LOADING_THRESHOLD {
-                        if let Some(task) = this
+                    if event.count.saturating_sub(event.visible_range.end) < LOADING_THRESHOLD
+                        && let Some(task) = this
                             .notification_store
                             .update(cx, |store, cx| store.load_more_notifications(false, cx))
-                        {
-                            task.detach();
-                        }
+                    {
+                        task.detach();
                     }
                 },
             ));
@@ -290,7 +289,7 @@ impl NotificationPanel {
                         .gap_1()
                         .size_full()
                         .overflow_hidden()
-                        .child(Label::new(text.clone()))
+                        .child(Label::new(text))
                         .child(
                             h_flex()
                                 .child(
@@ -321,7 +320,7 @@ impl NotificationPanel {
                                             .justify_end()
                                             .child(Button::new("decline", "Decline").on_click({
                                                 let notification = notification.clone();
-                                                let entity = cx.entity().clone();
+                                                let entity = cx.entity();
                                                 move |_, _, cx| {
                                                     entity.update(cx, |this, cx| {
                                                         this.respond_to_notification(
@@ -334,7 +333,7 @@ impl NotificationPanel {
                                             }))
                                             .child(Button::new("accept", "Accept").on_click({
                                                 let notification = notification.clone();
-                                                let entity = cx.entity().clone();
+                                                let entity = cx.entity();
                                                 move |_, _, cx| {
                                                     entity.update(cx, |this, cx| {
                                                         this.respond_to_notification(
@@ -469,20 +468,19 @@ impl NotificationPanel {
             channel_id,
             ..
         } = notification.clone()
+            && let Some(workspace) = self.workspace.upgrade()
         {
-            if let Some(workspace) = self.workspace.upgrade() {
-                window.defer(cx, move |window, cx| {
-                    workspace.update(cx, |workspace, cx| {
-                        if let Some(panel) = workspace.focus_panel::<ChatPanel>(window, cx) {
-                            panel.update(cx, |panel, cx| {
-                                panel
-                                    .select_channel(ChannelId(channel_id), Some(message_id), cx)
-                                    .detach_and_log_err(cx);
-                            });
-                        }
-                    });
+            window.defer(cx, move |window, cx| {
+                workspace.update(cx, |workspace, cx| {
+                    if let Some(panel) = workspace.focus_panel::<ChatPanel>(window, cx) {
+                        panel.update(cx, |panel, cx| {
+                            panel
+                                .select_channel(ChannelId(channel_id), Some(message_id), cx)
+                                .detach_and_log_err(cx);
+                        });
+                    }
                 });
-            }
+            });
         }
     }
 
@@ -491,18 +489,18 @@ impl NotificationPanel {
             return false;
         }
 
-        if let Notification::ChannelMessageMention { channel_id, .. } = &notification {
-            if let Some(workspace) = self.workspace.upgrade() {
-                return if let Some(panel) = workspace.read(cx).panel::<ChatPanel>(cx) {
-                    let panel = panel.read(cx);
-                    panel.is_scrolled_to_bottom()
-                        && panel
-                            .active_chat()
-                            .map_or(false, |chat| chat.read(cx).channel_id.0 == *channel_id)
-                } else {
-                    false
-                };
-            }
+        if let Notification::ChannelMessageMention { channel_id, .. } = &notification
+            && let Some(workspace) = self.workspace.upgrade()
+        {
+            return if let Some(panel) = workspace.read(cx).panel::<ChatPanel>(cx) {
+                let panel = panel.read(cx);
+                panel.is_scrolled_to_bottom()
+                    && panel
+                        .active_chat()
+                        .is_some_and(|chat| chat.read(cx).channel_id.0 == *channel_id)
+            } else {
+                false
+            };
         }
 
         false
@@ -582,16 +580,16 @@ impl NotificationPanel {
     }
 
     fn remove_toast(&mut self, notification_id: u64, cx: &mut Context<Self>) {
-        if let Some((current_id, _)) = &self.current_notification_toast {
-            if *current_id == notification_id {
-                self.current_notification_toast.take();
-                self.workspace
-                    .update(cx, |workspace, cx| {
-                        let id = NotificationId::unique::<NotificationToast>();
-                        workspace.dismiss_notification(&id, cx)
-                    })
-                    .ok();
-            }
+        if let Some((current_id, _)) = &self.current_notification_toast
+            && *current_id == notification_id
+        {
+            self.current_notification_toast.take();
+            self.workspace
+                .update(cx, |workspace, cx| {
+                    let id = NotificationId::unique::<NotificationToast>();
+                    workspace.dismiss_notification(&id, cx)
+                })
+                .ok();
         }
     }
 
@@ -643,7 +641,7 @@ impl Render for NotificationPanel {
                                             let client = client.clone();
                                             window
                                                 .spawn(cx, async move |cx| {
-                                                    match client.connect(true, &cx).await {
+                                                    match client.connect(true, cx).await {
                                                         util::ConnectionResult::Timeout => {
                                                             log::error!("Connection timeout");
                                                         }

crates/command_palette/src/command_palette.rs 🔗

@@ -206,7 +206,7 @@ impl CommandPaletteDelegate {
         if parse_zed_link(&query, cx).is_some() {
             intercept_results = vec![CommandInterceptResult {
                 action: OpenZedUrl { url: query.clone() }.boxed_clone(),
-                string: query.clone(),
+                string: query,
                 positions: vec![],
             }]
         }

crates/component/src/component_layout.rs 🔗

@@ -42,7 +42,7 @@ impl RenderOnce for ComponentExample {
                             div()
                                 .text_size(rems(0.875))
                                 .text_color(cx.theme().colors().text_muted)
-                                .child(description.clone()),
+                                .child(description),
                         )
                     }),
             )

crates/context_server/src/client.rs 🔗

@@ -67,11 +67,7 @@ pub(crate) struct Client {
 pub(crate) struct ContextServerId(pub Arc<str>);
 
 fn is_null_value<T: Serialize>(value: &T) -> bool {
-    if let Ok(Value::Null) = serde_json::to_value(value) {
-        true
-    } else {
-        false
-    }
+    matches!(serde_json::to_value(value), Ok(Value::Null))
 }
 
 #[derive(Serialize, Deserialize)]
@@ -161,7 +157,7 @@ impl Client {
         working_directory: &Option<PathBuf>,
         cx: AsyncApp,
     ) -> Result<Self> {
-        log::info!(
+        log::debug!(
             "starting context server (executable={:?}, args={:?})",
             binary.executable,
             &binary.args
@@ -271,10 +267,10 @@ impl Client {
                     );
                 }
             } else if let Ok(response) = serde_json::from_str::<AnyResponse>(&message) {
-                if let Some(handlers) = response_handlers.lock().as_mut() {
-                    if let Some(handler) = handlers.remove(&response.id) {
-                        handler(Ok(message.to_string()));
-                    }
+                if let Some(handlers) = response_handlers.lock().as_mut()
+                    && let Some(handler) = handlers.remove(&response.id)
+                {
+                    handler(Ok(message.to_string()));
                 }
             } else if let Ok(notification) = serde_json::from_str::<AnyNotification>(&message) {
                 let mut notification_handlers = notification_handlers.lock();
@@ -295,7 +291,7 @@ impl Client {
     /// Continuously reads and logs any error messages from the server.
     async fn handle_err(transport: Arc<dyn Transport>) -> anyhow::Result<()> {
         while let Some(err) = transport.receive_err().next().await {
-            log::warn!("context server stderr: {}", err.trim());
+            log::debug!("context server stderr: {}", err.trim());
         }
 
         Ok(())

crates/context_server/src/context_server.rs 🔗

@@ -137,7 +137,7 @@ impl ContextServer {
     }
 
     async fn initialize(&self, client: Client) -> Result<()> {
-        log::info!("starting context server {}", self.id);
+        log::debug!("starting context server {}", self.id);
         let protocol = crate::protocol::ModelContextProtocol::new(client);
         let client_info = types::Implementation {
             name: "Zed".to_string(),

crates/context_server/src/listener.rs 🔗

@@ -14,6 +14,7 @@ use serde::de::DeserializeOwned;
 use serde_json::{json, value::RawValue};
 use smol::stream::StreamExt;
 use std::{
+    any::TypeId,
     cell::RefCell,
     path::{Path, PathBuf},
     rc::Rc,
@@ -77,7 +78,7 @@ impl McpServer {
                 socket_path,
                 _server_task: server_task,
                 tools,
-                handlers: handlers,
+                handlers,
             })
         })
     }
@@ -87,23 +88,30 @@ impl McpServer {
         settings.inline_subschemas = true;
         let mut generator = settings.into_generator();
 
-        let output_schema = generator.root_schema_for::<T::Output>();
-        let unit_schema = generator.root_schema_for::<T::Output>();
+        let input_schema = generator.root_schema_for::<T::Input>();
+
+        let description = input_schema
+            .get("description")
+            .and_then(|desc| desc.as_str())
+            .map(|desc| desc.to_string());
+        debug_assert!(
+            description.is_some(),
+            "Input schema struct must include a doc comment for the tool description"
+        );
 
         let registered_tool = RegisteredTool {
             tool: Tool {
                 name: T::NAME.into(),
-                description: Some(tool.description().into()),
-                input_schema: generator.root_schema_for::<T::Input>().into(),
-                output_schema: if output_schema == unit_schema {
+                description,
+                input_schema: input_schema.into(),
+                output_schema: if TypeId::of::<T::Output>() == TypeId::of::<()>() {
                     None
                 } else {
-                    Some(output_schema.into())
+                    Some(generator.root_schema_for::<T::Output>().into())
                 },
                 annotations: Some(tool.annotations()),
             },
             handler: Box::new({
-                let tool = tool.clone();
                 move |input_value, cx| {
                     let input = match input_value {
                         Some(input) => serde_json::from_value(input),
@@ -315,12 +323,12 @@ impl McpServer {
                     Self::send_err(
                         request_id,
                         format!("Tool not found: {}", params.name),
-                        &outgoing_tx,
+                        outgoing_tx,
                     );
                 }
             }
             Err(err) => {
-                Self::send_err(request_id, err.to_string(), &outgoing_tx);
+                Self::send_err(request_id, err.to_string(), outgoing_tx);
             }
         }
     }
@@ -399,8 +407,6 @@ pub trait McpServerTool {
 
     const NAME: &'static str;
 
-    fn description(&self) -> &'static str;
-
     fn annotations(&self) -> ToolAnnotations {
         ToolAnnotations {
             title: None,
@@ -418,6 +424,7 @@ pub trait McpServerTool {
     ) -> impl Future<Output = Result<ToolResponse<Self::Output>>>;
 }
 
+#[derive(Debug)]
 pub struct ToolResponse<T> {
     pub content: Vec<ToolResponseContent>,
     pub structured_content: T,

crates/context_server/src/types.rs 🔗

@@ -691,7 +691,7 @@ impl CallToolResponse {
         let mut text = String::new();
         for chunk in &self.content {
             if let ToolResponseContent::Text { text: chunk } = chunk {
-                text.push_str(&chunk)
+                text.push_str(chunk)
             };
         }
         text
@@ -711,6 +711,16 @@ pub enum ToolResponseContent {
     Resource { resource: ResourceContents },
 }
 
+impl ToolResponseContent {
+    pub fn text(&self) -> Option<&str> {
+        if let ToolResponseContent::Text { text } = self {
+            Some(text)
+        } else {
+            None
+        }
+    }
+}
+
 #[derive(Debug, Serialize, Deserialize)]
 #[serde(rename_all = "camelCase")]
 pub struct ListToolsResponse {

crates/copilot/src/copilot.rs 🔗

@@ -21,7 +21,7 @@ use language::{
     point_from_lsp, point_to_lsp,
 };
 use lsp::{LanguageServer, LanguageServerBinary, LanguageServerId, LanguageServerName};
-use node_runtime::NodeRuntime;
+use node_runtime::{NodeRuntime, VersionStrategy};
 use parking_lot::Mutex;
 use project::DisableAiSettings;
 use request::StatusNotification;
@@ -81,10 +81,7 @@ pub fn init(
     };
     copilot_chat::init(fs.clone(), http.clone(), configuration, cx);
 
-    let copilot = cx.new({
-        let node_runtime = node_runtime.clone();
-        move |cx| Copilot::start(new_server_id, fs, node_runtime, cx)
-    });
+    let copilot = cx.new(move |cx| Copilot::start(new_server_id, fs, node_runtime, cx));
     Copilot::set_global(copilot.clone(), cx);
     cx.observe(&copilot, |copilot, cx| {
         copilot.update(cx, |copilot, cx| copilot.update_action_visibilities(cx));
@@ -129,7 +126,7 @@ impl CopilotServer {
     fn as_authenticated(&mut self) -> Result<&mut RunningCopilotServer> {
         let server = self.as_running()?;
         anyhow::ensure!(
-            matches!(server.sign_in_status, SignInStatus::Authorized { .. }),
+            matches!(server.sign_in_status, SignInStatus::Authorized),
             "must sign in before using copilot"
         );
         Ok(server)
@@ -349,7 +346,11 @@ impl Copilot {
         this.start_copilot(true, false, cx);
         cx.observe_global::<SettingsStore>(move |this, cx| {
             this.start_copilot(true, false, cx);
-            this.send_configuration_update(cx);
+            if let Ok(server) = this.server.as_running() {
+                notify_did_change_config_to_server(&server.lsp, cx)
+                    .context("copilot setting change: did change configuration")
+                    .log_err();
+            }
         })
         .detach();
         this
@@ -438,43 +439,6 @@ impl Copilot {
         if env.is_empty() { None } else { Some(env) }
     }
 
-    fn send_configuration_update(&mut self, cx: &mut Context<Self>) {
-        let copilot_settings = all_language_settings(None, cx)
-            .edit_predictions
-            .copilot
-            .clone();
-
-        let settings = json!({
-            "http": {
-                "proxy": copilot_settings.proxy,
-                "proxyStrictSSL": !copilot_settings.proxy_no_verify.unwrap_or(false)
-            },
-            "github-enterprise": {
-                "uri": copilot_settings.enterprise_uri
-            }
-        });
-
-        if let Some(copilot_chat) = copilot_chat::CopilotChat::global(cx) {
-            copilot_chat.update(cx, |chat, cx| {
-                chat.set_configuration(
-                    copilot_chat::CopilotChatConfiguration {
-                        enterprise_uri: copilot_settings.enterprise_uri.clone(),
-                    },
-                    cx,
-                );
-            });
-        }
-
-        if let Ok(server) = self.server.as_running() {
-            server
-                .lsp
-                .notify::<lsp::notification::DidChangeConfiguration>(
-                    &lsp::DidChangeConfigurationParams { settings },
-                )
-                .log_err();
-        }
-    }
-
     #[cfg(any(test, feature = "test-support"))]
     pub fn fake(cx: &mut gpui::TestAppContext) -> (Entity<Self>, lsp::FakeLanguageServer) {
         use fs::FakeFs;
@@ -573,6 +537,9 @@ impl Copilot {
                 })?
                 .await?;
 
+            this.update(cx, |_, cx| notify_did_change_config_to_server(&server, cx))?
+                .context("copilot: did change configuration")?;
+
             let status = server
                 .request::<request::CheckStatus>(request::CheckStatusParams {
                     local_checks_only: false,
@@ -598,8 +565,6 @@ impl Copilot {
                     });
                     cx.emit(Event::CopilotLanguageServerStarted);
                     this.update_sign_in_status(status, cx);
-                    // Send configuration now that the LSP is fully started
-                    this.send_configuration_update(cx);
                 }
                 Err(error) => {
                     this.server = CopilotServer::Error(error.to_string().into());
@@ -613,12 +578,12 @@ impl Copilot {
     pub(crate) fn sign_in(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> {
         if let CopilotServer::Running(server) = &mut self.server {
             let task = match &server.sign_in_status {
-                SignInStatus::Authorized { .. } => Task::ready(Ok(())).shared(),
+                SignInStatus::Authorized => Task::ready(Ok(())).shared(),
                 SignInStatus::SigningIn { task, .. } => {
                     cx.notify();
                     task.clone()
                 }
-                SignInStatus::SignedOut { .. } | SignInStatus::Unauthorized { .. } => {
+                SignInStatus::SignedOut { .. } | SignInStatus::Unauthorized => {
                     let lsp = server.lsp.clone();
                     let task = cx
                         .spawn(async move |this, cx| {
@@ -640,15 +605,13 @@ impl Copilot {
                                                 sign_in_status: status,
                                                 ..
                                             }) = &mut this.server
-                                            {
-                                                if let SignInStatus::SigningIn {
+                                                && let SignInStatus::SigningIn {
                                                     prompt: prompt_flow,
                                                     ..
                                                 } = status
-                                                {
-                                                    *prompt_flow = Some(flow.clone());
-                                                    cx.notify();
-                                                }
+                                            {
+                                                *prompt_flow = Some(flow.clone());
+                                                cx.notify();
                                             }
                                         })?;
                                         let response = lsp
@@ -764,7 +727,7 @@ impl Copilot {
             ..
         }) = &mut self.server
         {
-            if !matches!(status, SignInStatus::Authorized { .. }) {
+            if !matches!(status, SignInStatus::Authorized) {
                 return;
             }
 
@@ -814,59 +777,58 @@ impl Copilot {
         event: &language::BufferEvent,
         cx: &mut Context<Self>,
     ) -> Result<()> {
-        if let Ok(server) = self.server.as_running() {
-            if let Some(registered_buffer) = server.registered_buffers.get_mut(&buffer.entity_id())
-            {
-                match event {
-                    language::BufferEvent::Edited => {
-                        drop(registered_buffer.report_changes(&buffer, cx));
-                    }
-                    language::BufferEvent::Saved => {
+        if let Ok(server) = self.server.as_running()
+            && let Some(registered_buffer) = server.registered_buffers.get_mut(&buffer.entity_id())
+        {
+            match event {
+                language::BufferEvent::Edited => {
+                    drop(registered_buffer.report_changes(&buffer, cx));
+                }
+                language::BufferEvent::Saved => {
+                    server
+                        .lsp
+                        .notify::<lsp::notification::DidSaveTextDocument>(
+                            &lsp::DidSaveTextDocumentParams {
+                                text_document: lsp::TextDocumentIdentifier::new(
+                                    registered_buffer.uri.clone(),
+                                ),
+                                text: None,
+                            },
+                        )?;
+                }
+                language::BufferEvent::FileHandleChanged
+                | language::BufferEvent::LanguageChanged => {
+                    let new_language_id = id_for_language(buffer.read(cx).language());
+                    let Ok(new_uri) = uri_for_buffer(&buffer, cx) else {
+                        return Ok(());
+                    };
+                    if new_uri != registered_buffer.uri
+                        || new_language_id != registered_buffer.language_id
+                    {
+                        let old_uri = mem::replace(&mut registered_buffer.uri, new_uri);
+                        registered_buffer.language_id = new_language_id;
                         server
                             .lsp
-                            .notify::<lsp::notification::DidSaveTextDocument>(
-                                &lsp::DidSaveTextDocumentParams {
-                                    text_document: lsp::TextDocumentIdentifier::new(
+                            .notify::<lsp::notification::DidCloseTextDocument>(
+                                &lsp::DidCloseTextDocumentParams {
+                                    text_document: lsp::TextDocumentIdentifier::new(old_uri),
+                                },
+                            )?;
+                        server
+                            .lsp
+                            .notify::<lsp::notification::DidOpenTextDocument>(
+                                &lsp::DidOpenTextDocumentParams {
+                                    text_document: lsp::TextDocumentItem::new(
                                         registered_buffer.uri.clone(),
+                                        registered_buffer.language_id.clone(),
+                                        registered_buffer.snapshot_version,
+                                        registered_buffer.snapshot.text(),
                                     ),
-                                    text: None,
                                 },
                             )?;
                     }
-                    language::BufferEvent::FileHandleChanged
-                    | language::BufferEvent::LanguageChanged => {
-                        let new_language_id = id_for_language(buffer.read(cx).language());
-                        let Ok(new_uri) = uri_for_buffer(&buffer, cx) else {
-                            return Ok(());
-                        };
-                        if new_uri != registered_buffer.uri
-                            || new_language_id != registered_buffer.language_id
-                        {
-                            let old_uri = mem::replace(&mut registered_buffer.uri, new_uri);
-                            registered_buffer.language_id = new_language_id;
-                            server
-                                .lsp
-                                .notify::<lsp::notification::DidCloseTextDocument>(
-                                    &lsp::DidCloseTextDocumentParams {
-                                        text_document: lsp::TextDocumentIdentifier::new(old_uri),
-                                    },
-                                )?;
-                            server
-                                .lsp
-                                .notify::<lsp::notification::DidOpenTextDocument>(
-                                    &lsp::DidOpenTextDocumentParams {
-                                        text_document: lsp::TextDocumentItem::new(
-                                            registered_buffer.uri.clone(),
-                                            registered_buffer.language_id.clone(),
-                                            registered_buffer.snapshot_version,
-                                            registered_buffer.snapshot.text(),
-                                        ),
-                                    },
-                                )?;
-                        }
-                    }
-                    _ => {}
                 }
+                _ => {}
             }
         }
 
@@ -874,17 +836,17 @@ impl Copilot {
     }
 
     fn unregister_buffer(&mut self, buffer: &WeakEntity<Buffer>) {
-        if let Ok(server) = self.server.as_running() {
-            if let Some(buffer) = server.registered_buffers.remove(&buffer.entity_id()) {
-                server
-                    .lsp
-                    .notify::<lsp::notification::DidCloseTextDocument>(
-                        &lsp::DidCloseTextDocumentParams {
-                            text_document: lsp::TextDocumentIdentifier::new(buffer.uri),
-                        },
-                    )
-                    .ok();
-            }
+        if let Ok(server) = self.server.as_running()
+            && let Some(buffer) = server.registered_buffers.remove(&buffer.entity_id())
+        {
+            server
+                .lsp
+                .notify::<lsp::notification::DidCloseTextDocument>(
+                    &lsp::DidCloseTextDocumentParams {
+                        text_document: lsp::TextDocumentIdentifier::new(buffer.uri),
+                    },
+                )
+                .ok();
         }
     }
 
@@ -1047,8 +1009,8 @@ impl Copilot {
             CopilotServer::Error(error) => Status::Error(error.clone()),
             CopilotServer::Running(RunningCopilotServer { sign_in_status, .. }) => {
                 match sign_in_status {
-                    SignInStatus::Authorized { .. } => Status::Authorized,
-                    SignInStatus::Unauthorized { .. } => Status::Unauthorized,
+                    SignInStatus::Authorized => Status::Authorized,
+                    SignInStatus::Unauthorized => Status::Unauthorized,
                     SignInStatus::SigningIn { prompt, .. } => Status::SigningIn {
                         prompt: prompt.clone(),
                     },
@@ -1156,6 +1118,41 @@ fn uri_for_buffer(buffer: &Entity<Buffer>, cx: &App) -> Result<lsp::Url, ()> {
     }
 }
 
+fn notify_did_change_config_to_server(
+    server: &Arc<LanguageServer>,
+    cx: &mut Context<Copilot>,
+) -> std::result::Result<(), anyhow::Error> {
+    let copilot_settings = all_language_settings(None, cx)
+        .edit_predictions
+        .copilot
+        .clone();
+
+    if let Some(copilot_chat) = copilot_chat::CopilotChat::global(cx) {
+        copilot_chat.update(cx, |chat, cx| {
+            chat.set_configuration(
+                copilot_chat::CopilotChatConfiguration {
+                    enterprise_uri: copilot_settings.enterprise_uri.clone(),
+                },
+                cx,
+            );
+        });
+    }
+
+    let settings = json!({
+        "http": {
+            "proxy": copilot_settings.proxy,
+            "proxyStrictSSL": !copilot_settings.proxy_no_verify.unwrap_or(false)
+        },
+        "github-enterprise": {
+            "uri": copilot_settings.enterprise_uri
+        }
+    });
+
+    server.notify::<lsp::notification::DidChangeConfiguration>(&lsp::DidChangeConfigurationParams {
+        settings,
+    })
+}
+
 async fn clear_copilot_dir() {
     remove_matching(paths::copilot_dir(), |_| true).await
 }
@@ -1181,7 +1178,7 @@ async fn get_copilot_lsp(fs: Arc<dyn Fs>, node_runtime: NodeRuntime) -> anyhow::
             PACKAGE_NAME,
             &server_path,
             paths::copilot_dir(),
-            &latest_version,
+            VersionStrategy::Latest(&latest_version),
         )
         .await;
     if should_install {

crates/copilot/src/copilot_chat.rs 🔗

@@ -484,7 +484,7 @@ impl CopilotChat {
         };
 
         if this.oauth_token.is_some() {
-            cx.spawn(async move |this, mut cx| Self::update_models(&this, &mut cx).await)
+            cx.spawn(async move |this, cx| Self::update_models(&this, cx).await)
                 .detach_and_log_err(cx);
         }
 
@@ -863,7 +863,7 @@ mod tests {
               "object": "list"
             }"#;
 
-        let schema: ModelSchema = serde_json::from_str(&json).unwrap();
+        let schema: ModelSchema = serde_json::from_str(json).unwrap();
 
         assert_eq!(schema.data.len(), 2);
         assert_eq!(schema.data[0].id, "gpt-4");

crates/copilot/src/copilot_completion_provider.rs 🔗

@@ -1083,7 +1083,7 @@ mod tests {
         let replace_range_marker: TextRangeMarker = ('<', '>').into();
         let (_, mut marked_ranges) = marked_text_ranges_by(
             marked_string,
-            vec![complete_from_marker.clone(), replace_range_marker.clone()],
+            vec![complete_from_marker, replace_range_marker.clone()],
         );
 
         let replace_range =

crates/crashes/Cargo.toml 🔗

@@ -10,9 +10,15 @@ crash-handler.workspace = true
 log.workspace = true
 minidumper.workspace = true
 paths.workspace = true
+release_channel.workspace = true
 smol.workspace = true
+serde.workspace = true
+serde_json.workspace = true
 workspace-hack.workspace = true
 
+[target.'cfg(target_os = "macos")'.dependencies]
+mach2.workspace = true
+
 [lints]
 workspace = true
 

crates/crashes/src/crashes.rs 🔗

@@ -1,15 +1,20 @@
 use crash_handler::CrashHandler;
 use log::info;
 use minidumper::{Client, LoopAction, MinidumpBinary};
+use release_channel::{RELEASE_CHANNEL, ReleaseChannel};
+use serde::{Deserialize, Serialize};
 
+#[cfg(target_os = "macos")]
+use std::sync::atomic::AtomicU32;
 use std::{
     env,
-    fs::File,
+    fs::{self, File},
     io,
+    panic::Location,
     path::{Path, PathBuf},
     process::{self, Command},
     sync::{
-        OnceLock,
+        Arc, OnceLock,
         atomic::{AtomicBool, Ordering},
     },
     thread,
@@ -17,12 +22,20 @@ use std::{
 };
 
 // set once the crash handler has initialized and the client has connected to it
-pub static CRASH_HANDLER: AtomicBool = AtomicBool::new(false);
+pub static CRASH_HANDLER: OnceLock<Arc<Client>> = OnceLock::new();
 // set when the first minidump request is made to avoid generating duplicate crash reports
 pub static REQUESTED_MINIDUMP: AtomicBool = AtomicBool::new(false);
-const CRASH_HANDLER_TIMEOUT: Duration = Duration::from_secs(60);
+const CRASH_HANDLER_PING_TIMEOUT: Duration = Duration::from_secs(60);
+const CRASH_HANDLER_CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
+
+#[cfg(target_os = "macos")]
+static PANIC_THREAD_ID: AtomicU32 = AtomicU32::new(0);
+
+pub async fn init(crash_init: InitCrashHandler) {
+    if *RELEASE_CHANNEL == ReleaseChannel::Dev && env::var("ZED_GENERATE_MINIDUMPS").is_err() {
+        return;
+    }
 
-pub async fn init(id: String) {
     let exe = env::current_exe().expect("unable to find ourselves");
     let zed_pid = process::id();
     // TODO: we should be able to get away with using 1 crash-handler process per machine,
@@ -53,9 +66,11 @@ pub async fn init(id: String) {
         smol::Timer::after(retry_frequency).await;
     }
     let client = maybe_client.unwrap();
-    client.send_message(1, id).unwrap(); // set session id on the server
+    client
+        .send_message(1, serde_json::to_vec(&crash_init).unwrap())
+        .unwrap();
 
-    let client = std::sync::Arc::new(client);
+    let client = Arc::new(client);
     let handler = crash_handler::CrashHandler::attach(unsafe {
         let client = client.clone();
         crash_handler::make_crash_event(move |crash_context: &crash_handler::CrashContext| {
@@ -64,7 +79,9 @@ pub async fn init(id: String) {
                 .compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed)
                 .is_ok()
             {
-                client.send_message(2, "mistakes were made").unwrap();
+                #[cfg(target_os = "macos")]
+                suspend_all_other_threads();
+
                 client.ping().unwrap();
                 client.request_dump(crash_context).is_ok()
             } else {
@@ -79,7 +96,7 @@ pub async fn init(id: String) {
     {
         handler.set_ptracer(Some(server_pid));
     }
-    CRASH_HANDLER.store(true, Ordering::Release);
+    CRASH_HANDLER.set(client.clone()).ok();
     std::mem::forget(handler);
     info!("crash handler registered");
 
@@ -89,61 +106,151 @@ pub async fn init(id: String) {
     }
 }
 
+#[cfg(target_os = "macos")]
+unsafe fn suspend_all_other_threads() {
+    let task = unsafe { mach2::traps::current_task() };
+    let mut threads: mach2::mach_types::thread_act_array_t = std::ptr::null_mut();
+    let mut count = 0;
+    unsafe {
+        mach2::task::task_threads(task, &raw mut threads, &raw mut count);
+    }
+    let current = unsafe { mach2::mach_init::mach_thread_self() };
+    let panic_thread = PANIC_THREAD_ID.load(Ordering::SeqCst);
+    for i in 0..count {
+        let t = unsafe { *threads.add(i as usize) };
+        if t != current && t != panic_thread {
+            unsafe { mach2::thread_act::thread_suspend(t) };
+        }
+    }
+}
+
 pub struct CrashServer {
-    session_id: OnceLock<String>,
+    initialization_params: OnceLock<InitCrashHandler>,
+    panic_info: OnceLock<CrashPanic>,
+    has_connection: Arc<AtomicBool>,
+}
+
+#[derive(Debug, Deserialize, Serialize, Clone)]
+pub struct CrashInfo {
+    pub init: InitCrashHandler,
+    pub panic: Option<CrashPanic>,
+    pub minidump_error: Option<String>,
+}
+
+#[derive(Debug, Deserialize, Serialize, Clone)]
+pub struct InitCrashHandler {
+    pub session_id: String,
+    pub zed_version: String,
+    pub release_channel: String,
+    pub commit_sha: String,
+    // pub gpu: String,
+}
+
+#[derive(Deserialize, Serialize, Debug, Clone)]
+pub struct CrashPanic {
+    pub message: String,
+    pub span: String,
 }
 
 impl minidumper::ServerHandler for CrashServer {
     fn create_minidump_file(&self) -> Result<(File, PathBuf), io::Error> {
-        let err_message = "Need to send a message with the ID upon starting the crash handler";
+        let err_message = "Missing initialization data";
         let dump_path = paths::logs_dir()
-            .join(self.session_id.get().expect(err_message))
+            .join(
+                &self
+                    .initialization_params
+                    .get()
+                    .expect(err_message)
+                    .session_id,
+            )
             .with_extension("dmp");
         let file = File::create(&dump_path)?;
         Ok((file, dump_path))
     }
 
     fn on_minidump_created(&self, result: Result<MinidumpBinary, minidumper::Error>) -> LoopAction {
-        match result {
+        let minidump_error = match result {
             Ok(mut md_bin) => {
                 use io::Write;
                 let _ = md_bin.file.flush();
-                info!("wrote minidump to disk {:?}", md_bin.path);
-            }
-            Err(e) => {
-                info!("failed to write minidump: {:#}", e);
+                None
             }
-        }
+            Err(e) => Some(format!("{e:?}")),
+        };
+
+        let crash_info = CrashInfo {
+            init: self
+                .initialization_params
+                .get()
+                .expect("not initialized")
+                .clone(),
+            panic: self.panic_info.get().cloned(),
+            minidump_error,
+        };
+
+        let crash_data_path = paths::logs_dir()
+            .join(&crash_info.init.session_id)
+            .with_extension("json");
+
+        fs::write(crash_data_path, serde_json::to_vec(&crash_info).unwrap()).ok();
+
         LoopAction::Exit
     }
 
     fn on_message(&self, kind: u32, buffer: Vec<u8>) {
-        let message = String::from_utf8(buffer).expect("invalid utf-8");
-        info!("kind: {kind}, message: {message}",);
-        if kind == 1 {
-            self.session_id
-                .set(message)
-                .expect("session id already initialized");
+        match kind {
+            1 => {
+                let init_data =
+                    serde_json::from_slice::<InitCrashHandler>(&buffer).expect("invalid init data");
+                self.initialization_params
+                    .set(init_data)
+                    .expect("already initialized");
+            }
+            2 => {
+                let panic_data =
+                    serde_json::from_slice::<CrashPanic>(&buffer).expect("invalid panic data");
+                self.panic_info.set(panic_data).expect("already panicked");
+            }
+            _ => {
+                panic!("invalid message kind");
+            }
         }
     }
 
-    fn on_client_disconnected(&self, clients: usize) -> LoopAction {
-        info!("client disconnected, {clients} remaining");
-        if clients == 0 {
-            LoopAction::Exit
-        } else {
-            LoopAction::Continue
-        }
+    fn on_client_disconnected(&self, _clients: usize) -> LoopAction {
+        LoopAction::Exit
+    }
+
+    fn on_client_connected(&self, _clients: usize) -> LoopAction {
+        self.has_connection.store(true, Ordering::SeqCst);
+        LoopAction::Continue
     }
 }
 
-pub fn handle_panic() {
+pub fn handle_panic(message: String, span: Option<&Location>) {
+    let span = span
+        .map(|loc| format!("{}:{}", loc.file(), loc.line()))
+        .unwrap_or_default();
+
     // wait 500ms for the crash handler process to start up
     // if it's still not there just write panic info and no minidump
     let retry_frequency = Duration::from_millis(100);
     for _ in 0..5 {
-        if CRASH_HANDLER.load(Ordering::Acquire) {
+        if let Some(client) = CRASH_HANDLER.get() {
+            client
+                .send_message(
+                    2,
+                    serde_json::to_vec(&CrashPanic { message, span }).unwrap(),
+                )
+                .ok();
             log::error!("triggering a crash to generate a minidump...");
+
+            #[cfg(target_os = "macos")]
+            PANIC_THREAD_ID.store(
+                unsafe { mach2::mach_init::mach_thread_self() },
+                Ordering::SeqCst,
+            );
+
             #[cfg(target_os = "linux")]
             CrashHandler.simulate_signal(crash_handler::Signal::Trap as u32);
             #[cfg(not(target_os = "linux"))]
@@ -159,14 +266,30 @@ pub fn crash_server(socket: &Path) {
         log::info!("Couldn't create socket, there may already be a running crash server");
         return;
     };
-    let ab = AtomicBool::new(false);
+
+    let shutdown = Arc::new(AtomicBool::new(false));
+    let has_connection = Arc::new(AtomicBool::new(false));
+
+    std::thread::spawn({
+        let shutdown = shutdown.clone();
+        let has_connection = has_connection.clone();
+        move || {
+            std::thread::sleep(CRASH_HANDLER_CONNECT_TIMEOUT);
+            if !has_connection.load(Ordering::SeqCst) {
+                shutdown.store(true, Ordering::SeqCst);
+            }
+        }
+    });
+
     server
         .run(
             Box::new(CrashServer {
-                session_id: OnceLock::new(),
+                initialization_params: OnceLock::new(),
+                panic_info: OnceLock::new(),
+                has_connection,
             }),
-            &ab,
-            Some(CRASH_HANDLER_TIMEOUT),
+            &shutdown,
+            Some(CRASH_HANDLER_PING_TIMEOUT),
         )
         .expect("failed to run server");
 }

crates/credentials_provider/src/credentials_provider.rs 🔗

@@ -19,7 +19,7 @@ use release_channel::ReleaseChannel;
 /// Only works in development. Setting this environment variable in other
 /// release channels is a no-op.
 static ZED_DEVELOPMENT_USE_KEYCHAIN: LazyLock<bool> = LazyLock::new(|| {
-    std::env::var("ZED_DEVELOPMENT_USE_KEYCHAIN").map_or(false, |value| !value.is_empty())
+    std::env::var("ZED_DEVELOPMENT_USE_KEYCHAIN").is_ok_and(|value| !value.is_empty())
 });
 
 /// A provider for credentials.

crates/dap/src/adapters.rs 🔗

@@ -285,7 +285,7 @@ pub async fn download_adapter_from_github(
     }
 
     if !adapter_path.exists() {
-        fs.create_dir(&adapter_path.as_path())
+        fs.create_dir(adapter_path.as_path())
             .await
             .context("Failed creating adapter path")?;
     }

crates/dap/src/client.rs 🔗

@@ -23,7 +23,7 @@ impl SessionId {
         Self(client_id as u32)
     }
 
-    pub fn to_proto(&self) -> u64 {
+    pub fn to_proto(self) -> u64 {
         self.0 as u64
     }
 }

crates/dap_adapters/src/codelldb.rs 🔗

@@ -338,8 +338,8 @@ impl DebugAdapter for CodeLldbDebugAdapter {
         if command.is_none() {
             delegate.output_to_console(format!("Checking latest version of {}...", self.name()));
             let adapter_path = paths::debug_adapters_dir().join(&Self::ADAPTER_NAME);
-            let version_path =
-                if let Ok(version) = self.fetch_latest_adapter_version(delegate).await {
+            let version_path = match self.fetch_latest_adapter_version(delegate).await {
+                Ok(version) => {
                     adapters::download_adapter_from_github(
                         self.name(),
                         version.clone(),
@@ -351,10 +351,26 @@ impl DebugAdapter for CodeLldbDebugAdapter {
                         adapter_path.join(format!("{}_{}", Self::ADAPTER_NAME, version.tag_name));
                     remove_matching(&adapter_path, |entry| entry != version_path).await;
                     version_path
-                } else {
-                    let mut paths = delegate.fs().read_dir(&adapter_path).await?;
-                    paths.next().await.context("No adapter found")??
-                };
+                }
+                Err(e) => {
+                    delegate.output_to_console("Unable to fetch latest version".to_string());
+                    log::error!("Error fetching latest version of {}: {}", self.name(), e);
+                    delegate.output_to_console(format!(
+                        "Searching for adapters in: {}",
+                        adapter_path.display()
+                    ));
+                    let mut paths = delegate
+                        .fs()
+                        .read_dir(&adapter_path)
+                        .await
+                        .context("No cached adapter directory")?;
+                    paths
+                        .next()
+                        .await
+                        .context("No cached adapter found")?
+                        .context("No cached adapter found")?
+                }
+            };
             let adapter_dir = version_path.join("extension").join("adapter");
             let path = adapter_dir.join("codelldb").to_string_lossy().to_string();
             self.path_to_codelldb.set(path.clone()).ok();
@@ -369,7 +385,7 @@ impl DebugAdapter for CodeLldbDebugAdapter {
                     && let Some(source_languages) = config.get("sourceLanguages").filter(|value| {
                         value
                             .as_array()
-                            .map_or(false, |array| array.iter().all(Value::is_string))
+                            .is_some_and(|array| array.iter().all(Value::is_string))
                     })
                 {
                     let ret = vec![

crates/dap_adapters/src/go.rs 🔗

@@ -36,7 +36,7 @@ impl GoDebugAdapter {
         delegate: &Arc<dyn DapDelegate>,
     ) -> Result<AdapterVersion> {
         let release = latest_github_release(
-            &"zed-industries/delve-shim-dap",
+            "zed-industries/delve-shim-dap",
             true,
             false,
             delegate.http_client(),

crates/dap_adapters/src/javascript.rs 🔗

@@ -99,10 +99,10 @@ impl JsDebugAdapter {
                 }
             }
 
-            if let Some(env) = configuration.get("env").cloned() {
-                if let Ok(env) = serde_json::from_value(env) {
-                    envs = env;
-                }
+            if let Some(env) = configuration.get("env").cloned()
+                && let Ok(env) = serde_json::from_value(env)
+            {
+                envs = env;
             }
 
             configuration
@@ -514,7 +514,7 @@ impl DebugAdapter for JsDebugAdapter {
             }
         }
 
-        self.get_installed_binary(delegate, &config, user_installed_path, user_args, cx)
+        self.get_installed_binary(delegate, config, user_installed_path, user_args, cx)
             .await
     }
 

crates/dap_adapters/src/python.rs 🔗

@@ -24,6 +24,7 @@ use util::{ResultExt, maybe};
 
 #[derive(Default)]
 pub(crate) struct PythonDebugAdapter {
+    base_venv_path: OnceCell<Result<Arc<Path>, String>>,
     debugpy_whl_base_path: OnceCell<Result<Arc<Path>, String>>,
 }
 
@@ -91,14 +92,12 @@ impl PythonDebugAdapter {
         })
     }
 
-    async fn fetch_wheel(delegate: &Arc<dyn DapDelegate>) -> Result<Arc<Path>, String> {
-        let system_python = Self::system_python_name(delegate)
-            .await
-            .ok_or_else(|| String::from("Could not find a Python installation"))?;
-        let command: &OsStr = system_python.as_ref();
+    async fn fetch_wheel(&self, delegate: &Arc<dyn DapDelegate>) -> Result<Arc<Path>, String> {
         let download_dir = debug_adapters_dir().join(Self::ADAPTER_NAME).join("wheels");
         std::fs::create_dir_all(&download_dir).map_err(|e| e.to_string())?;
-        let installation_succeeded = util::command::new_smol_command(command)
+        let system_python = self.base_venv_path(delegate).await?;
+
+        let installation_succeeded = util::command::new_smol_command(system_python.as_ref())
             .args([
                 "-m",
                 "pip",
@@ -114,7 +113,7 @@ impl PythonDebugAdapter {
             .status
             .success();
         if !installation_succeeded {
-            return Err("debugpy installation failed".into());
+            return Err("debugpy installation failed (could not fetch Debugpy's wheel)".into());
         }
 
         let wheel_path = std::fs::read_dir(&download_dir)
@@ -139,7 +138,7 @@ impl PythonDebugAdapter {
         Ok(Arc::from(wheel_path.path()))
     }
 
-    async fn maybe_fetch_new_wheel(delegate: &Arc<dyn DapDelegate>) {
+    async fn maybe_fetch_new_wheel(&self, delegate: &Arc<dyn DapDelegate>) {
         let latest_release = delegate
             .http_client()
             .get(
@@ -152,6 +151,9 @@ impl PythonDebugAdapter {
         maybe!(async move {
             let response = latest_release.filter(|response| response.status().is_success())?;
 
+            let download_dir = debug_adapters_dir().join(Self::ADAPTER_NAME);
+            std::fs::create_dir_all(&download_dir).ok()?;
+
             let mut output = String::new();
             response
                 .into_body()
@@ -188,7 +190,7 @@ impl PythonDebugAdapter {
                     )
                     .await
                     .ok()?;
-                Self::fetch_wheel(delegate).await.ok()?;
+                self.fetch_wheel(delegate).await.ok()?;
             }
             Some(())
         })
@@ -201,7 +203,7 @@ impl PythonDebugAdapter {
     ) -> Result<Arc<Path>, String> {
         self.debugpy_whl_base_path
             .get_or_init(|| async move {
-                Self::maybe_fetch_new_wheel(delegate).await;
+                self.maybe_fetch_new_wheel(delegate).await;
                 Ok(Arc::from(
                     debug_adapters_dir()
                         .join(Self::ADAPTER_NAME)
@@ -214,6 +216,45 @@ impl PythonDebugAdapter {
             .clone()
     }
 
+    async fn base_venv_path(&self, delegate: &Arc<dyn DapDelegate>) -> Result<Arc<Path>, String> {
+        self.base_venv_path
+            .get_or_init(|| async {
+                let base_python = Self::system_python_name(delegate)
+                    .await
+                    .ok_or_else(|| String::from("Could not find a Python installation"))?;
+
+                let did_succeed = util::command::new_smol_command(base_python)
+                    .args(["-m", "venv", "zed_base_venv"])
+                    .current_dir(
+                        paths::debug_adapters_dir().join(Self::DEBUG_ADAPTER_NAME.as_ref()),
+                    )
+                    .spawn()
+                    .map_err(|e| format!("{e:#?}"))?
+                    .status()
+                    .await
+                    .map_err(|e| format!("{e:#?}"))?
+                    .success();
+                if !did_succeed {
+                    return Err("Failed to create base virtual environment".into());
+                }
+
+                const DIR: &str = if cfg!(target_os = "windows") {
+                    "Scripts"
+                } else {
+                    "bin"
+                };
+                Ok(Arc::from(
+                    paths::debug_adapters_dir()
+                        .join(Self::DEBUG_ADAPTER_NAME.as_ref())
+                        .join("zed_base_venv")
+                        .join(DIR)
+                        .join("python3")
+                        .as_ref(),
+                ))
+            })
+            .await
+            .clone()
+    }
     async fn system_python_name(delegate: &Arc<dyn DapDelegate>) -> Option<String> {
         const BINARY_NAMES: [&str; 3] = ["python3", "python", "py"];
         let mut name = None;
@@ -676,7 +717,7 @@ 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, None)
                 .await;
         }
 
@@ -713,7 +754,7 @@ impl DebugAdapter for PythonDebugAdapter {
             return self
                 .get_installed_binary(
                     delegate,
-                    &config,
+                    config,
                     None,
                     user_args,
                     Some(toolchain.path.to_string()),
@@ -721,7 +762,7 @@ impl DebugAdapter for PythonDebugAdapter {
                 .await;
         }
 
-        self.get_installed_binary(delegate, &config, None, user_args, None)
+        self.get_installed_binary(delegate, config, None, user_args, None)
             .await
     }
 

crates/db/src/db.rs 🔗

@@ -37,7 +37,7 @@ const FALLBACK_DB_NAME: &str = "FALLBACK_MEMORY_DB";
 const DB_FILE_NAME: &str = "db.sqlite";
 
 pub static ZED_STATELESS: LazyLock<bool> =
-    LazyLock::new(|| env::var("ZED_STATELESS").map_or(false, |v| !v.is_empty()));
+    LazyLock::new(|| env::var("ZED_STATELESS").is_ok_and(|v| !v.is_empty()));
 
 pub static ALL_FILE_DB_FAILED: LazyLock<AtomicBool> = LazyLock::new(|| AtomicBool::new(false));
 
@@ -74,7 +74,7 @@ pub async fn open_db<M: Migrator + 'static>(db_dir: &Path, scope: &str) -> Threa
 }
 
 async fn open_main_db<M: Migrator>(db_path: &Path) -> Option<ThreadSafeConnection> {
-    log::info!("Opening database {}", db_path.display());
+    log::trace!("Opening database {}", db_path.display());
     ThreadSafeConnection::builder::<M>(db_path.to_string_lossy().as_ref(), true)
         .with_db_initialization_query(DB_INITIALIZE_QUERY)
         .with_connection_initialize_query(CONNECTION_INITIALIZE_QUERY)
@@ -238,7 +238,7 @@ mod tests {
             .unwrap();
         let _bad_db = open_db::<BadDB>(
             tempdir.path(),
-            &release_channel::ReleaseChannel::Dev.dev_name(),
+            release_channel::ReleaseChannel::Dev.dev_name(),
         )
         .await;
     }
@@ -279,7 +279,7 @@ mod tests {
         {
             let corrupt_db = open_db::<CorruptedDB>(
                 tempdir.path(),
-                &release_channel::ReleaseChannel::Dev.dev_name(),
+                release_channel::ReleaseChannel::Dev.dev_name(),
             )
             .await;
             assert!(corrupt_db.persistent());
@@ -287,7 +287,7 @@ mod tests {
 
         let good_db = open_db::<GoodDB>(
             tempdir.path(),
-            &release_channel::ReleaseChannel::Dev.dev_name(),
+            release_channel::ReleaseChannel::Dev.dev_name(),
         )
         .await;
         assert!(
@@ -334,7 +334,7 @@ mod tests {
             // Setup the bad database
             let corrupt_db = open_db::<CorruptedDB>(
                 tempdir.path(),
-                &release_channel::ReleaseChannel::Dev.dev_name(),
+                release_channel::ReleaseChannel::Dev.dev_name(),
             )
             .await;
             assert!(corrupt_db.persistent());
@@ -347,7 +347,7 @@ mod tests {
             let guard = thread::spawn(move || {
                 let good_db = smol::block_on(open_db::<GoodDB>(
                     tmp_path.as_path(),
-                    &release_channel::ReleaseChannel::Dev.dev_name(),
+                    release_channel::ReleaseChannel::Dev.dev_name(),
                 ));
                 assert!(
                     good_db.select_row::<usize>("SELECT * FROM test2").unwrap()()

crates/db/src/kvp.rs 🔗

@@ -20,7 +20,7 @@ pub trait Dismissable {
         KEY_VALUE_STORE
             .read_kvp(Self::KEY)
             .log_err()
-            .map_or(false, |s| s.is_some())
+            .is_some_and(|s| s.is_some())
     }
 
     fn set_dismissed(is_dismissed: bool, cx: &mut App) {

crates/debugger_tools/src/dap_log.rs 🔗

@@ -392,7 +392,7 @@ impl LogStore {
                         session.label(),
                         session
                             .adapter_client()
-                            .map_or(false, |client| client.has_adapter_logs()),
+                            .is_some_and(|client| client.has_adapter_logs()),
                     )
                 });
 
@@ -485,7 +485,7 @@ impl LogStore {
         &mut self,
         id: &LogStoreEntryIdentifier<'_>,
     ) -> Option<&Vec<SharedString>> {
-        self.get_debug_adapter_state(&id)
+        self.get_debug_adapter_state(id)
             .map(|state| &state.rpc_messages.initialization_sequence)
     }
 }
@@ -536,11 +536,11 @@ impl Render for DapLogToolbarItemView {
                     })
                     .unwrap_or_else(|| "No adapter selected".into()),
             ))
-            .menu(move |mut window, cx| {
+            .menu(move |window, cx| {
                 let log_view = log_view.clone();
                 let menu_rows = menu_rows.clone();
                 let project = project.clone();
-                ContextMenu::build(&mut window, cx, move |mut menu, window, _cx| {
+                ContextMenu::build(window, cx, move |mut menu, window, _cx| {
                     for row in menu_rows.into_iter() {
                         menu = menu.custom_row(move |_window, _cx| {
                             div()
@@ -661,11 +661,11 @@ impl ToolbarItemView for DapLogToolbarItemView {
         _window: &mut Window,
         cx: &mut Context<Self>,
     ) -> workspace::ToolbarItemLocation {
-        if let Some(item) = active_pane_item {
-            if let Some(log_view) = item.downcast::<DapLogView>() {
-                self.log_view = Some(log_view.clone());
-                return workspace::ToolbarItemLocation::PrimaryLeft;
-            }
+        if let Some(item) = active_pane_item
+            && let Some(log_view) = item.downcast::<DapLogView>()
+        {
+            self.log_view = Some(log_view);
+            return workspace::ToolbarItemLocation::PrimaryLeft;
         }
         self.log_view = None;
 
@@ -1131,7 +1131,7 @@ impl LogStore {
         project: &WeakEntity<Project>,
         session_id: SessionId,
     ) -> Vec<SharedString> {
-        self.projects.get(&project).map_or(vec![], |state| {
+        self.projects.get(project).map_or(vec![], |state| {
             state
                 .debug_sessions
                 .get(&session_id)

crates/debugger_ui/src/debugger_panel.rs 🔗

@@ -36,7 +36,7 @@ use settings::Settings;
 use std::sync::{Arc, LazyLock};
 use task::{DebugScenario, TaskContext};
 use tree_sitter::{Query, StreamingIterator as _};
-use ui::{ContextMenu, Divider, PopoverMenuHandle, Tooltip, prelude::*};
+use ui::{ContextMenu, Divider, PopoverMenuHandle, Tab, Tooltip, prelude::*};
 use util::{ResultExt, debug_panic, maybe};
 use workspace::SplitDirection;
 use workspace::item::SaveOptions;
@@ -257,7 +257,7 @@ impl DebugPanel {
                                         .as_ref()
                                         .map(|entity| entity.downgrade()),
                                     task_context: task_context.clone(),
-                                    worktree_id: worktree_id,
+                                    worktree_id,
                                 });
                             };
                             running.resolve_scenario(
@@ -386,10 +386,10 @@ impl DebugPanel {
             return;
         };
 
-        let dap_store_handle = self.project.read(cx).dap_store().clone();
+        let dap_store_handle = self.project.read(cx).dap_store();
         let label = curr_session.read(cx).label();
         let quirks = curr_session.read(cx).quirks();
-        let adapter = curr_session.read(cx).adapter().clone();
+        let adapter = curr_session.read(cx).adapter();
         let binary = curr_session.read(cx).binary().cloned().unwrap();
         let task_context = curr_session.read(cx).task_context().clone();
 
@@ -447,9 +447,9 @@ impl DebugPanel {
             return;
         };
 
-        let dap_store_handle = self.project.read(cx).dap_store().clone();
+        let dap_store_handle = self.project.read(cx).dap_store();
         let label = self.label_for_child_session(&parent_session, request, cx);
-        let adapter = parent_session.read(cx).adapter().clone();
+        let adapter = parent_session.read(cx).adapter();
         let quirks = parent_session.read(cx).quirks();
         let Some(mut binary) = parent_session.read(cx).binary().cloned() else {
             log::error!("Attempted to start a child-session without a binary");
@@ -530,10 +530,9 @@ impl DebugPanel {
                     .active_session
                     .as_ref()
                     .map(|session| session.entity_id())
+                    && active_session_id == entity_id
                 {
-                    if active_session_id == entity_id {
-                        this.active_session = this.sessions_with_children.keys().next().cloned();
-                    }
+                    this.active_session = this.sessions_with_children.keys().next().cloned();
                 }
                 cx.notify()
             })
@@ -642,14 +641,16 @@ impl DebugPanel {
                     }
                 })
         };
+
         let documentation_button = || {
             IconButton::new("debug-open-documentation", IconName::CircleHelp)
                 .icon_size(IconSize::Small)
                 .on_click(move |_, _, cx| cx.open_url("https://zed.dev/docs/debugger"))
                 .tooltip(Tooltip::text("Open Documentation"))
         };
+
         let logs_button = || {
-            IconButton::new("debug-open-logs", IconName::ScrollText)
+            IconButton::new("debug-open-logs", IconName::Notepad)
                 .icon_size(IconSize::Small)
                 .on_click(move |_, window, cx| {
                     window.dispatch_action(debugger_tools::OpenDebugAdapterLogs.boxed_clone(), cx)
@@ -658,16 +659,18 @@ impl DebugPanel {
         };
 
         Some(
-            div.border_b_1()
-                .border_color(cx.theme().colors().border)
-                .p_1()
+            div.w_full()
+                .py_1()
+                .px_1p5()
                 .justify_between()
-                .w_full()
+                .border_b_1()
+                .border_color(cx.theme().colors().border)
                 .when(is_side, |this| this.gap_1())
                 .child(
                     h_flex()
+                        .justify_between()
                         .child(
-                            h_flex().gap_2().w_full().when_some(
+                            h_flex().gap_1().w_full().when_some(
                                 active_session
                                     .as_ref()
                                     .map(|session| session.read(cx).running_state()),
@@ -679,6 +682,7 @@ impl DebugPanel {
                                     let capabilities = running_state.read(cx).capabilities(cx);
                                     let supports_detach =
                                         running_state.read(cx).session().read(cx).is_attached();
+
                                     this.map(|this| {
                                         if thread_status == ThreadStatus::Running {
                                             this.child(
@@ -686,10 +690,9 @@ impl DebugPanel {
                                                     "debug-pause",
                                                     IconName::DebugPause,
                                                 )
-                                                .icon_size(IconSize::XSmall)
-                                                .shape(ui::IconButtonShape::Square)
+                                                .icon_size(IconSize::Small)
                                                 .on_click(window.listener_for(
-                                                    &running_state,
+                                                    running_state,
                                                     |this, _, _window, cx| {
                                                         this.pause_thread(cx);
                                                     },
@@ -698,7 +701,7 @@ impl DebugPanel {
                                                     let focus_handle = focus_handle.clone();
                                                     move |window, cx| {
                                                         Tooltip::for_action_in(
-                                                            "Pause program",
+                                                            "Pause Program",
                                                             &Pause,
                                                             &focus_handle,
                                                             window,
@@ -713,10 +716,9 @@ impl DebugPanel {
                                                     "debug-continue",
                                                     IconName::DebugContinue,
                                                 )
-                                                .icon_size(IconSize::XSmall)
-                                                .shape(ui::IconButtonShape::Square)
+                                                .icon_size(IconSize::Small)
                                                 .on_click(window.listener_for(
-                                                    &running_state,
+                                                    running_state,
                                                     |this, _, _window, cx| this.continue_thread(cx),
                                                 ))
                                                 .disabled(thread_status != ThreadStatus::Stopped)
@@ -724,7 +726,7 @@ impl DebugPanel {
                                                     let focus_handle = focus_handle.clone();
                                                     move |window, cx| {
                                                         Tooltip::for_action_in(
-                                                            "Continue program",
+                                                            "Continue Program",
                                                             &Continue,
                                                             &focus_handle,
                                                             window,
@@ -737,10 +739,9 @@ impl DebugPanel {
                                     })
                                     .child(
                                         IconButton::new("debug-step-over", IconName::ArrowRight)
-                                            .icon_size(IconSize::XSmall)
-                                            .shape(ui::IconButtonShape::Square)
+                                            .icon_size(IconSize::Small)
                                             .on_click(window.listener_for(
-                                                &running_state,
+                                                running_state,
                                                 |this, _, _window, cx| {
                                                     this.step_over(cx);
                                                 },
@@ -750,7 +751,7 @@ impl DebugPanel {
                                                 let focus_handle = focus_handle.clone();
                                                 move |window, cx| {
                                                     Tooltip::for_action_in(
-                                                        "Step over",
+                                                        "Step Over",
                                                         &StepOver,
                                                         &focus_handle,
                                                         window,
@@ -764,10 +765,9 @@ impl DebugPanel {
                                             "debug-step-into",
                                             IconName::ArrowDownRight,
                                         )
-                                        .icon_size(IconSize::XSmall)
-                                        .shape(ui::IconButtonShape::Square)
+                                        .icon_size(IconSize::Small)
                                         .on_click(window.listener_for(
-                                            &running_state,
+                                            running_state,
                                             |this, _, _window, cx| {
                                                 this.step_in(cx);
                                             },
@@ -777,7 +777,7 @@ impl DebugPanel {
                                             let focus_handle = focus_handle.clone();
                                             move |window, cx| {
                                                 Tooltip::for_action_in(
-                                                    "Step in",
+                                                    "Step In",
                                                     &StepInto,
                                                     &focus_handle,
                                                     window,
@@ -788,10 +788,9 @@ impl DebugPanel {
                                     )
                                     .child(
                                         IconButton::new("debug-step-out", IconName::ArrowUpRight)
-                                            .icon_size(IconSize::XSmall)
-                                            .shape(ui::IconButtonShape::Square)
+                                            .icon_size(IconSize::Small)
                                             .on_click(window.listener_for(
-                                                &running_state,
+                                                running_state,
                                                 |this, _, _window, cx| {
                                                     this.step_out(cx);
                                                 },
@@ -801,7 +800,7 @@ impl DebugPanel {
                                                 let focus_handle = focus_handle.clone();
                                                 move |window, cx| {
                                                     Tooltip::for_action_in(
-                                                        "Step out",
+                                                        "Step Out",
                                                         &StepOut,
                                                         &focus_handle,
                                                         window,
@@ -812,10 +811,10 @@ impl DebugPanel {
                                     )
                                     .child(Divider::vertical())
                                     .child(
-                                        IconButton::new("debug-restart", IconName::DebugRestart)
-                                            .icon_size(IconSize::XSmall)
+                                        IconButton::new("debug-restart", IconName::RotateCcw)
+                                            .icon_size(IconSize::Small)
                                             .on_click(window.listener_for(
-                                                &running_state,
+                                                running_state,
                                                 |this, _, window, cx| {
                                                     this.rerun_session(window, cx);
                                                 },
@@ -835,9 +834,9 @@ impl DebugPanel {
                                     )
                                     .child(
                                         IconButton::new("debug-stop", IconName::Power)
-                                            .icon_size(IconSize::XSmall)
+                                            .icon_size(IconSize::Small)
                                             .on_click(window.listener_for(
-                                                &running_state,
+                                                running_state,
                                                 |this, _, _window, cx| {
                                                     if this.session().read(cx).is_building() {
                                                         this.session().update(cx, |session, cx| {
@@ -890,9 +889,9 @@ impl DebugPanel {
                                                     thread_status != ThreadStatus::Stopped
                                                         && thread_status != ThreadStatus::Running,
                                                 )
-                                                .icon_size(IconSize::XSmall)
+                                                .icon_size(IconSize::Small)
                                                 .on_click(window.listener_for(
-                                                    &running_state,
+                                                    running_state,
                                                     |this, _, _, cx| {
                                                         this.detach_client(cx);
                                                     },
@@ -915,7 +914,6 @@ impl DebugPanel {
                                 },
                             ),
                         )
-                        .justify_around()
                         .when(is_side, |this| {
                             this.child(new_session_button())
                                 .child(logs_button())
@@ -924,7 +922,7 @@ impl DebugPanel {
                 )
                 .child(
                     h_flex()
-                        .gap_2()
+                        .gap_0p5()
                         .when(is_side, |this| this.justify_between())
                         .child(
                             h_flex().when_some(
@@ -934,7 +932,6 @@ impl DebugPanel {
                                     .cloned(),
                                 |this, running_state| {
                                     this.children({
-                                        let running_state = running_state.clone();
                                         let threads =
                                             running_state.update(cx, |running_state, cx| {
                                                 let session = running_state.session();
@@ -954,12 +951,15 @@ impl DebugPanel {
                                             )
                                         })
                                     })
-                                    .when(!is_side, |this| this.gap_2().child(Divider::vertical()))
+                                    .when(!is_side, |this| {
+                                        this.gap_0p5().child(Divider::vertical())
+                                    })
                                 },
                             ),
                         )
                         .child(
                             h_flex()
+                                .gap_0p5()
                                 .children(self.render_session_menu(
                                     self.active_session(),
                                     self.running_state(cx),
@@ -1158,7 +1158,7 @@ impl DebugPanel {
                         workspace
                             .project()
                             .read(cx)
-                            .project_path_for_absolute_path(&path, cx)
+                            .project_path_for_absolute_path(path, cx)
                             .context(
                                 "Couldn't get project path for .zed/debug.json in active worktree",
                             )
@@ -1300,10 +1300,10 @@ impl DebugPanel {
         cx: &mut Context<'_, Self>,
     ) -> Option<SharedString> {
         let adapter = parent_session.read(cx).adapter();
-        if let Some(adapter) = DapRegistry::global(cx).adapter(&adapter) {
-            if let Some(label) = adapter.label_for_child_session(request) {
-                return Some(label.into());
-            }
+        if let Some(adapter) = DapRegistry::global(cx).adapter(&adapter)
+            && let Some(label) = adapter.label_for_child_session(request)
+        {
+            return Some(label.into());
         }
         None
     }
@@ -1644,7 +1644,6 @@ impl Render for DebugPanel {
                 }
             })
             .on_action({
-                let this = this.clone();
                 move |_: &ToggleSessionPicker, window, cx| {
                     this.update(cx, |this, cx| {
                         this.toggle_session_picker(window, cx);
@@ -1702,6 +1701,7 @@ impl Render for DebugPanel {
                     this.child(active_session)
                 } else {
                     let docked_to_bottom = self.position(window, cx) == DockPosition::Bottom;
+
                     let welcome_experience = v_flex()
                         .when_else(
                             docked_to_bottom,
@@ -1767,54 +1767,58 @@ impl Render for DebugPanel {
                                 );
                             }),
                         );
-                    let breakpoint_list =
-                        v_flex()
-                            .group("base-breakpoint-list")
-                            .items_start()
-                            .when_else(
-                                docked_to_bottom,
-                                |this| this.min_w_1_3().h_full(),
-                                |this| this.w_full().h_2_3(),
-                            )
-                            .p_1()
-                            .child(
-                                h_flex()
-                                    .pl_1()
-                                    .w_full()
-                                    .justify_between()
-                                    .child(Label::new("Breakpoints").size(LabelSize::Small))
-                                    .child(h_flex().visible_on_hover("base-breakpoint-list").child(
+
+                    let breakpoint_list = v_flex()
+                        .group("base-breakpoint-list")
+                        .when_else(
+                            docked_to_bottom,
+                            |this| this.min_w_1_3().h_full(),
+                            |this| this.size_full().h_2_3(),
+                        )
+                        .child(
+                            h_flex()
+                                .track_focus(&self.breakpoint_list.focus_handle(cx))
+                                .h(Tab::container_height(cx))
+                                .p_1p5()
+                                .w_full()
+                                .justify_between()
+                                .border_b_1()
+                                .border_color(cx.theme().colors().border_variant)
+                                .child(Label::new("Breakpoints").size(LabelSize::Small))
+                                .child(
+                                    h_flex().visible_on_hover("base-breakpoint-list").child(
                                         self.breakpoint_list.read(cx).render_control_strip(),
-                                    ))
-                                    .track_focus(&self.breakpoint_list.focus_handle(cx)),
-                            )
-                            .child(Divider::horizontal())
-                            .child(self.breakpoint_list.clone());
+                                    ),
+                                ),
+                        )
+                        .child(self.breakpoint_list.clone());
+
                     this.child(
                         v_flex()
-                            .h_full()
+                            .size_full()
                             .gap_1()
                             .items_center()
                             .justify_center()
-                            .child(
-                                div()
-                                    .when_else(docked_to_bottom, Div::h_flex, Div::v_flex)
-                                    .size_full()
-                                    .map(|this| {
-                                        if docked_to_bottom {
-                                            this.items_start()
-                                                .child(breakpoint_list)
-                                                .child(Divider::vertical())
-                                                .child(welcome_experience)
-                                                .child(Divider::vertical())
-                                        } else {
-                                            this.items_end()
-                                                .child(welcome_experience)
-                                                .child(Divider::horizontal())
-                                                .child(breakpoint_list)
-                                        }
-                                    }),
-                            ),
+                            .map(|this| {
+                                if docked_to_bottom {
+                                    this.child(
+                                        h_flex()
+                                            .size_full()
+                                            .child(breakpoint_list)
+                                            .child(Divider::vertical())
+                                            .child(welcome_experience)
+                                            .child(Divider::vertical()),
+                                    )
+                                } else {
+                                    this.child(
+                                        v_flex()
+                                            .size_full()
+                                            .child(welcome_experience)
+                                            .child(Divider::horizontal())
+                                            .child(breakpoint_list),
+                                    )
+                                }
+                            }),
                     )
                 }
             })

crates/debugger_ui/src/debugger_ui.rs 🔗

@@ -272,7 +272,6 @@ pub fn init(cx: &mut App) {
                     }
                 })
                 .on_action({
-                    let active_item = active_item.clone();
                     move |_: &ToggleIgnoreBreakpoints, _, cx| {
                         active_item
                             .update(cx, |item, cx| item.toggle_ignore_breakpoints(cx))
@@ -293,9 +292,8 @@ pub fn init(cx: &mut App) {
                     let Some(debug_panel) = workspace.read(cx).panel::<DebugPanel>(cx) else {
                         return;
                     };
-                    let Some(active_session) = debug_panel
-                        .clone()
-                        .update(cx, |panel, _| panel.active_session())
+                    let Some(active_session) =
+                        debug_panel.update(cx, |panel, _| panel.active_session())
                     else {
                         return;
                     };

crates/debugger_ui/src/dropdown_menus.rs 🔗

@@ -272,10 +272,9 @@ impl DebugPanel {
             .child(session_entry.label_element(self_depth, cx))
             .child(
                 IconButton::new("close-debug-session", IconName::Close)
-                    .visible_on_hover(id.clone())
+                    .visible_on_hover(id)
                     .icon_size(IconSize::Small)
                     .on_click({
-                        let weak = weak.clone();
                         move |_, window, cx| {
                             weak.update(cx, |panel, cx| {
                                 panel.close_session(session_entity_id, window, cx);

crates/debugger_ui/src/new_process_modal.rs 🔗

@@ -343,10 +343,10 @@ impl NewProcessModal {
             return;
         }
 
-        if let NewProcessMode::Launch = &self.mode {
-            if self.configure_mode.read(cx).save_to_debug_json.selected() {
-                self.save_debug_scenario(window, cx);
-            }
+        if let NewProcessMode::Launch = &self.mode
+            && self.configure_mode.read(cx).save_to_debug_json.selected()
+        {
+            self.save_debug_scenario(window, cx);
         }
 
         let Some(debugger) = self.debugger.clone() else {
@@ -413,7 +413,7 @@ impl NewProcessModal {
         let Some(adapter) = self.debugger.as_ref() else {
             return;
         };
-        let scenario = self.debug_scenario(&adapter, cx);
+        let scenario = self.debug_scenario(adapter, cx);
         cx.spawn_in(window, async move |this, cx| {
             let scenario = scenario.await.context("no scenario to save")?;
             let worktree_id = task_contexts
@@ -659,12 +659,7 @@ impl Render for NewProcessModal {
                             this.mode = NewProcessMode::Attach;
 
                             if let Some(debugger) = this.debugger.as_ref() {
-                                Self::update_attach_picker(
-                                    &this.attach_mode,
-                                    &debugger,
-                                    window,
-                                    cx,
-                                );
+                                Self::update_attach_picker(&this.attach_mode, debugger, window, cx);
                             }
                             this.mode_focus_handle(cx).focus(window);
                             cx.notify();
@@ -790,7 +785,7 @@ impl RenderOnce for AttachMode {
         v_flex()
             .w_full()
             .track_focus(&self.attach_picker.focus_handle(cx))
-            .child(self.attach_picker.clone())
+            .child(self.attach_picker)
     }
 }
 
@@ -1083,7 +1078,7 @@ impl DebugDelegate {
                     .into_iter()
                     .map(|(scenario, context)| {
                         let (kind, scenario) =
-                            Self::get_scenario_kind(&languages, &dap_registry, scenario);
+                            Self::get_scenario_kind(&languages, dap_registry, scenario);
                         (kind, scenario, Some(context))
                     })
                     .chain(
@@ -1100,7 +1095,7 @@ impl DebugDelegate {
                             .filter(|(_, scenario)| valid_adapters.contains(&scenario.adapter))
                             .map(|(kind, scenario)| {
                                 let (language, scenario) =
-                                    Self::get_scenario_kind(&languages, &dap_registry, scenario);
+                                    Self::get_scenario_kind(&languages, dap_registry, scenario);
                                 (language.or(Some(kind)), scenario, None)
                             }),
                     )

crates/debugger_ui/src/onboarding_modal.rs 🔗

@@ -131,7 +131,7 @@ impl Render for DebuggerOnboardingModal {
                     .child(Headline::new("Zed's Debugger").size(HeadlineSize::Large)),
             )
             .child(h_flex().absolute().top_2().right_2().child(
-                IconButton::new("cancel", IconName::X).on_click(cx.listener(
+                IconButton::new("cancel", IconName::Close).on_click(cx.listener(
                     |_, _: &ClickEvent, _window, cx| {
                         debugger_onboarding_event!("Cancelled", trigger = "X click");
                         cx.emit(DismissEvent);

crates/debugger_ui/src/persistence.rs 🔗

@@ -256,7 +256,7 @@ pub(crate) fn deserialize_pane_layout(
             Some(Member::Axis(PaneAxis::load(
                 if should_invert { axis.invert() } else { axis },
                 members,
-                flexes.clone(),
+                flexes,
             )))
         }
         SerializedPaneLayout::Pane(serialized_pane) => {
@@ -341,7 +341,7 @@ impl SerializedPaneLayout {
     pub(crate) fn in_order(&self) -> Vec<SerializedPaneLayout> {
         let mut panes = vec![];
 
-        Self::inner_in_order(&self, &mut panes);
+        Self::inner_in_order(self, &mut panes);
         panes
     }
 

crates/debugger_ui/src/session.rs 🔗

@@ -87,7 +87,7 @@ impl DebugSession {
         self.stack_trace_view.get_or_init(|| {
             let stackframe_list = running_state.read(cx).stack_frame_list().clone();
 
-            let stack_frame_view = cx.new(|cx| {
+            cx.new(|cx| {
                 StackTraceView::new(
                     workspace.clone(),
                     project.clone(),
@@ -95,9 +95,7 @@ impl DebugSession {
                     window,
                     cx,
                 )
-            });
-
-            stack_frame_view
+            })
         })
     }
 

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

@@ -48,10 +48,8 @@ use task::{
 };
 use terminal_view::TerminalView;
 use ui::{
-    ActiveTheme, AnyElement, App, ButtonCommon as _, Clickable as _, Context, FluentBuilder,
-    IconButton, IconName, IconSize, InteractiveElement, IntoElement, Label, LabelCommon as _,
-    ParentElement, Render, SharedString, StatefulInteractiveElement, Styled, Tab, Tooltip,
-    VisibleOnHover, VisualContext, Window, div, h_flex, v_flex,
+    FluentBuilder, IntoElement, Render, StatefulInteractiveElement, Tab, Tooltip, VisibleOnHover,
+    VisualContext, prelude::*,
 };
 use util::ResultExt;
 use variable_list::VariableList;
@@ -104,7 +102,7 @@ impl Render for RunningState {
             .find(|pane| pane.read(cx).is_zoomed());
 
         let active = self.panes.panes().into_iter().next();
-        let pane = if let Some(ref zoomed_pane) = zoomed_pane {
+        let pane = if let Some(zoomed_pane) = zoomed_pane {
             zoomed_pane.update(cx, |pane, cx| pane.render(window, cx).into_any_element())
         } else if let Some(active) = active {
             self.panes
@@ -182,7 +180,7 @@ impl SubView {
         let weak_list = list.downgrade();
         let focus_handle = list.focus_handle(cx);
         let this = Self::new(
-            focus_handle.clone(),
+            focus_handle,
             list.into(),
             DebuggerPaneItem::BreakpointList,
             cx,
@@ -293,7 +291,7 @@ pub(crate) fn new_debugger_pane(
             let Some(project) = project.upgrade() else {
                 return ControlFlow::Break(());
             };
-            let this_pane = cx.entity().clone();
+            let this_pane = cx.entity();
             let item = if tab.pane == this_pane {
                 pane.item_for_index(tab.ix)
             } else {
@@ -360,7 +358,7 @@ pub(crate) fn new_debugger_pane(
         }
     };
 
-    let ret = cx.new(move |cx| {
+    cx.new(move |cx| {
         let mut pane = Pane::new(
             workspace.clone(),
             project.clone(),
@@ -416,19 +414,19 @@ pub(crate) fn new_debugger_pane(
                     .and_then(|item| item.downcast::<SubView>());
                 let is_hovered = as_subview
                     .as_ref()
-                    .map_or(false, |item| item.read(cx).hovered);
+                    .is_some_and(|item| item.read(cx).hovered);
 
                 h_flex()
+                    .track_focus(&focus_handle)
                     .group(pane_group_id.clone())
+                    .pl_1p5()
+                    .pr_1()
                     .justify_between()
-                    .bg(cx.theme().colors().tab_bar_background)
                     .border_b_1()
-                    .px_2()
                     .border_color(cx.theme().colors().border)
-                    .track_focus(&focus_handle)
+                    .bg(cx.theme().colors().tab_bar_background)
                     .on_action(|_: &menu::Cancel, window, cx| {
                         if cx.stop_active_drag(window) {
-                            return;
                         } else {
                             cx.propagate();
                         }
@@ -450,7 +448,7 @@ pub(crate) fn new_debugger_pane(
                             .children(pane.items().enumerate().map(|(ix, item)| {
                                 let selected = active_pane_item
                                     .as_ref()
-                                    .map_or(false, |active| active.item_id() == item.item_id());
+                                    .is_some_and(|active| active.item_id() == item.item_id());
                                 let deemphasized = !pane.has_focus(window, cx);
                                 let item_ = item.boxed_clone();
                                 div()
@@ -503,7 +501,7 @@ pub(crate) fn new_debugger_pane(
                                     .on_drag(
                                         DraggedTab {
                                             item: item.boxed_clone(),
-                                            pane: cx.entity().clone(),
+                                            pane: cx.entity(),
                                             detail: 0,
                                             is_active: selected,
                                             ix,
@@ -514,6 +512,7 @@ pub(crate) fn new_debugger_pane(
                     )
                     .child({
                         let zoomed = pane.is_zoomed();
+
                         h_flex()
                             .visible_on_hover(pane_group_id)
                             .when(is_hovered, |this| this.visible())
@@ -537,7 +536,7 @@ pub(crate) fn new_debugger_pane(
                                         IconName::Maximize
                                     },
                                 )
-                                .icon_size(IconSize::XSmall)
+                                .icon_size(IconSize::Small)
                                 .on_click(cx.listener(move |pane, _, _, cx| {
                                     let is_zoomed = pane.is_zoomed();
                                     pane.set_zoomed(!is_zoomed, cx);
@@ -563,9 +562,7 @@ pub(crate) fn new_debugger_pane(
             }
         });
         pane
-    });
-
-    ret
+    })
 }
 
 pub struct DebugTerminal {
@@ -592,10 +589,11 @@ impl DebugTerminal {
 }
 
 impl gpui::Render for DebugTerminal {
-    fn render(&mut self, _window: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
+    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
         div()
-            .size_full()
             .track_focus(&self.focus_handle)
+            .size_full()
+            .bg(cx.theme().colors().editor_background)
             .children(self.terminal.clone())
     }
 }
@@ -626,7 +624,7 @@ impl RunningState {
                 if s.starts_with("\"$ZED_") && s.ends_with('"') {
                     *s = s[1..s.len() - 1].to_string();
                 }
-                if let Some(substituted) = substitute_variables_in_str(&s, context) {
+                if let Some(substituted) = substitute_variables_in_str(s, context) {
                     *s = substituted;
                 }
             }
@@ -656,7 +654,7 @@ impl RunningState {
                 }
                 resolve_path(s);
 
-                if let Some(substituted) = substitute_variables_in_str(&s, context) {
+                if let Some(substituted) = substitute_variables_in_str(s, context) {
                     *s = substituted;
                 }
             }
@@ -953,7 +951,7 @@ impl RunningState {
                                 inventory.read(cx).task_template_by_label(
                                     buffer,
                                     worktree_id,
-                                    &label,
+                                    label,
                                     cx,
                                 )
                             })
@@ -1014,10 +1012,9 @@ impl RunningState {
                     ..task.resolved.clone()
                 };
                 let terminal = project
-                    .update_in(cx, |project, window, cx| {
+                    .update(cx, |project, cx| {
                         project.create_terminal(
                             TerminalKind::Task(task_with_shell.clone()),
-                            window.window_handle(),
                             cx,
                         )
                     })?
@@ -1116,9 +1113,8 @@ impl RunningState {
         };
         let session = self.session.read(cx);
 
-        let cwd = Some(&request.cwd)
-            .filter(|cwd| cwd.len() > 0)
-            .map(PathBuf::from)
+        let cwd = (!request.cwd.is_empty())
+            .then(|| PathBuf::from(&request.cwd))
             .or_else(|| session.binary().unwrap().cwd.clone());
 
         let mut envs: HashMap<String, String> =
@@ -1153,7 +1149,7 @@ impl RunningState {
             } else {
                 None
             }
-        } else if args.len() > 0 {
+        } else if !args.is_empty() {
             Some(args.remove(0))
         } else {
             None
@@ -1170,9 +1166,9 @@ impl RunningState {
             id: task::TaskId("debug".to_string()),
             full_label: title.clone(),
             label: title.clone(),
-            command: command.clone(),
+            command,
             args,
-            command_label: title.clone(),
+            command_label: title,
             cwd,
             env: envs,
             use_new_terminal: true,
@@ -1189,9 +1185,7 @@ impl RunningState {
         let workspace = self.workspace.clone();
         let weak_project = project.downgrade();
 
-        let terminal_task = project.update(cx, |project, cx| {
-            project.create_terminal(kind, window.window_handle(), cx)
-        });
+        let terminal_task = project.update(cx, |project, cx| project.create_terminal(kind, cx));
         let terminal_task = cx.spawn_in(window, async move |_, cx| {
             let terminal = terminal_task.await?;
 
@@ -1312,7 +1306,7 @@ impl RunningState {
         let mut pane_item_status = IndexMap::from_iter(
             DebuggerPaneItem::all()
                 .iter()
-                .filter(|kind| kind.is_supported(&caps))
+                .filter(|kind| kind.is_supported(caps))
                 .map(|kind| (*kind, false)),
         );
         self.panes.panes().iter().for_each(|pane| {
@@ -1373,7 +1367,7 @@ impl RunningState {
         this.serialize_layout(window, cx);
         match event {
             Event::Remove { .. } => {
-                let _did_find_pane = this.panes.remove(&source_pane).is_ok();
+                let _did_find_pane = this.panes.remove(source_pane).is_ok();
                 debug_assert!(_did_find_pane);
                 cx.notify();
             }
@@ -1761,7 +1755,7 @@ impl RunningState {
             this.activate_item(0, false, false, window, cx);
         });
 
-        let rightmost_pane = new_debugger_pane(workspace.clone(), project.clone(), window, cx);
+        let rightmost_pane = new_debugger_pane(workspace.clone(), project, window, cx);
         rightmost_pane.update(cx, |this, cx| {
             this.add_item(
                 Box::new(SubView::new(

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

@@ -23,11 +23,8 @@ use project::{
     worktree_store::WorktreeStore,
 };
 use ui::{
-    ActiveTheme, AnyElement, App, ButtonCommon, Clickable, Color, Context, Disableable, Div,
-    Divider, FluentBuilder as _, Icon, IconButton, IconName, IconSize, InteractiveElement,
-    IntoElement, Label, LabelCommon, LabelSize, ListItem, ParentElement, Render, RenderOnce,
-    Scrollbar, ScrollbarState, SharedString, StatefulInteractiveElement, Styled, Toggleable,
-    Tooltip, Window, div, h_flex, px, v_flex,
+    Divider, DividerColor, FluentBuilder as _, Indicator, IntoElement, ListItem, Render, Scrollbar,
+    ScrollbarState, StatefulInteractiveElement, Tooltip, prelude::*,
 };
 use workspace::Workspace;
 use zed_actions::{ToggleEnableBreakpoint, UnsetBreakpoint};
@@ -242,14 +239,12 @@ impl BreakpointList {
     }
 
     fn select_next(&mut self, _: &menu::SelectNext, window: &mut Window, cx: &mut Context<Self>) {
-        if self.strip_mode.is_some() {
-            if self.input.focus_handle(cx).contains_focused(window, cx) {
-                cx.propagate();
-                return;
-            }
+        if self.strip_mode.is_some() && self.input.focus_handle(cx).contains_focused(window, cx) {
+            cx.propagate();
+            return;
         }
         let ix = match self.selected_ix {
-            _ if self.breakpoints.len() == 0 => None,
+            _ if self.breakpoints.is_empty() => None,
             None => Some(0),
             Some(ix) => {
                 if ix == self.breakpoints.len() - 1 {
@@ -268,14 +263,12 @@ impl BreakpointList {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        if self.strip_mode.is_some() {
-            if self.input.focus_handle(cx).contains_focused(window, cx) {
-                cx.propagate();
-                return;
-            }
+        if self.strip_mode.is_some() && self.input.focus_handle(cx).contains_focused(window, cx) {
+            cx.propagate();
+            return;
         }
         let ix = match self.selected_ix {
-            _ if self.breakpoints.len() == 0 => None,
+            _ if self.breakpoints.is_empty() => None,
             None => Some(self.breakpoints.len() - 1),
             Some(ix) => {
                 if ix == 0 {
@@ -289,13 +282,11 @@ impl BreakpointList {
     }
 
     fn select_first(&mut self, _: &menu::SelectFirst, window: &mut Window, cx: &mut Context<Self>) {
-        if self.strip_mode.is_some() {
-            if self.input.focus_handle(cx).contains_focused(window, cx) {
-                cx.propagate();
-                return;
-            }
+        if self.strip_mode.is_some() && self.input.focus_handle(cx).contains_focused(window, cx) {
+            cx.propagate();
+            return;
         }
-        let ix = if self.breakpoints.len() > 0 {
+        let ix = if !self.breakpoints.is_empty() {
             Some(0)
         } else {
             None
@@ -304,13 +295,11 @@ impl BreakpointList {
     }
 
     fn select_last(&mut self, _: &menu::SelectLast, window: &mut Window, cx: &mut Context<Self>) {
-        if self.strip_mode.is_some() {
-            if self.input.focus_handle(cx).contains_focused(window, cx) {
-                cx.propagate();
-                return;
-            }
+        if self.strip_mode.is_some() && self.input.focus_handle(cx).contains_focused(window, cx) {
+            cx.propagate();
+            return;
         }
-        let ix = if self.breakpoints.len() > 0 {
+        let ix = if !self.breakpoints.is_empty() {
             Some(self.breakpoints.len() - 1)
         } else {
             None
@@ -340,8 +329,8 @@ impl BreakpointList {
                 let text = self.input.read(cx).text(cx);
 
                 match mode {
-                    ActiveBreakpointStripMode::Log => match &entry.kind {
-                        BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
+                    ActiveBreakpointStripMode::Log => {
+                        if let BreakpointEntryKind::LineBreakpoint(line_breakpoint) = &entry.kind {
                             Self::edit_line_breakpoint_inner(
                                 &self.breakpoint_store,
                                 line_breakpoint.breakpoint.path.clone(),
@@ -350,10 +339,9 @@ impl BreakpointList {
                                 cx,
                             );
                         }
-                        _ => {}
-                    },
-                    ActiveBreakpointStripMode::Condition => match &entry.kind {
-                        BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
+                    }
+                    ActiveBreakpointStripMode::Condition => {
+                        if let BreakpointEntryKind::LineBreakpoint(line_breakpoint) = &entry.kind {
                             Self::edit_line_breakpoint_inner(
                                 &self.breakpoint_store,
                                 line_breakpoint.breakpoint.path.clone(),
@@ -362,10 +350,9 @@ impl BreakpointList {
                                 cx,
                             );
                         }
-                        _ => {}
-                    },
-                    ActiveBreakpointStripMode::HitCondition => match &entry.kind {
-                        BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
+                    }
+                    ActiveBreakpointStripMode::HitCondition => {
+                        if let BreakpointEntryKind::LineBreakpoint(line_breakpoint) = &entry.kind {
                             Self::edit_line_breakpoint_inner(
                                 &self.breakpoint_store,
                                 line_breakpoint.breakpoint.path.clone(),
@@ -374,8 +361,7 @@ impl BreakpointList {
                                 cx,
                             );
                         }
-                        _ => {}
-                    },
+                    }
                 }
                 self.focus_handle.focus(window);
             } else {
@@ -404,11 +390,9 @@ impl BreakpointList {
         let Some(entry) = self.selected_ix.and_then(|ix| self.breakpoints.get_mut(ix)) else {
             return;
         };
-        if self.strip_mode.is_some() {
-            if self.input.focus_handle(cx).contains_focused(window, cx) {
-                cx.propagate();
-                return;
-            }
+        if self.strip_mode.is_some() && self.input.focus_handle(cx).contains_focused(window, cx) {
+            cx.propagate();
+            return;
         }
 
         match &mut entry.kind {
@@ -439,13 +423,10 @@ impl BreakpointList {
             return;
         };
 
-        match &mut entry.kind {
-            BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
-                let path = line_breakpoint.breakpoint.path.clone();
-                let row = line_breakpoint.breakpoint.row;
-                self.edit_line_breakpoint(path, row, BreakpointEditAction::Toggle, cx);
-            }
-            _ => {}
+        if let BreakpointEntryKind::LineBreakpoint(line_breakpoint) = &mut entry.kind {
+            let path = line_breakpoint.breakpoint.path.clone();
+            let row = line_breakpoint.breakpoint.row;
+            self.edit_line_breakpoint(path, row, BreakpointEditAction::Toggle, cx);
         }
         cx.notify();
     }
@@ -497,7 +478,7 @@ impl BreakpointList {
     fn toggle_data_breakpoint(&mut self, id: &str, cx: &mut Context<Self>) {
         if let Some(session) = &self.session {
             session.update(cx, |this, cx| {
-                this.toggle_data_breakpoint(&id, cx);
+                this.toggle_data_breakpoint(id, cx);
             });
         }
     }
@@ -505,7 +486,7 @@ impl BreakpointList {
     fn toggle_exception_breakpoint(&mut self, id: &str, cx: &mut Context<Self>) {
         if let Some(session) = &self.session {
             session.update(cx, |this, cx| {
-                this.toggle_exception_breakpoint(&id, cx);
+                this.toggle_exception_breakpoint(id, cx);
             });
             cx.notify();
             const EXCEPTION_SERIALIZATION_INTERVAL: Duration = Duration::from_secs(1);
@@ -541,7 +522,7 @@ impl BreakpointList {
             cx.background_executor()
                 .spawn(async move { KEY_VALUE_STORE.write_kvp(key, value?).await })
         } else {
-            return Task::ready(Result::Ok(()));
+            Task::ready(Result::Ok(()))
         }
     }
 
@@ -569,6 +550,7 @@ impl BreakpointList {
             .map(|session| SupportedBreakpointProperties::from(session.read(cx).capabilities()))
             .unwrap_or_else(SupportedBreakpointProperties::empty);
         let strip_mode = self.strip_mode;
+
         uniform_list(
             "breakpoint-list",
             self.breakpoints.len(),
@@ -591,7 +573,7 @@ impl BreakpointList {
             }),
         )
         .track_scroll(self.scroll_handle.clone())
-        .flex_grow()
+        .flex_1()
     }
 
     fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Stateful<Div> {
@@ -630,6 +612,7 @@ impl BreakpointList {
     pub(crate) fn render_control_strip(&self) -> AnyElement {
         let selection_kind = self.selection_kind();
         let focus_handle = self.focus_handle.clone();
+
         let remove_breakpoint_tooltip = selection_kind.map(|(kind, _)| match kind {
             SelectedBreakpointKind::Source => "Remove breakpoint from a breakpoint list",
             SelectedBreakpointKind::Exception => {
@@ -637,6 +620,7 @@ impl BreakpointList {
             }
             SelectedBreakpointKind::Data => "Remove data breakpoint from a breakpoint list",
         });
+
         let toggle_label = selection_kind.map(|(_, is_enabled)| {
             if is_enabled {
                 (
@@ -649,13 +633,12 @@ impl BreakpointList {
         });
 
         h_flex()
-            .gap_2()
             .child(
                 IconButton::new(
                     "disable-breakpoint-breakpoint-list",
                     IconName::DebugDisabledBreakpoint,
                 )
-                .icon_size(IconSize::XSmall)
+                .icon_size(IconSize::Small)
                 .when_some(toggle_label, |this, (label, meta)| {
                     this.tooltip({
                         let focus_handle = focus_handle.clone();
@@ -681,9 +664,8 @@ impl BreakpointList {
                 }),
             )
             .child(
-                IconButton::new("remove-breakpoint-breakpoint-list", IconName::X)
-                    .icon_size(IconSize::XSmall)
-                    .icon_color(ui::Color::Error)
+                IconButton::new("remove-breakpoint-breakpoint-list", IconName::Trash)
+                    .icon_size(IconSize::Small)
                     .when_some(remove_breakpoint_tooltip, |this, tooltip| {
                         this.tooltip({
                             let focus_handle = focus_handle.clone();
@@ -703,14 +685,12 @@ impl BreakpointList {
                         selection_kind.map(|kind| kind.0) != Some(SelectedBreakpointKind::Source),
                     )
                     .on_click({
-                        let focus_handle = focus_handle.clone();
                         move |_, window, cx| {
                             focus_handle.focus(window);
                             window.dispatch_action(UnsetBreakpoint.boxed_clone(), cx)
                         }
                     }),
             )
-            .mr_2()
             .into_any_element()
     }
 }
@@ -791,6 +771,7 @@ impl Render for BreakpointList {
                 .chain(data_breakpoints)
                 .chain(exception_breakpoints),
         );
+
         v_flex()
             .id("breakpoint-list")
             .key_context("BreakpointList")
@@ -806,35 +787,33 @@ impl Render for BreakpointList {
             .on_action(cx.listener(Self::next_breakpoint_property))
             .on_action(cx.listener(Self::previous_breakpoint_property))
             .size_full()
-            .m_0p5()
-            .child(
-                v_flex()
-                    .size_full()
-                    .child(self.render_list(cx))
-                    .child(self.render_vertical_scrollbar(cx)),
-            )
+            .pt_1()
+            .child(self.render_list(cx))
+            .child(self.render_vertical_scrollbar(cx))
             .when_some(self.strip_mode, |this, _| {
-                this.child(Divider::horizontal()).child(
-                    h_flex()
-                        // .w_full()
-                        .m_0p5()
-                        .p_0p5()
-                        .border_1()
-                        .rounded_sm()
-                        .when(
-                            self.input.focus_handle(cx).contains_focused(window, cx),
-                            |this| {
-                                let colors = cx.theme().colors();
-                                let border = if self.input.read(cx).read_only(cx) {
-                                    colors.border_disabled
-                                } else {
-                                    colors.border_focused
-                                };
-                                this.border_color(border)
-                            },
-                        )
-                        .child(self.input.clone()),
-                )
+                this.child(Divider::horizontal().color(DividerColor::Border))
+                    .child(
+                        h_flex()
+                            .p_1()
+                            .rounded_sm()
+                            .bg(cx.theme().colors().editor_background)
+                            .border_1()
+                            .when(
+                                self.input.focus_handle(cx).contains_focused(window, cx),
+                                |this| {
+                                    let colors = cx.theme().colors();
+
+                                    let border_color = if self.input.read(cx).read_only(cx) {
+                                        colors.border_disabled
+                                    } else {
+                                        colors.border_transparent
+                                    };
+
+                                    this.border_color(border_color)
+                                },
+                            )
+                            .child(self.input.clone()),
+                    )
             })
     }
 }
@@ -865,12 +844,17 @@ impl LineBreakpoint {
         let path = self.breakpoint.path.clone();
         let row = self.breakpoint.row;
         let is_enabled = self.breakpoint.state.is_enabled();
+
         let indicator = div()
             .id(SharedString::from(format!(
                 "breakpoint-ui-toggle-{:?}/{}:{}",
                 self.dir, self.name, self.line
             )))
-            .cursor_pointer()
+            .child(
+                Icon::new(icon_name)
+                    .color(Color::Debugger)
+                    .size(IconSize::XSmall),
+            )
             .tooltip({
                 let focus_handle = focus_handle.clone();
                 move |window, cx| {
@@ -902,17 +886,14 @@ impl LineBreakpoint {
                     .ok();
                 }
             })
-            .child(
-                Icon::new(icon_name)
-                    .color(Color::Debugger)
-                    .size(IconSize::XSmall),
-            )
             .on_mouse_down(MouseButton::Left, move |_, _, _| {});
 
         ListItem::new(SharedString::from(format!(
             "breakpoint-ui-item-{:?}/{}:{}",
             self.dir, self.name, self.line
         )))
+        .toggle_state(is_selected)
+        .inset(true)
         .on_click({
             let weak = weak.clone();
             move |_, window, cx| {
@@ -922,23 +903,20 @@ impl LineBreakpoint {
                 .ok();
             }
         })
-        .start_slot(indicator)
-        .rounded()
         .on_secondary_mouse_down(|_, _, cx| {
             cx.stop_propagation();
         })
+        .start_slot(indicator)
         .child(
             h_flex()
-                .w_full()
-                .mr_4()
-                .py_0p5()
-                .gap_1()
-                .min_h(px(26.))
-                .justify_between()
                 .id(SharedString::from(format!(
                     "breakpoint-ui-on-click-go-to-line-{:?}/{}:{}",
                     self.dir, self.name, self.line
                 )))
+                .w_full()
+                .gap_1()
+                .min_h(rems_from_px(26.))
+                .justify_between()
                 .on_click({
                     let weak = weak.clone();
                     move |_, window, cx| {
@@ -949,9 +927,9 @@ impl LineBreakpoint {
                         .ok();
                     }
                 })
-                .cursor_pointer()
                 .child(
                     h_flex()
+                        .id("label-container")
                         .gap_0p5()
                         .child(
                             Label::new(format!("{}:{}", self.name, self.line))
@@ -971,16 +949,18 @@ impl LineBreakpoint {
                                     .line_height_style(ui::LineHeightStyle::UiLabel)
                                     .truncate(),
                             )
-                        })),
+                        }))
+                        .when_some(self.dir.as_ref(), |this, parent_dir| {
+                            this.tooltip(Tooltip::text(format!(
+                                "Worktree parent path: {parent_dir}"
+                            )))
+                        }),
                 )
-                .when_some(self.dir.as_ref(), |this, parent_dir| {
-                    this.tooltip(Tooltip::text(format!("Worktree parent path: {parent_dir}")))
-                })
                 .child(BreakpointOptionsStrip {
                     props,
                     breakpoint: BreakpointEntry {
                         kind: BreakpointEntryKind::LineBreakpoint(self.clone()),
-                        weak: weak,
+                        weak,
                     },
                     is_selected,
                     focus_handle,
@@ -988,15 +968,16 @@ impl LineBreakpoint {
                     index: ix,
                 }),
         )
-        .toggle_state(is_selected)
     }
 }
+
 #[derive(Clone, Debug)]
 struct ExceptionBreakpoint {
     id: String,
     data: ExceptionBreakpointsFilter,
     is_enabled: bool,
 }
+
 #[derive(Clone, Debug)]
 struct DataBreakpoint(project::debugger::session::DataBreakpointState);
 
@@ -1017,17 +998,24 @@ impl DataBreakpoint {
         };
         let is_enabled = self.0.is_enabled;
         let id = self.0.dap.data_id.clone();
+
         ListItem::new(SharedString::from(format!(
             "data-breakpoint-ui-item-{}",
             self.0.dap.data_id
         )))
-        .rounded()
+        .toggle_state(is_selected)
+        .inset(true)
         .start_slot(
             div()
                 .id(SharedString::from(format!(
                     "data-breakpoint-ui-item-{}-click-handler",
                     self.0.dap.data_id
                 )))
+                .child(
+                    Icon::new(IconName::Binary)
+                        .color(color)
+                        .size(IconSize::Small),
+                )
                 .tooltip({
                     let focus_handle = focus_handle.clone();
                     move |window, cx| {
@@ -1052,25 +1040,18 @@ impl DataBreakpoint {
                         })
                         .ok();
                     }
-                })
-                .cursor_pointer()
-                .child(
-                    Icon::new(IconName::Binary)
-                        .color(color)
-                        .size(IconSize::Small),
-                ),
+                }),
         )
         .child(
             h_flex()
                 .w_full()
-                .mr_4()
-                .py_0p5()
+                .gap_1()
+                .min_h(rems_from_px(26.))
                 .justify_between()
                 .child(
                     v_flex()
                         .py_1()
                         .gap_1()
-                        .min_h(px(26.))
                         .justify_center()
                         .id(("data-breakpoint-label", ix))
                         .child(
@@ -1091,7 +1072,6 @@ impl DataBreakpoint {
                     index: ix,
                 }),
         )
-        .toggle_state(is_selected)
     }
 }
 
@@ -1113,10 +1093,13 @@ impl ExceptionBreakpoint {
         let id = SharedString::from(&self.id);
         let is_enabled = self.is_enabled;
         let weak = list.clone();
+
         ListItem::new(SharedString::from(format!(
             "exception-breakpoint-ui-item-{}",
             self.id
         )))
+        .toggle_state(is_selected)
+        .inset(true)
         .on_click({
             let list = list.clone();
             move |_, window, cx| {
@@ -1124,7 +1107,6 @@ impl ExceptionBreakpoint {
                     .ok();
             }
         })
-        .rounded()
         .on_secondary_mouse_down(|_, _, cx| {
             cx.stop_propagation();
         })
@@ -1134,6 +1116,11 @@ impl ExceptionBreakpoint {
                     "exception-breakpoint-ui-item-{}-click-handler",
                     self.id
                 )))
+                .child(
+                    Icon::new(IconName::Flame)
+                        .color(color)
+                        .size(IconSize::Small),
+                )
                 .tooltip({
                     let focus_handle = focus_handle.clone();
                     move |window, cx| {
@@ -1151,32 +1138,24 @@ impl ExceptionBreakpoint {
                     }
                 })
                 .on_click({
-                    let list = list.clone();
                     move |_, _, cx| {
                         list.update(cx, |this, cx| {
                             this.toggle_exception_breakpoint(&id, cx);
                         })
                         .ok();
                     }
-                })
-                .cursor_pointer()
-                .child(
-                    Icon::new(IconName::Flame)
-                        .color(color)
-                        .size(IconSize::Small),
-                ),
+                }),
         )
         .child(
             h_flex()
                 .w_full()
-                .mr_4()
-                .py_0p5()
+                .gap_1()
+                .min_h(rems_from_px(26.))
                 .justify_between()
                 .child(
                     v_flex()
                         .py_1()
                         .gap_1()
-                        .min_h(px(26.))
                         .justify_center()
                         .id(("exception-breakpoint-label", ix))
                         .child(
@@ -1192,7 +1171,7 @@ impl ExceptionBreakpoint {
                     props,
                     breakpoint: BreakpointEntry {
                         kind: BreakpointEntryKind::ExceptionBreakpoint(self.clone()),
-                        weak: weak,
+                        weak,
                     },
                     is_selected,
                     focus_handle,
@@ -1200,7 +1179,6 @@ impl ExceptionBreakpoint {
                     index: ix,
                 }),
         )
-        .toggle_state(is_selected)
     }
 }
 #[derive(Clone, Debug)]
@@ -1302,6 +1280,7 @@ impl BreakpointEntry {
         }
     }
 }
+
 bitflags::bitflags! {
     #[derive(Clone, Copy)]
     pub struct SupportedBreakpointProperties: u32 {
@@ -1360,6 +1339,7 @@ impl BreakpointOptionsStrip {
     fn is_toggled(&self, expected_mode: ActiveBreakpointStripMode) -> bool {
         self.is_selected && self.strip_mode == Some(expected_mode)
     }
+
     fn on_click_callback(
         &self,
         mode: ActiveBreakpointStripMode,
@@ -1379,7 +1359,8 @@ impl BreakpointOptionsStrip {
             .ok();
         }
     }
-    fn add_border(
+
+    fn add_focus_styles(
         &self,
         kind: ActiveBreakpointStripMode,
         available: bool,
@@ -1388,22 +1369,25 @@ impl BreakpointOptionsStrip {
     ) -> impl Fn(Div) -> Div {
         move |this: Div| {
             // Avoid layout shifts in case there's no colored border
-            let this = this.border_2().rounded_sm();
+            let this = this.border_1().rounded_sm();
+            let color = cx.theme().colors();
+
             if self.is_selected && self.strip_mode == Some(kind) {
-                let theme = cx.theme().colors();
                 if self.focus_handle.is_focused(window) {
-                    this.border_color(theme.border_selected)
+                    this.bg(color.editor_background)
+                        .border_color(color.border_focused)
                 } else {
-                    this.border_color(theme.border_disabled)
+                    this.border_color(color.border)
                 }
             } else if !available {
-                this.border_color(cx.theme().colors().border_disabled)
+                this.border_color(color.border_transparent)
             } else {
                 this
             }
         }
     }
 }
+
 impl RenderOnce for BreakpointOptionsStrip {
     fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
         let id = self.breakpoint.id();
@@ -1426,73 +1410,117 @@ impl RenderOnce for BreakpointOptionsStrip {
         };
         let color_for_toggle = |is_enabled| {
             if is_enabled {
-                ui::Color::Default
+                Color::Default
             } else {
-                ui::Color::Muted
+                Color::Muted
             }
         };
 
         h_flex()
-            .gap_1()
+            .gap_px()
+            .mr_3() // Space to avoid overlapping with the scrollbar
             .child(
-                div().map(self.add_border(ActiveBreakpointStripMode::Log, supports_logs, window, cx))
+                div()
+                    .map(self.add_focus_styles(
+                        ActiveBreakpointStripMode::Log,
+                        supports_logs,
+                        window,
+                        cx,
+                    ))
                     .child(
                         IconButton::new(
                             SharedString::from(format!("{id}-log-toggle")),
-                            IconName::ScrollText,
+                            IconName::Notepad,
                         )
-                        .icon_size(IconSize::XSmall)
+                        .shape(ui::IconButtonShape::Square)
                         .style(style_for_toggle(ActiveBreakpointStripMode::Log, has_logs))
+                        .icon_size(IconSize::Small)
                         .icon_color(color_for_toggle(has_logs))
+                        .when(has_logs, |this| this.indicator(Indicator::dot().color(Color::Info)))
                         .disabled(!supports_logs)
                         .toggle_state(self.is_toggled(ActiveBreakpointStripMode::Log))
-                        .on_click(self.on_click_callback(ActiveBreakpointStripMode::Log)).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))
+                        .on_click(self.on_click_callback(ActiveBreakpointStripMode::Log))
+                        .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,
+                            )
+                        }),
                     )
                     .when(!has_logs && !self.is_selected, |this| this.invisible()),
             )
             .child(
-                div().map(self.add_border(
-                    ActiveBreakpointStripMode::Condition,
-                    supports_condition,
-                    window, cx
-                ))
+                div()
+                    .map(self.add_focus_styles(
+                        ActiveBreakpointStripMode::Condition,
+                        supports_condition,
+                        window,
+                        cx,
+                    ))
                     .child(
                         IconButton::new(
                             SharedString::from(format!("{id}-condition-toggle")),
                             IconName::SplitAlt,
                         )
-                        .icon_size(IconSize::XSmall)
+                        .shape(ui::IconButtonShape::Square)
                         .style(style_for_toggle(
                             ActiveBreakpointStripMode::Condition,
-                            has_condition
+                            has_condition,
                         ))
+                        .icon_size(IconSize::Small)
                         .icon_color(color_for_toggle(has_condition))
+                        .when(has_condition, |this| this.indicator(Indicator::dot().color(Color::Info)))
                         .disabled(!supports_condition)
                         .toggle_state(self.is_toggled(ActiveBreakpointStripMode::Condition))
                         .on_click(self.on_click_callback(ActiveBreakpointStripMode::Condition))
-                        .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))
+                        .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,
+                            )
+                        }),
                     )
                     .when(!has_condition && !self.is_selected, |this| this.invisible()),
             )
             .child(
-                div().map(self.add_border(
-                    ActiveBreakpointStripMode::HitCondition,
-                    supports_hit_condition,window, cx
-                ))
+                div()
+                    .map(self.add_focus_styles(
+                        ActiveBreakpointStripMode::HitCondition,
+                        supports_hit_condition,
+                        window,
+                        cx,
+                    ))
                     .child(
                         IconButton::new(
                             SharedString::from(format!("{id}-hit-condition-toggle")),
                             IconName::ArrowDown10,
                         )
-                        .icon_size(IconSize::XSmall)
                         .style(style_for_toggle(
                             ActiveBreakpointStripMode::HitCondition,
                             has_hit_condition,
                         ))
+                        .shape(ui::IconButtonShape::Square)
+                        .icon_size(IconSize::Small)
                         .icon_color(color_for_toggle(has_hit_condition))
+                        .when(has_hit_condition, |this| this.indicator(Indicator::dot().color(Color::Info)))
                         .disabled(!supports_hit_condition)
                         .toggle_state(self.is_toggled(ActiveBreakpointStripMode::HitCondition))
-                        .on_click(self.on_click_callback(ActiveBreakpointStripMode::HitCondition)).tooltip(|window, cx| Tooltip::with_meta("Set Hit Condition", None, "Set expression that controls how many hits of the breakpoint are ignored.", window, cx))
+                        .on_click(self.on_click_callback(ActiveBreakpointStripMode::HitCondition))
+                        .tooltip(|window, cx| {
+                            Tooltip::with_meta(
+                                "Set Hit Condition",
+                                None,
+                                "Set expression that controls how many hits of the breakpoint are ignored.",
+                                window,
+                                cx,
+                            )
+                        }),
                     )
                     .when(!has_hit_condition && !self.is_selected, |this| {
                         this.invisible()

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

@@ -352,7 +352,7 @@ impl Console {
                     .child(
                         div()
                             .px_1()
-                            .child(Icon::new(IconName::ChevronDownSmall).size(IconSize::XSmall)),
+                            .child(Icon::new(IconName::ChevronDown).size(IconSize::XSmall)),
                     ),
             )
             .when(
@@ -365,9 +365,9 @@ impl Console {
                         Some(ContextMenu::build(window, cx, |context_menu, _, _| {
                             context_menu
                                 .when_some(keybinding_target.clone(), |el, keybinding_target| {
-                                    el.context(keybinding_target.clone())
+                                    el.context(keybinding_target)
                                 })
-                                .action("Watch expression", WatchExpression.boxed_clone())
+                                .action("Watch Expression", WatchExpression.boxed_clone())
                         }))
                     })
                 },
@@ -452,18 +452,22 @@ impl Render for Console {
     fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
         let query_focus_handle = self.query_bar.focus_handle(cx);
         self.update_output(window, cx);
+
         v_flex()
             .track_focus(&self.focus_handle)
             .key_context("DebugConsole")
             .on_action(cx.listener(Self::evaluate))
             .on_action(cx.listener(Self::watch_expression))
             .size_full()
+            .border_2()
+            .bg(cx.theme().colors().editor_background)
             .child(self.render_console(cx))
             .when(self.is_running(cx), |this| {
                 this.child(Divider::horizontal()).child(
                     h_flex()
                         .on_action(cx.listener(Self::previous_query))
                         .on_action(cx.listener(Self::next_query))
+                        .p_1()
                         .gap_1()
                         .bg(cx.theme().colors().editor_background)
                         .child(self.render_query_bar(cx))
@@ -474,6 +478,9 @@ impl Render for Console {
                             .on_click(move |_, window, cx| {
                                 window.dispatch_action(Box::new(Confirm), cx)
                             })
+                            .layer(ui::ElevationIndex::ModalSurface)
+                            .size(ui::ButtonSize::Compact)
+                            .child(Label::new("Evaluate"))
                             .tooltip({
                                 let query_focus_handle = query_focus_handle.clone();
 
@@ -486,10 +493,7 @@ impl Render for Console {
                                         cx,
                                     )
                                 }
-                            })
-                            .layer(ui::ElevationIndex::ModalSurface)
-                            .size(ui::ButtonSize::Compact)
-                            .child(Label::new("Evaluate")),
+                            }),
                             self.render_submit_menu(
                                 ElementId::Name("split-button-right-confirm-button".into()),
                                 Some(query_focus_handle.clone()),
@@ -499,7 +503,6 @@ impl Render for Console {
                         )),
                 )
             })
-            .border_2()
     }
 }
 
@@ -608,17 +611,16 @@ impl ConsoleQueryBarCompletionProvider {
             for variable in console.variable_list.update(cx, |variable_list, cx| {
                 variable_list.completion_variables(cx)
             }) {
-                if let Some(evaluate_name) = &variable.evaluate_name {
-                    if variables
+                if let Some(evaluate_name) = &variable.evaluate_name
+                    && variables
                         .insert(evaluate_name.clone(), variable.value.clone())
                         .is_none()
-                    {
-                        string_matches.push(StringMatchCandidate {
-                            id: 0,
-                            string: evaluate_name.clone(),
-                            char_bag: evaluate_name.chars().collect(),
-                        });
-                    }
+                {
+                    string_matches.push(StringMatchCandidate {
+                        id: 0,
+                        string: evaluate_name.clone(),
+                        char_bag: evaluate_name.chars().collect(),
+                    });
                 }
 
                 if variables
@@ -694,7 +696,7 @@ impl ConsoleQueryBarCompletionProvider {
         new_bytes: &[u8],
         snapshot: &TextBufferSnapshot,
     ) -> Range<Anchor> {
-        let buffer_offset = buffer_position.to_offset(&snapshot);
+        let buffer_offset = buffer_position.to_offset(snapshot);
         let buffer_bytes = &buffer_text.as_bytes()[0..buffer_offset];
 
         let mut prefix_len = 0;
@@ -974,7 +976,7 @@ mod tests {
             &cx.buffer_text(),
             snapshot.anchor_before(buffer_position),
             replacement.as_bytes(),
-            &snapshot,
+            snapshot,
         );
 
         cx.update_editor(|editor, _, cx| {

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

@@ -18,10 +18,8 @@ use project::debugger::{MemoryCell, dap_command::DataBreakpointContext, session:
 use settings::Settings;
 use theme::ThemeSettings;
 use ui::{
-    ActiveTheme, AnyElement, App, Color, Context, ContextMenu, Div, Divider, DropdownMenu, Element,
-    FluentBuilder, Icon, IconName, InteractiveElement, IntoElement, Label, LabelCommon,
-    ParentElement, Pixels, PopoverMenuHandle, Render, Scrollbar, ScrollbarState, SharedString,
-    StatefulInteractiveElement, Styled, TextSize, Tooltip, Window, div, h_flex, px, v_flex,
+    ContextMenu, Divider, DropdownMenu, FluentBuilder, IntoElement, PopoverMenuHandle, Render,
+    Scrollbar, ScrollbarState, StatefulInteractiveElement, Tooltip, prelude::*,
 };
 use workspace::Workspace;
 
@@ -264,7 +262,7 @@ impl MemoryView {
         cx: &mut Context<Self>,
     ) {
         use parse_int::parse;
-        let Ok(as_address) = parse::<u64>(&memory_reference) else {
+        let Ok(as_address) = parse::<u64>(memory_reference) else {
             return;
         };
         let access_size = evaluate_name
@@ -463,7 +461,7 @@ impl MemoryView {
             let data_breakpoint_info = this.data_breakpoint_info(context.clone(), None, cx);
             cx.spawn(async move |this, cx| {
                 if let Some(info) = data_breakpoint_info.await {
-                    let Some(data_id) = info.data_id.clone() else {
+                    let Some(data_id) = info.data_id else {
                         return;
                     };
                     _ = this.update(cx, |this, cx| {
@@ -933,7 +931,7 @@ impl Render for MemoryView {
                 v_flex()
                     .size_full()
                     .on_drag_move(cx.listener(|this, evt, _, _| {
-                        this.handle_memory_drag(&evt);
+                        this.handle_memory_drag(evt);
                     }))
                     .child(self.render_memory(cx).size_full())
                     .children(self.open_context_menu.as_ref().map(|(menu, position, _)| {

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

@@ -157,7 +157,7 @@ impl ModuleList {
                 h_flex()
                     .text_ui_xs(cx)
                     .text_color(cx.theme().colors().text_muted)
-                    .when_some(module.path.clone(), |this, path| this.child(path)),
+                    .when_some(module.path, |this, path| this.child(path)),
             )
             .into_any()
     }
@@ -223,7 +223,7 @@ impl ModuleList {
 
     fn select_next(&mut self, _: &menu::SelectNext, _window: &mut Window, cx: &mut Context<Self>) {
         let ix = match self.selected_ix {
-            _ if self.entries.len() == 0 => None,
+            _ if self.entries.is_empty() => None,
             None => Some(0),
             Some(ix) => {
                 if ix == self.entries.len() - 1 {
@@ -243,7 +243,7 @@ impl ModuleList {
         cx: &mut Context<Self>,
     ) {
         let ix = match self.selected_ix {
-            _ if self.entries.len() == 0 => None,
+            _ if self.entries.is_empty() => None,
             None => Some(self.entries.len() - 1),
             Some(ix) => {
                 if ix == 0 {
@@ -262,7 +262,7 @@ impl ModuleList {
         _window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        let ix = if self.entries.len() > 0 {
+        let ix = if !self.entries.is_empty() {
             Some(0)
         } else {
             None
@@ -271,7 +271,7 @@ impl ModuleList {
     }
 
     fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
-        let ix = if self.entries.len() > 0 {
+        let ix = if !self.entries.is_empty() {
             Some(self.entries.len() - 1)
         } else {
             None

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

@@ -126,7 +126,7 @@ impl StackFrameList {
         self.stack_frames(cx)
             .unwrap_or_default()
             .into_iter()
-            .map(|stack_frame| stack_frame.dap.clone())
+            .map(|stack_frame| stack_frame.dap)
             .collect()
     }
 
@@ -224,7 +224,7 @@ impl StackFrameList {
 
         let collapsed_entries = std::mem::take(&mut collapsed_entries);
         if !collapsed_entries.is_empty() {
-            entries.push(StackFrameEntry::Collapsed(collapsed_entries.clone()));
+            entries.push(StackFrameEntry::Collapsed(collapsed_entries));
         }
         self.entries = entries;
 
@@ -418,7 +418,7 @@ impl StackFrameList {
         let source = stack_frame.source.clone();
         let is_selected_frame = Some(ix) == self.selected_ix;
 
-        let path = source.clone().and_then(|s| s.path.or(s.name));
+        let path = source.and_then(|s| s.path.or(s.name));
         let formatted_path = path.map(|path| format!("{}:{}", path, stack_frame.line,));
         let formatted_path = formatted_path.map(|path| {
             Label::new(path)
@@ -493,7 +493,7 @@ impl StackFrameList {
                             .child(
                                 IconButton::new(
                                     ("restart-stack-frame", stack_frame.id),
-                                    IconName::DebugRestart,
+                                    IconName::RotateCcw,
                                 )
                                 .icon_size(IconSize::Small)
                                 .on_click(cx.listener({
@@ -621,7 +621,7 @@ impl StackFrameList {
 
     fn select_next(&mut self, _: &menu::SelectNext, _window: &mut Window, cx: &mut Context<Self>) {
         let ix = match self.selected_ix {
-            _ if self.entries.len() == 0 => None,
+            _ if self.entries.is_empty() => None,
             None => Some(0),
             Some(ix) => {
                 if ix == self.entries.len() - 1 {
@@ -641,7 +641,7 @@ impl StackFrameList {
         cx: &mut Context<Self>,
     ) {
         let ix = match self.selected_ix {
-            _ if self.entries.len() == 0 => None,
+            _ if self.entries.is_empty() => None,
             None => Some(self.entries.len() - 1),
             Some(ix) => {
                 if ix == 0 {
@@ -660,7 +660,7 @@ impl StackFrameList {
         _window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        let ix = if self.entries.len() > 0 {
+        let ix = if !self.entries.is_empty() {
             Some(0)
         } else {
             None
@@ -669,7 +669,7 @@ impl StackFrameList {
     }
 
     fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
-        let ix = if self.entries.len() > 0 {
+        let ix = if !self.entries.is_empty() {
             Some(self.entries.len() - 1)
         } else {
             None

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

@@ -272,7 +272,7 @@ impl VariableList {
         let mut entries = vec![];
 
         let scopes: Vec<_> = self.session.update(cx, |session, cx| {
-            session.scopes(stack_frame_id, cx).iter().cloned().collect()
+            session.scopes(stack_frame_id, cx).to_vec()
         });
 
         let mut contains_local_scope = false;
@@ -291,7 +291,7 @@ impl VariableList {
                 }
 
                 self.session.update(cx, |session, cx| {
-                    session.variables(scope.variables_reference, cx).len() > 0
+                    !session.variables(scope.variables_reference, cx).is_empty()
                 })
             })
             .map(|scope| {
@@ -313,7 +313,7 @@ impl VariableList {
                         watcher.variables_reference,
                         watcher.variables_reference,
                         EntryPath::for_watcher(watcher.expression.clone()),
-                        DapEntry::Watcher(watcher.clone()),
+                        DapEntry::Watcher(watcher),
                     )
                 })
                 .collect::<Vec<_>>(),
@@ -947,7 +947,7 @@ impl VariableList {
     #[track_caller]
     #[cfg(test)]
     pub(crate) fn assert_visual_entries(&self, expected: Vec<&str>) {
-        const INDENT: &'static str = "    ";
+        const INDENT: &str = "    ";
 
         let entries = &self.entries;
         let mut visual_entries = Vec::with_capacity(entries.len());
@@ -997,7 +997,7 @@ impl VariableList {
                 DapEntry::Watcher { .. } => continue,
                 DapEntry::Variable(dap) => scopes[idx].1.push(dap.clone()),
                 DapEntry::Scope(scope) => {
-                    if scopes.len() > 0 {
+                    if !scopes.is_empty() {
                         idx += 1;
                     }
 
@@ -1289,7 +1289,7 @@ impl VariableList {
                             }),
                         )
                         .child(self.render_variable_value(
-                            &entry,
+                            entry,
                             &variable_color,
                             watcher.value.to_string(),
                             cx,
@@ -1301,8 +1301,6 @@ impl VariableList {
                         IconName::Close,
                     )
                     .on_click({
-                        let weak = weak.clone();
-                        let path = path.clone();
                         move |_, window, cx| {
                             weak.update(cx, |variable_list, cx| {
                                 variable_list.selection = Some(path.clone());
@@ -1470,7 +1468,6 @@ impl VariableList {
                     }))
                 })
                 .on_secondary_mouse_down(cx.listener({
-                    let path = path.clone();
                     let entry = variable.clone();
                     move |this, event: &MouseDownEvent, window, cx| {
                         this.selection = Some(path.clone());
@@ -1494,7 +1491,7 @@ impl VariableList {
                             }),
                         )
                         .child(self.render_variable_value(
-                            &variable,
+                            variable,
                             &variable_color,
                             dap.value.clone(),
                             cx,

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

@@ -139,7 +139,7 @@ async fn test_show_attach_modal_and_select_process(
     workspace
         .update(cx, |_, window, cx| {
             let names =
-                attach_modal.update(cx, |modal, cx| attach_modal::_process_names(&modal, cx));
+                attach_modal.update(cx, |modal, cx| attach_modal::_process_names(modal, cx));
             // Initially all processes are visible.
             assert_eq!(3, names.len());
             attach_modal.update(cx, |this, cx| {
@@ -154,7 +154,7 @@ async fn test_show_attach_modal_and_select_process(
     workspace
         .update(cx, |_, _, cx| {
             let names =
-                attach_modal.update(cx, |modal, cx| attach_modal::_process_names(&modal, cx));
+                attach_modal.update(cx, |modal, cx| attach_modal::_process_names(modal, cx));
             // Initially all processes are visible.
             assert_eq!(2, names.len());
         })

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

@@ -1330,7 +1330,6 @@ async fn test_unsetting_breakpoints_on_clear_breakpoint_action(
     let called_set_breakpoints = Arc::new(AtomicBool::new(false));
 
     client.on_request::<SetBreakpoints, _>({
-        let called_set_breakpoints = called_set_breakpoints.clone();
         move |_, args| {
             assert!(
                 args.breakpoints.is_none_or(|bps| bps.is_empty()),
@@ -1445,7 +1444,6 @@ async fn test_we_send_arguments_from_user_config(
     let launch_handler_called = Arc::new(AtomicBool::new(false));
 
     start_debug_session_with(&workspace, cx, debug_definition.clone(), {
-        let debug_definition = debug_definition.clone();
         let launch_handler_called = launch_handler_called.clone();
 
         move |client| {
@@ -1783,9 +1781,8 @@ async fn test_debug_adapters_shutdown_on_app_quit(
     let disconnect_request_received = Arc::new(AtomicBool::new(false));
     let disconnect_clone = disconnect_request_received.clone();
 
-    let disconnect_clone_for_handler = disconnect_clone.clone();
     client.on_request::<Disconnect, _>(move |_, _| {
-        disconnect_clone_for_handler.store(true, Ordering::SeqCst);
+        disconnect_clone.store(true, Ordering::SeqCst);
         Ok(())
     });
 

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

@@ -106,9 +106,7 @@ async fn test_debug_session_substitutes_variables_and_relativizes_paths(
                         );
 
                         let expected_other_field = if input_path.contains("$ZED_WORKTREE_ROOT") {
-                            input_path
-                                .replace("$ZED_WORKTREE_ROOT", &path!("/test/worktree/path"))
-                                .to_owned()
+                            input_path.replace("$ZED_WORKTREE_ROOT", path!("/test/worktree/path"))
                         } else {
                             input_path.to_string()
                         };

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

@@ -1445,11 +1445,8 @@ async fn test_variable_list_only_sends_requests_when_rendering(
 
     cx.run_until_parked();
 
-    let running_state = active_debug_session_panel(workspace, cx).update_in(cx, |item, _, _| {
-        let state = item.running_state().clone();
-
-        state
-    });
+    let running_state = active_debug_session_panel(workspace, cx)
+        .update_in(cx, |item, _, _| item.running_state().clone());
 
     client
         .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {

crates/diagnostics/src/diagnostic_renderer.rs 🔗

@@ -46,7 +46,7 @@ impl DiagnosticRenderer {
                     markdown.push_str(" (");
                 }
                 if let Some(source) = diagnostic.source.as_ref() {
-                    markdown.push_str(&Markdown::escape(&source));
+                    markdown.push_str(&Markdown::escape(source));
                 }
                 if diagnostic.source.is_some() && diagnostic.code.is_some() {
                     markdown.push(' ');
@@ -287,15 +287,13 @@ impl DiagnosticBlock {
                     }
                 }
             }
-        } else {
-            if let Some(diagnostic) = editor
-                .snapshot(window, cx)
-                .buffer_snapshot
-                .diagnostic_group(buffer_id, group_id)
-                .nth(ix)
-            {
-                Self::jump_to(editor, diagnostic.range, window, cx)
-            }
+        } else if let Some(diagnostic) = editor
+            .snapshot(window, cx)
+            .buffer_snapshot
+            .diagnostic_group(buffer_id, group_id)
+            .nth(ix)
+        {
+            Self::jump_to(editor, diagnostic.range, window, cx)
         };
     }
 
@@ -306,7 +304,7 @@ impl DiagnosticBlock {
         cx: &mut Context<Editor>,
     ) {
         let snapshot = &editor.buffer().read(cx).snapshot(cx);
-        let range = range.start.to_offset(&snapshot)..range.end.to_offset(&snapshot);
+        let range = range.start.to_offset(snapshot)..range.end.to_offset(snapshot);
 
         editor.unfold_ranges(&[range.start..range.end], true, false, cx);
         editor.change_selections(Default::default(), window, cx, |s| {

crates/diagnostics/src/diagnostics.rs 🔗

@@ -383,12 +383,10 @@ impl ProjectDiagnosticsEditor {
             } else {
                 self.update_all_diagnostics(false, window, cx);
             }
+        } else if self.update_excerpts_task.is_some() {
+            self.update_excerpts_task = None;
         } else {
-            if self.update_excerpts_task.is_some() {
-                self.update_excerpts_task = None;
-            } else {
-                self.update_all_diagnostics(false, window, cx);
-            }
+            self.update_all_diagnostics(false, window, cx);
         }
         cx.notify();
     }
@@ -528,7 +526,7 @@ impl ProjectDiagnosticsEditor {
             lsp::DiagnosticSeverity::ERROR
         };
 
-        cx.spawn_in(window, async move |this, mut cx| {
+        cx.spawn_in(window, async move |this, cx| {
             let diagnostics = buffer_snapshot
                 .diagnostics_in_range::<_, text::Anchor>(
                     Point::zero()..buffer_snapshot.max_point(),
@@ -542,7 +540,7 @@ impl ProjectDiagnosticsEditor {
                     return true;
                 }
                 this.diagnostics.insert(buffer_id, diagnostics.clone());
-                return false;
+                false
             })?;
             if unchanged {
                 return Ok(());
@@ -595,7 +593,7 @@ impl ProjectDiagnosticsEditor {
                     b.initial_range.clone(),
                     DEFAULT_MULTIBUFFER_CONTEXT,
                     buffer_snapshot.clone(),
-                    &mut cx,
+                    cx,
                 )
                 .await;
                 let i = excerpt_ranges
@@ -639,17 +637,15 @@ impl ProjectDiagnosticsEditor {
                 #[cfg(test)]
                 let cloned_blocks = blocks.clone();
 
-                if was_empty {
-                    if let Some(anchor_range) = anchor_ranges.first() {
-                        let range_to_select = anchor_range.start..anchor_range.start;
-                        this.editor.update(cx, |editor, cx| {
-                            editor.change_selections(Default::default(), window, cx, |s| {
-                                s.select_anchor_ranges([range_to_select]);
-                            })
-                        });
-                        if this.focus_handle.is_focused(window) {
-                            this.editor.read(cx).focus_handle(cx).focus(window);
-                        }
+                if was_empty && let Some(anchor_range) = anchor_ranges.first() {
+                    let range_to_select = anchor_range.start..anchor_range.start;
+                    this.editor.update(cx, |editor, cx| {
+                        editor.change_selections(Default::default(), window, cx, |s| {
+                            s.select_anchor_ranges([range_to_select]);
+                        })
+                    });
+                    if this.focus_handle.is_focused(window) {
+                        this.editor.read(cx).focus_handle(cx).focus(window);
                     }
                 }
 
@@ -980,18 +976,16 @@ async fn heuristic_syntactic_expand(
         // Remove blank lines from start and end
         if let Some(start_row) = (outline_range.start.row..outline_range.end.row)
             .find(|row| !snapshot.line_indent_for_row(*row).is_line_blank())
-        {
-            if let Some(end_row) = (outline_range.start.row..outline_range.end.row + 1)
+            && let Some(end_row) = (outline_range.start.row..outline_range.end.row + 1)
                 .rev()
                 .find(|row| !snapshot.line_indent_for_row(*row).is_line_blank())
-            {
-                let row_count = end_row.saturating_sub(start_row);
-                if row_count <= max_row_count {
-                    return Some(RangeInclusive::new(
-                        outline_range.start.row,
-                        outline_range.end.row,
-                    ));
-                }
+        {
+            let row_count = end_row.saturating_sub(start_row);
+            if row_count <= max_row_count {
+                return Some(RangeInclusive::new(
+                    outline_range.start.row,
+                    outline_range.end.row,
+                ));
             }
         }
     }

crates/diagnostics/src/diagnostics_tests.rs 🔗

@@ -862,7 +862,7 @@ async fn test_random_diagnostics_with_inlays(cx: &mut TestAppContext, mut rng: S
             21..=50 => mutated_diagnostics.update_in(cx, |diagnostics, window, cx| {
                 diagnostics.editor.update(cx, |editor, cx| {
                     let snapshot = editor.snapshot(window, cx);
-                    if snapshot.buffer_snapshot.len() > 0 {
+                    if !snapshot.buffer_snapshot.is_empty() {
                         let position = rng.gen_range(0..snapshot.buffer_snapshot.len());
                         let position = snapshot.buffer_snapshot.clip_offset(position, Bias::Left);
                         log::info!(
@@ -971,7 +971,7 @@ async fn active_diagnostics_dismiss_after_invalidation(cx: &mut TestAppContext)
 
     let mut cx = EditorTestContext::new(cx).await;
     let lsp_store =
-        cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
+        cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
 
     cx.set_state(indoc! {"
         ˇfn func(abc def: i32) -> u32 {
@@ -1065,7 +1065,7 @@ async fn cycle_through_same_place_diagnostics(cx: &mut TestAppContext) {
 
     let mut cx = EditorTestContext::new(cx).await;
     let lsp_store =
-        cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
+        cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
 
     cx.set_state(indoc! {"
         ˇfn func(abc def: i32) -> u32 {
@@ -1239,7 +1239,7 @@ async fn test_diagnostics_with_links(cx: &mut TestAppContext) {
         }
     "});
     let lsp_store =
-        cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
+        cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
 
     cx.update(|_, cx| {
         lsp_store.update(cx, |lsp_store, cx| {
@@ -1293,7 +1293,7 @@ async fn test_hover_diagnostic_and_info_popovers(cx: &mut gpui::TestAppContext)
         fn «test»() { println!(); }
     "});
     let lsp_store =
-        cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
+        cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
     cx.update(|_, cx| {
         lsp_store.update(cx, |lsp_store, cx| {
             lsp_store.update_diagnostics(
@@ -1450,7 +1450,7 @@ async fn go_to_diagnostic_with_severity(cx: &mut TestAppContext) {
 
     let mut cx = EditorTestContext::new(cx).await;
     let lsp_store =
-        cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
+        cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
 
     cx.set_state(indoc! {"error warning info hiˇnt"});
 

crates/diagnostics/src/toolbar_controls.rs 🔗

@@ -54,7 +54,7 @@ impl Render for ToolbarControls {
             .map(|div| {
                 if is_updating {
                     div.child(
-                        IconButton::new("stop-updating", IconName::StopFilled)
+                        IconButton::new("stop-updating", IconName::Stop)
                             .icon_color(Color::Info)
                             .shape(IconButtonShape::Square)
                             .tooltip(Tooltip::for_action_title(
@@ -73,7 +73,7 @@ impl Render for ToolbarControls {
                     )
                 } else {
                     div.child(
-                        IconButton::new("refresh-diagnostics", IconName::Update)
+                        IconButton::new("refresh-diagnostics", IconName::ArrowCircle)
                             .icon_color(Color::Info)
                             .shape(IconButtonShape::Square)
                             .disabled(!has_stale_excerpts && !fetch_cargo_diagnostics)

crates/docs_preprocessor/src/main.rs 🔗

@@ -8,7 +8,7 @@ use std::borrow::Cow;
 use std::collections::{HashMap, HashSet};
 use std::io::{self, Read};
 use std::process;
-use std::sync::LazyLock;
+use std::sync::{LazyLock, OnceLock};
 use util::paths::PathExt;
 
 static KEYMAP_MACOS: LazyLock<KeymapFile> = LazyLock::new(|| {
@@ -21,7 +21,7 @@ static KEYMAP_LINUX: LazyLock<KeymapFile> = LazyLock::new(|| {
 
 static ALL_ACTIONS: LazyLock<Vec<ActionDef>> = LazyLock::new(dump_all_gpui_actions);
 
-const FRONT_MATTER_COMMENT: &'static str = "<!-- ZED_META {} -->";
+const FRONT_MATTER_COMMENT: &str = "<!-- ZED_META {} -->";
 
 fn main() -> Result<()> {
     zlog::init();
@@ -61,15 +61,13 @@ impl PreprocessorError {
             for alias in action.deprecated_aliases {
                 if alias == &action_name {
                     return PreprocessorError::DeprecatedActionUsed {
-                        used: action_name.clone(),
+                        used: action_name,
                         should_be: action.name.to_string(),
                     };
                 }
             }
         }
-        PreprocessorError::ActionNotFound {
-            action_name: action_name.to_string(),
-        }
+        PreprocessorError::ActionNotFound { action_name }
     }
 }
 
@@ -101,12 +99,13 @@ fn handle_preprocessing() -> Result<()> {
     let mut errors = HashSet::<PreprocessorError>::new();
 
     handle_frontmatter(&mut book, &mut errors);
+    template_big_table_of_actions(&mut book);
     template_and_validate_keybindings(&mut book, &mut errors);
     template_and_validate_actions(&mut book, &mut errors);
 
     if !errors.is_empty() {
-        const ANSI_RED: &'static str = "\x1b[31m";
-        const ANSI_RESET: &'static str = "\x1b[0m";
+        const ANSI_RED: &str = "\x1b[31m";
+        const ANSI_RESET: &str = "\x1b[0m";
         for error in &errors {
             eprintln!("{ANSI_RED}ERROR{ANSI_RESET}: {}", error);
         }
@@ -129,7 +128,7 @@ fn handle_frontmatter(book: &mut Book, errors: &mut HashSet<PreprocessorError>)
                 let Some((name, value)) = line.split_once(':') else {
                     errors.insert(PreprocessorError::InvalidFrontmatterLine(format!(
                         "{}: {}",
-                        chapter_breadcrumbs(&chapter),
+                        chapter_breadcrumbs(chapter),
                         line
                     )));
                     continue;
@@ -143,11 +142,20 @@ fn handle_frontmatter(book: &mut Book, errors: &mut HashSet<PreprocessorError>)
                 &serde_json::to_string(&metadata).expect("Failed to serialize metadata"),
             )
         });
-        match new_content {
-            Cow::Owned(content) => {
-                chapter.content = content;
-            }
-            Cow::Borrowed(_) => {}
+        if let Cow::Owned(content) = new_content {
+            chapter.content = content;
+        }
+    });
+}
+
+fn template_big_table_of_actions(book: &mut Book) {
+    for_each_chapter_mut(book, |chapter| {
+        let needle = "{#ACTIONS_TABLE#}";
+        if let Some(start) = chapter.content.rfind(needle) {
+            chapter.content.replace_range(
+                start..start + needle.len(),
+                &generate_big_table_of_actions(),
+            );
         }
     });
 }
@@ -282,6 +290,7 @@ struct ActionDef {
     name: &'static str,
     human_name: String,
     deprecated_aliases: &'static [&'static str],
+    docs: Option<&'static str>,
 }
 
 fn dump_all_gpui_actions() -> Vec<ActionDef> {
@@ -290,12 +299,13 @@ fn dump_all_gpui_actions() -> Vec<ActionDef> {
             name: action.name,
             human_name: command_palette::humanize_action_name(action.name),
             deprecated_aliases: action.deprecated_aliases,
+            docs: action.documentation,
         })
         .collect::<Vec<ActionDef>>();
 
     actions.sort_by_key(|a| a.name);
 
-    return actions;
+    actions
 }
 
 fn handle_postprocessing() -> Result<()> {
@@ -388,7 +398,7 @@ fn handle_postprocessing() -> Result<()> {
         let meta_title = format!("{} | {}", page_title, meta_title);
         zlog::trace!(logger => "Updating {:?}", pretty_path(&file, &root_dir));
         let contents = contents.replace("#description#", meta_description);
-        let contents = TITLE_REGEX
+        let contents = title_regex()
             .replace(&contents, |_: &regex::Captures| {
                 format!("<title>{}</title>", meta_title)
             })
@@ -402,21 +412,75 @@ fn handle_postprocessing() -> Result<()> {
         path: &'a std::path::PathBuf,
         root: &'a std::path::PathBuf,
     ) -> &'a std::path::Path {
-        &path.strip_prefix(&root).unwrap_or(&path)
+        path.strip_prefix(&root).unwrap_or(path)
     }
-    const TITLE_REGEX: std::cell::LazyCell<Regex> =
-        std::cell::LazyCell::new(|| Regex::new(r"<title>\s*(.*?)\s*</title>").unwrap());
     fn extract_title_from_page(contents: &str, pretty_path: &std::path::Path) -> String {
-        let title_tag_contents = &TITLE_REGEX
-            .captures(&contents)
+        let title_tag_contents = &title_regex()
+            .captures(contents)
             .with_context(|| format!("Failed to find title in {:?}", pretty_path))
             .expect("Page has <title> element")[1];
-        let title = title_tag_contents
+
+        title_tag_contents
             .trim()
             .strip_suffix("- Zed")
             .unwrap_or(title_tag_contents)
             .trim()
-            .to_string();
-        title
+            .to_string()
     }
 }
+
+fn title_regex() -> &'static Regex {
+    static TITLE_REGEX: OnceLock<Regex> = OnceLock::new();
+    TITLE_REGEX.get_or_init(|| Regex::new(r"<title>\s*(.*?)\s*</title>").unwrap())
+}
+
+fn generate_big_table_of_actions() -> String {
+    let actions = &*ALL_ACTIONS;
+    let mut output = String::new();
+
+    let mut actions_sorted = actions.iter().collect::<Vec<_>>();
+    actions_sorted.sort_by_key(|a| a.name);
+
+    // Start the definition list with custom styling for better spacing
+    output.push_str("<dl style=\"line-height: 1.8;\">\n");
+
+    for action in actions_sorted.into_iter() {
+        // Add the humanized action name as the term with margin
+        output.push_str(
+            "<dt style=\"margin-top: 1.5em; margin-bottom: 0.5em; font-weight: bold;\"><code>",
+        );
+        output.push_str(&action.human_name);
+        output.push_str("</code></dt>\n");
+
+        // Add the definition with keymap name and description
+        output.push_str("<dd style=\"margin-left: 2em; margin-bottom: 1em;\">\n");
+
+        // Add the description, escaping HTML if needed
+        if let Some(description) = action.docs {
+            output.push_str(
+                &description
+                    .replace("&", "&amp;")
+                    .replace("<", "&lt;")
+                    .replace(">", "&gt;"),
+            );
+            output.push_str("<br>\n");
+        }
+        output.push_str("Keymap Name: <code>");
+        output.push_str(action.name);
+        output.push_str("</code><br>\n");
+        if !action.deprecated_aliases.is_empty() {
+            output.push_str("Deprecated Aliases:");
+            for alias in action.deprecated_aliases.iter() {
+                output.push_str("<code>");
+                output.push_str(alias);
+                output.push_str("</code>, ");
+            }
+        }
+        output.push_str("\n</dd>\n");
+    }
+
+    // Close the definition list
+    output.push_str("</dl>\n");
+
+    output
+}

crates/edit_prediction/src/edit_prediction.rs 🔗

@@ -34,7 +34,7 @@ pub enum DataCollectionState {
 
 impl DataCollectionState {
     pub fn is_supported(&self) -> bool {
-        !matches!(self, DataCollectionState::Unsupported { .. })
+        !matches!(self, DataCollectionState::Unsupported)
     }
 
     pub fn is_enabled(&self) -> bool {

crates/edit_prediction_button/src/edit_prediction_button.rs 🔗

@@ -127,7 +127,7 @@ impl Render for EditPredictionButton {
                             }),
                     );
                 }
-                let this = cx.entity().clone();
+                let this = cx.entity();
 
                 div().child(
                     PopoverMenu::new("copilot")
@@ -168,7 +168,7 @@ impl Render for EditPredictionButton {
                         let account_status = agent.account_status.clone();
                         match account_status {
                             AccountStatus::NeedsActivation { activate_url } => {
-                                SupermavenButtonStatus::NeedsActivation(activate_url.clone())
+                                SupermavenButtonStatus::NeedsActivation(activate_url)
                             }
                             AccountStatus::Unknown => SupermavenButtonStatus::Initializing,
                             AccountStatus::Ready => SupermavenButtonStatus::Ready,
@@ -182,10 +182,10 @@ impl Render for EditPredictionButton {
                 let icon = status.to_icon();
                 let tooltip_text = status.to_tooltip();
                 let has_menu = status.has_menu();
-                let this = cx.entity().clone();
+                let this = cx.entity();
                 let fs = self.fs.clone();
 
-                return div().child(
+                div().child(
                     PopoverMenu::new("supermaven")
                         .menu(move |window, cx| match &status {
                             SupermavenButtonStatus::NeedsActivation(activate_url) => {
@@ -230,7 +230,7 @@ impl Render for EditPredictionButton {
                             },
                         )
                         .with_handle(self.popover_menu_handle.clone()),
-                );
+                )
             }
 
             EditPredictionProvider::Zed => {
@@ -331,7 +331,7 @@ impl Render for EditPredictionButton {
                         })
                     });
 
-                let this = cx.entity().clone();
+                let this = cx.entity();
 
                 let mut popover_menu = PopoverMenu::new("zeta")
                     .menu(move |window, cx| {
@@ -343,7 +343,7 @@ impl Render for EditPredictionButton {
                 let is_refreshing = self
                     .edit_prediction_provider
                     .as_ref()
-                    .map_or(false, |provider| provider.is_refreshing(cx));
+                    .is_some_and(|provider| provider.is_refreshing(cx));
 
                 if is_refreshing {
                     popover_menu = popover_menu.trigger(

crates/editor/src/actions.rs 🔗

@@ -273,6 +273,16 @@ pub enum UuidVersion {
     V7,
 }
 
+/// Splits selection into individual lines.
+#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)]
+#[action(namespace = editor)]
+#[serde(deny_unknown_fields)]
+pub struct SplitSelectionIntoLines {
+    /// Keep the text selected after splitting instead of collapsing to cursors.
+    #[serde(default)]
+    pub keep_selections: bool,
+}
+
 /// Goes to the next diagnostic in the file.
 #[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)]
 #[action(namespace = editor)]
@@ -672,8 +682,6 @@ actions!(
         SortLinesCaseInsensitive,
         /// Sorts selected lines case-sensitively.
         SortLinesCaseSensitive,
-        /// Splits selection into individual lines.
-        SplitSelectionIntoLines,
         /// Stops the language server for the current file.
         StopLanguageServer,
         /// Switches between source and header files.

crates/editor/src/clangd_ext.rs 🔗

@@ -13,7 +13,7 @@ use crate::{Editor, SwitchSourceHeader, element::register_action};
 use project::lsp_store::clangd_ext::CLANGD_SERVER_NAME;
 
 fn is_c_language(language: &Language) -> bool {
-    return language.name() == "C++".into() || language.name() == "C".into();
+    language.name() == "C++".into() || language.name() == "C".into()
 }
 
 pub fn switch_source_header(
@@ -104,6 +104,6 @@ pub fn apply_related_actions(editor: &Entity<Editor>, window: &mut Window, cx: &
         .filter_map(|buffer| buffer.read(cx).language())
         .any(|language| is_c_language(language))
     {
-        register_action(&editor, window, switch_source_header);
+        register_action(editor, window, switch_source_header);
     }
 }

crates/editor/src/code_completion_tests.rs 🔗

@@ -317,7 +317,7 @@ async fn filter_and_sort_matches(
     let candidates: Arc<[StringMatchCandidate]> = completions
         .iter()
         .enumerate()
-        .map(|(id, completion)| StringMatchCandidate::new(id, &completion.label.filter_text()))
+        .map(|(id, completion)| StringMatchCandidate::new(id, completion.label.filter_text()))
         .collect();
     let cancel_flag = Arc::new(AtomicBool::new(false));
     let background_executor = cx.executor();
@@ -331,5 +331,5 @@ async fn filter_and_sort_matches(
         background_executor,
     )
     .await;
-    CompletionsMenu::sort_string_matches(matches, Some(query), snippet_sort_order, &completions)
+    CompletionsMenu::sort_string_matches(matches, Some(query), snippet_sort_order, completions)
 }

crates/editor/src/code_context_menus.rs 🔗

@@ -321,7 +321,7 @@ impl CompletionsMenu {
         let match_candidates = choices
             .iter()
             .enumerate()
-            .map(|(id, completion)| StringMatchCandidate::new(id, &completion))
+            .map(|(id, completion)| StringMatchCandidate::new(id, completion))
             .collect();
         let entries = choices
             .iter()
@@ -514,7 +514,7 @@ impl CompletionsMenu {
         // Expand the range to resolve more completions than are predicted to be visible, to reduce
         // jank on navigation.
         let entry_indices = util::expanded_and_wrapped_usize_range(
-            entry_range.clone(),
+            entry_range,
             RESOLVE_BEFORE_ITEMS,
             RESOLVE_AFTER_ITEMS,
             entries.len(),
@@ -1111,10 +1111,8 @@ impl CompletionsMenu {
             let query_start_doesnt_match_split_words = query_start_lower
                 .map(|query_char| {
                     !split_words(&string_match.string).any(|word| {
-                        word.chars()
-                            .next()
-                            .and_then(|c| c.to_lowercase().next())
-                            .map_or(false, |word_char| word_char == query_char)
+                        word.chars().next().and_then(|c| c.to_lowercase().next())
+                            == Some(query_char)
                     })
                 })
                 .unwrap_or(false);

crates/editor/src/display_map.rs 🔗

@@ -969,13 +969,13 @@ impl DisplaySnapshot {
             if let Some(chunk_highlight) = chunk.highlight_style {
                 // For color inlays, blend the color with the editor background
                 let mut processed_highlight = chunk_highlight;
-                if chunk.is_inlay {
-                    if let Some(inlay_color) = chunk_highlight.color {
-                        // Only blend if the color has transparency (alpha < 1.0)
-                        if inlay_color.a < 1.0 {
-                            let blended_color = editor_style.background.blend(inlay_color);
-                            processed_highlight.color = Some(blended_color);
-                        }
+                if chunk.is_inlay
+                    && let Some(inlay_color) = chunk_highlight.color
+                {
+                    // Only blend if the color has transparency (alpha < 1.0)
+                    if inlay_color.a < 1.0 {
+                        let blended_color = editor_style.background.blend(inlay_color);
+                        processed_highlight.color = Some(blended_color);
                     }
                 }
 
@@ -991,7 +991,7 @@ impl DisplaySnapshot {
             if let Some(severity) = chunk.diagnostic_severity.filter(|severity| {
                 self.diagnostics_max_severity
                     .into_lsp()
-                    .map_or(false, |max_severity| severity <= &max_severity)
+                    .is_some_and(|max_severity| severity <= &max_severity)
             }) {
                 if chunk.is_unnecessary {
                     diagnostic_highlight.fade_out = Some(editor_style.unnecessary_code_fade);
@@ -2351,11 +2351,12 @@ pub mod tests {
                 .highlight_style
                 .and_then(|style| style.color)
                 .map_or(black, |color| color.to_rgb());
-            if let Some((last_chunk, last_severity, last_color)) = chunks.last_mut() {
-                if *last_severity == chunk.diagnostic_severity && *last_color == color {
-                    last_chunk.push_str(chunk.text);
-                    continue;
-                }
+            if let Some((last_chunk, last_severity, last_color)) = chunks.last_mut()
+                && *last_severity == chunk.diagnostic_severity
+                && *last_color == color
+            {
+                last_chunk.push_str(chunk.text);
+                continue;
             }
 
             chunks.push((chunk.text.to_string(), chunk.diagnostic_severity, color));
@@ -2901,11 +2902,12 @@ pub mod tests {
                 .syntax_highlight_id
                 .and_then(|id| id.style(theme)?.color);
             let highlight_color = chunk.highlight_style.and_then(|style| style.color);
-            if let Some((last_chunk, last_syntax_color, last_highlight_color)) = chunks.last_mut() {
-                if syntax_color == *last_syntax_color && highlight_color == *last_highlight_color {
-                    last_chunk.push_str(chunk.text);
-                    continue;
-                }
+            if let Some((last_chunk, last_syntax_color, last_highlight_color)) = chunks.last_mut()
+                && syntax_color == *last_syntax_color
+                && highlight_color == *last_highlight_color
+            {
+                last_chunk.push_str(chunk.text);
+                continue;
             }
             chunks.push((chunk.text.to_string(), syntax_color, highlight_color));
         }

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

@@ -290,7 +290,10 @@ pub enum Block {
     ExcerptBoundary {
         excerpt: ExcerptInfo,
         height: u32,
-        starts_new_buffer: bool,
+    },
+    BufferHeader {
+        excerpt: ExcerptInfo,
+        height: u32,
     },
 }
 
@@ -303,27 +306,37 @@ impl Block {
                 ..
             } => BlockId::ExcerptBoundary(next_excerpt.id),
             Block::FoldedBuffer { first_excerpt, .. } => BlockId::FoldedBuffer(first_excerpt.id),
+            Block::BufferHeader {
+                excerpt: next_excerpt,
+                ..
+            } => BlockId::ExcerptBoundary(next_excerpt.id),
         }
     }
 
     pub fn has_height(&self) -> bool {
         match self {
             Block::Custom(block) => block.height.is_some(),
-            Block::ExcerptBoundary { .. } | Block::FoldedBuffer { .. } => true,
+            Block::ExcerptBoundary { .. }
+            | Block::FoldedBuffer { .. }
+            | Block::BufferHeader { .. } => true,
         }
     }
 
     pub fn height(&self) -> u32 {
         match self {
             Block::Custom(block) => block.height.unwrap_or(0),
-            Block::ExcerptBoundary { height, .. } | Block::FoldedBuffer { height, .. } => *height,
+            Block::ExcerptBoundary { height, .. }
+            | Block::FoldedBuffer { height, .. }
+            | Block::BufferHeader { height, .. } => *height,
         }
     }
 
     pub fn style(&self) -> BlockStyle {
         match self {
             Block::Custom(block) => block.style,
-            Block::ExcerptBoundary { .. } | Block::FoldedBuffer { .. } => BlockStyle::Sticky,
+            Block::ExcerptBoundary { .. }
+            | Block::FoldedBuffer { .. }
+            | Block::BufferHeader { .. } => BlockStyle::Sticky,
         }
     }
 
@@ -332,6 +345,7 @@ impl Block {
             Block::Custom(block) => matches!(block.placement, BlockPlacement::Above(_)),
             Block::FoldedBuffer { .. } => false,
             Block::ExcerptBoundary { .. } => true,
+            Block::BufferHeader { .. } => true,
         }
     }
 
@@ -340,6 +354,7 @@ impl Block {
             Block::Custom(block) => matches!(block.placement, BlockPlacement::Near(_)),
             Block::FoldedBuffer { .. } => false,
             Block::ExcerptBoundary { .. } => false,
+            Block::BufferHeader { .. } => false,
         }
     }
 
@@ -351,6 +366,7 @@ impl Block {
             ),
             Block::FoldedBuffer { .. } => false,
             Block::ExcerptBoundary { .. } => false,
+            Block::BufferHeader { .. } => false,
         }
     }
 
@@ -359,6 +375,7 @@ impl Block {
             Block::Custom(block) => matches!(block.placement, BlockPlacement::Replace(_)),
             Block::FoldedBuffer { .. } => true,
             Block::ExcerptBoundary { .. } => false,
+            Block::BufferHeader { .. } => false,
         }
     }
 
@@ -367,6 +384,7 @@ impl Block {
             Block::Custom(_) => false,
             Block::FoldedBuffer { .. } => true,
             Block::ExcerptBoundary { .. } => true,
+            Block::BufferHeader { .. } => true,
         }
     }
 
@@ -374,9 +392,8 @@ impl Block {
         match self {
             Block::Custom(_) => false,
             Block::FoldedBuffer { .. } => true,
-            Block::ExcerptBoundary {
-                starts_new_buffer, ..
-            } => *starts_new_buffer,
+            Block::ExcerptBoundary { .. } => false,
+            Block::BufferHeader { .. } => true,
         }
     }
 }
@@ -393,14 +410,14 @@ impl Debug for Block {
                 .field("first_excerpt", &first_excerpt)
                 .field("height", height)
                 .finish(),
-            Self::ExcerptBoundary {
-                starts_new_buffer,
-                excerpt,
-                height,
-            } => f
+            Self::ExcerptBoundary { excerpt, height } => f
                 .debug_struct("ExcerptBoundary")
                 .field("excerpt", excerpt)
-                .field("starts_new_buffer", starts_new_buffer)
+                .field("height", height)
+                .finish(),
+            Self::BufferHeader { excerpt, height } => f
+                .debug_struct("BufferHeader")
+                .field("excerpt", excerpt)
                 .field("height", height)
                 .finish(),
         }
@@ -525,26 +542,22 @@ impl BlockMap {
             // * Below blocks that end at the start of the edit
             // However, if we hit a replace block that ends at the start of the edit we want to reconstruct it.
             new_transforms.append(cursor.slice(&old_start, Bias::Left), &());
-            if let Some(transform) = cursor.item() {
-                if transform.summary.input_rows > 0
-                    && cursor.end() == old_start
-                    && transform
-                        .block
-                        .as_ref()
-                        .map_or(true, |b| !b.is_replacement())
-                {
-                    // Preserve the transform (push and next)
-                    new_transforms.push(transform.clone(), &());
-                    cursor.next();
+            if let Some(transform) = cursor.item()
+                && transform.summary.input_rows > 0
+                && cursor.end() == old_start
+                && transform.block.as_ref().is_none_or(|b| !b.is_replacement())
+            {
+                // Preserve the transform (push and next)
+                new_transforms.push(transform.clone(), &());
+                cursor.next();
 
-                    // Preserve below blocks at end of edit
-                    while let Some(transform) = cursor.item() {
-                        if transform.block.as_ref().map_or(false, |b| b.place_below()) {
-                            new_transforms.push(transform.clone(), &());
-                            cursor.next();
-                        } else {
-                            break;
-                        }
+                // Preserve below blocks at end of edit
+                while let Some(transform) = cursor.item() {
+                    if transform.block.as_ref().is_some_and(|b| b.place_below()) {
+                        new_transforms.push(transform.clone(), &());
+                        cursor.next();
+                    } else {
+                        break;
                     }
                 }
             }
@@ -607,7 +620,7 @@ impl BlockMap {
 
             // Discard below blocks at the end of the edit. They'll be reconstructed.
             while let Some(transform) = cursor.item() {
-                if transform.block.as_ref().map_or(false, |b| b.place_below()) {
+                if transform.block.as_ref().is_some_and(|b| b.place_below()) {
                     cursor.next();
                 } else {
                     break;
@@ -657,22 +670,20 @@ impl BlockMap {
                     .iter()
                     .filter_map(|block| {
                         let placement = block.placement.to_wrap_row(wrap_snapshot)?;
-                        if let BlockPlacement::Above(row) = placement {
-                            if row < new_start {
-                                return None;
-                            }
+                        if let BlockPlacement::Above(row) = placement
+                            && row < new_start
+                        {
+                            return None;
                         }
                         Some((placement, Block::Custom(block.clone())))
                     }),
             );
 
-            if buffer.show_headers() {
-                blocks_in_edit.extend(self.header_and_footer_blocks(
-                    buffer,
-                    (start_bound, end_bound),
-                    wrap_snapshot,
-                ));
-            }
+            blocks_in_edit.extend(self.header_and_footer_blocks(
+                buffer,
+                (start_bound, end_bound),
+                wrap_snapshot,
+            ));
 
             BlockMap::sort_blocks(&mut blocks_in_edit);
 
@@ -775,7 +786,7 @@ impl BlockMap {
                     if self.buffers_with_disabled_headers.contains(&new_buffer_id) {
                         continue;
                     }
-                    if self.folded_buffers.contains(&new_buffer_id) {
+                    if self.folded_buffers.contains(&new_buffer_id) && buffer.show_headers() {
                         let mut last_excerpt_end_row = first_excerpt.end_row;
 
                         while let Some(next_boundary) = boundaries.peek() {
@@ -808,20 +819,24 @@ impl BlockMap {
                     }
                 }
 
-                if new_buffer_id.is_some() {
+                let starts_new_buffer = new_buffer_id.is_some();
+                let block = if starts_new_buffer && buffer.show_headers() {
                     height += self.buffer_header_height;
-                } else {
+                    Block::BufferHeader {
+                        excerpt: excerpt_boundary.next,
+                        height,
+                    }
+                } else if excerpt_boundary.prev.is_some() {
                     height += self.excerpt_header_height;
-                }
-
-                return Some((
-                    BlockPlacement::Above(WrapRow(wrap_row)),
                     Block::ExcerptBoundary {
                         excerpt: excerpt_boundary.next,
                         height,
-                        starts_new_buffer: new_buffer_id.is_some(),
-                    },
-                ));
+                    }
+                } else {
+                    continue;
+                };
+
+                return Some((BlockPlacement::Above(WrapRow(wrap_row)), block));
             }
         })
     }
@@ -846,13 +861,25 @@ impl BlockMap {
                     (
                         Block::ExcerptBoundary {
                             excerpt: excerpt_a, ..
+                        }
+                        | Block::BufferHeader {
+                            excerpt: excerpt_a, ..
                         },
                         Block::ExcerptBoundary {
                             excerpt: excerpt_b, ..
+                        }
+                        | Block::BufferHeader {
+                            excerpt: excerpt_b, ..
                         },
                     ) => Some(excerpt_a.id).cmp(&Some(excerpt_b.id)),
-                    (Block::ExcerptBoundary { .. }, Block::Custom(_)) => Ordering::Less,
-                    (Block::Custom(_), Block::ExcerptBoundary { .. }) => Ordering::Greater,
+                    (
+                        Block::ExcerptBoundary { .. } | Block::BufferHeader { .. },
+                        Block::Custom(_),
+                    ) => Ordering::Less,
+                    (
+                        Block::Custom(_),
+                        Block::ExcerptBoundary { .. } | Block::BufferHeader { .. },
+                    ) => Ordering::Greater,
                     (Block::Custom(block_a), Block::Custom(block_b)) => block_a
                         .priority
                         .cmp(&block_b.priority)
@@ -977,10 +1004,10 @@ impl BlockMapReader<'_> {
                 break;
             }
 
-            if let Some(BlockId::Custom(id)) = transform.block.as_ref().map(|block| block.id()) {
-                if id == block_id {
-                    return Some(cursor.start().1);
-                }
+            if let Some(BlockId::Custom(id)) = transform.block.as_ref().map(|block| block.id())
+                && id == block_id
+            {
+                return Some(cursor.start().1);
             }
             cursor.next();
         }
@@ -1299,14 +1326,14 @@ impl BlockSnapshot {
 
         let mut input_start = transform_input_start;
         let mut input_end = transform_input_start;
-        if let Some(transform) = cursor.item() {
-            if transform.block.is_none() {
-                input_start += rows.start - transform_output_start;
-                input_end += cmp::min(
-                    rows.end - transform_output_start,
-                    transform.summary.input_rows,
-                );
-            }
+        if let Some(transform) = cursor.item()
+            && transform.block.is_none()
+        {
+            input_start += rows.start - transform_output_start;
+            input_end += cmp::min(
+                rows.end - transform_output_start,
+                transform.summary.input_rows,
+            );
         }
 
         BlockChunks {
@@ -1329,7 +1356,7 @@ impl BlockSnapshot {
         let Dimensions(output_start, input_start, _) = cursor.start();
         let overshoot = if cursor
             .item()
-            .map_or(false, |transform| transform.block.is_none())
+            .is_some_and(|transform| transform.block.is_none())
         {
             start_row.0 - output_start.0
         } else {
@@ -1359,7 +1386,7 @@ impl BlockSnapshot {
                         && transform
                             .block
                             .as_ref()
-                            .map_or(false, |block| block.height() > 0))
+                            .is_some_and(|block| block.height() > 0))
                 {
                     break;
                 }
@@ -1381,7 +1408,9 @@ impl BlockSnapshot {
 
         while let Some(transform) = cursor.item() {
             match &transform.block {
-                Some(Block::ExcerptBoundary { excerpt, .. }) => {
+                Some(
+                    Block::ExcerptBoundary { excerpt, .. } | Block::BufferHeader { excerpt, .. },
+                ) => {
                     return Some(StickyHeaderExcerpt { excerpt });
                 }
                 Some(block) if block.is_buffer_header() => return None,
@@ -1472,18 +1501,18 @@ impl BlockSnapshot {
                 longest_row_chars = summary.longest_row_chars;
             }
 
-            if let Some(transform) = cursor.item() {
-                if transform.block.is_none() {
-                    let Dimensions(output_start, input_start, _) = cursor.start();
-                    let overshoot = range.end.0 - output_start.0;
-                    let wrap_start_row = input_start.0;
-                    let wrap_end_row = input_start.0 + overshoot;
-                    let summary = self
-                        .wrap_snapshot
-                        .text_summary_for_range(wrap_start_row..wrap_end_row);
-                    if summary.longest_row_chars > longest_row_chars {
-                        longest_row = BlockRow(output_start.0 + summary.longest_row);
-                    }
+            if let Some(transform) = cursor.item()
+                && transform.block.is_none()
+            {
+                let Dimensions(output_start, input_start, _) = cursor.start();
+                let overshoot = range.end.0 - output_start.0;
+                let wrap_start_row = input_start.0;
+                let wrap_end_row = input_start.0 + overshoot;
+                let summary = self
+                    .wrap_snapshot
+                    .text_summary_for_range(wrap_start_row..wrap_end_row);
+                if summary.longest_row_chars > longest_row_chars {
+                    longest_row = BlockRow(output_start.0 + summary.longest_row);
                 }
             }
         }
@@ -1512,7 +1541,7 @@ 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().map_or(false, |t| t.block.is_some())
+        cursor.item().is_some_and(|t| t.block.is_some())
     }
 
     pub(super) fn is_folded_buffer_header(&self, row: BlockRow) -> bool {
@@ -1530,11 +1559,11 @@ impl BlockSnapshot {
             .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().map_or(false, |transform| {
+        cursor.item().is_some_and(|transform| {
             transform
                 .block
                 .as_ref()
-                .map_or(false, |block| block.is_replacement())
+                .is_some_and(|block| block.is_replacement())
         })
     }
 
@@ -1557,12 +1586,11 @@ impl BlockSnapshot {
 
                 match transform.block.as_ref() {
                     Some(block) => {
-                        if block.is_replacement() {
-                            if ((bias == Bias::Left || search_left) && output_start <= point.0)
-                                || (!search_left && output_start >= point.0)
-                            {
-                                return BlockPoint(output_start);
-                            }
+                        if block.is_replacement()
+                            && (((bias == Bias::Left || search_left) && output_start <= point.0)
+                                || (!search_left && output_start >= point.0))
+                        {
+                            return BlockPoint(output_start);
                         }
                     }
                     None => {
@@ -1655,7 +1683,7 @@ impl BlockChunks<'_> {
             if transform
                 .block
                 .as_ref()
-                .map_or(false, |block| block.height() == 0)
+                .is_some_and(|block| block.height() == 0)
             {
                 self.transforms.next();
             } else {
@@ -1666,7 +1694,7 @@ impl BlockChunks<'_> {
         if self
             .transforms
             .item()
-            .map_or(false, |transform| transform.block.is_none())
+            .is_some_and(|transform| transform.block.is_none())
         {
             let start_input_row = self.transforms.start().1.0;
             let start_output_row = self.transforms.start().0.0;
@@ -1776,7 +1804,7 @@ impl Iterator for BlockRows<'_> {
                 if transform
                     .block
                     .as_ref()
-                    .map_or(false, |block| block.height() == 0)
+                    .is_some_and(|block| block.height() == 0)
                 {
                     self.transforms.next();
                 } else {
@@ -1788,7 +1816,7 @@ impl Iterator for BlockRows<'_> {
             if transform
                 .block
                 .as_ref()
-                .map_or(true, |block| block.is_replacement())
+                .is_none_or(|block| block.is_replacement())
             {
                 self.input_rows.seek(self.transforms.start().1.0);
             }
@@ -2161,7 +2189,7 @@ mod tests {
         }
 
         let multi_buffer_snapshot = multi_buffer.read(cx).snapshot(cx);
-        let (_, inlay_snapshot) = InlayMap::new(multi_buffer_snapshot.clone());
+        let (_, inlay_snapshot) = InlayMap::new(multi_buffer_snapshot);
         let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
         let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
         let (_, wraps_snapshot) = WrapMap::new(tab_snapshot, font, font_size, Some(wrap_width), cx);
@@ -2280,7 +2308,7 @@ mod tests {
             new_heights.insert(block_ids[0], 3);
             block_map_writer.resize(new_heights);
 
-            let snapshot = block_map.read(wraps_snapshot.clone(), Default::default());
+            let snapshot = block_map.read(wraps_snapshot, Default::default());
             // Same height as before, should remain the same
             assert_eq!(snapshot.text(), "aaa\n\n\n\n\n\nbbb\nccc\nddd\n\n\n");
         }
@@ -2290,8 +2318,6 @@ mod tests {
     fn test_blocks_on_wrapped_lines(cx: &mut gpui::TestAppContext) {
         cx.update(init_test);
 
-        let _font_id = cx.text_system().font_id(&font("Helvetica")).unwrap();
-
         let text = "one two three\nfour five six\nseven eight";
 
         let buffer = cx.update(|cx| MultiBuffer::build_simple(text, cx));
@@ -2367,16 +2393,14 @@ mod tests {
             buffer.edit([(Point::new(2, 0)..Point::new(3, 0), "")], None, cx);
             buffer.snapshot(cx)
         });
-        let (inlay_snapshot, inlay_edits) = inlay_map.sync(
-            buffer_snapshot.clone(),
-            buffer_subscription.consume().into_inner(),
-        );
+        let (inlay_snapshot, inlay_edits) =
+            inlay_map.sync(buffer_snapshot, buffer_subscription.consume().into_inner());
         let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
         let (tab_snapshot, tab_edits) = tab_map.sync(fold_snapshot, fold_edits, tab_size);
         let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
             wrap_map.sync(tab_snapshot, tab_edits, cx)
         });
-        let blocks_snapshot = block_map.read(wraps_snapshot.clone(), wrap_edits);
+        let blocks_snapshot = block_map.read(wraps_snapshot, wrap_edits);
         assert_eq!(blocks_snapshot.text(), "line1\n\n\n\n\nline5");
 
         let buffer_snapshot = buffer.update(cx, |buffer, cx| {
@@ -2461,7 +2485,7 @@ mod tests {
         // Removing the replace block shows all the hidden blocks again.
         let mut writer = block_map.write(wraps_snapshot.clone(), Default::default());
         writer.remove(HashSet::from_iter([replace_block_id]));
-        let blocks_snapshot = block_map.read(wraps_snapshot.clone(), Default::default());
+        let blocks_snapshot = block_map.read(wraps_snapshot, Default::default());
         assert_eq!(
             blocks_snapshot.text(),
             "\nline1\n\nline2\n\n\nline 2.1\nline2.2\nline 2.3\nline 2.4\n\nline4\n\nline5"
@@ -2800,7 +2824,7 @@ mod tests {
         buffer.read_with(cx, |buffer, cx| {
             writer.fold_buffers([buffer_id_3], buffer, cx);
         });
-        let blocks_snapshot = block_map.read(wrap_snapshot.clone(), Patch::default());
+        let blocks_snapshot = block_map.read(wrap_snapshot, Patch::default());
         let blocks = blocks_snapshot
             .blocks_in_range(0..u32::MAX)
             .collect::<Vec<_>>();
@@ -2853,7 +2877,7 @@ mod tests {
         assert_eq!(buffer_ids.len(), 1);
         let buffer_id = buffer_ids[0];
 
-        let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+        let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot);
         let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
         let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
         let (_, wrap_snapshot) =
@@ -2867,7 +2891,7 @@ mod tests {
         buffer.read_with(cx, |buffer, cx| {
             writer.fold_buffers([buffer_id], buffer, cx);
         });
-        let blocks_snapshot = block_map.read(wrap_snapshot.clone(), Patch::default());
+        let blocks_snapshot = block_map.read(wrap_snapshot, Patch::default());
         let blocks = blocks_snapshot
             .blocks_in_range(0..u32::MAX)
             .collect::<Vec<_>>();
@@ -2875,12 +2899,7 @@ mod tests {
             1,
             blocks
                 .iter()
-                .filter(|(_, block)| {
-                    match block {
-                        Block::FoldedBuffer { .. } => true,
-                        _ => false,
-                    }
-                })
+                .filter(|(_, block)| { matches!(block, Block::FoldedBuffer { .. }) })
                 .count(),
             "Should have one folded block, producing a header of the second buffer"
         );
@@ -3197,9 +3216,9 @@ mod tests {
             // so we special case row 0 to assume a leading '\n'.
             //
             // Linehood is the birthright of strings.
-            let mut input_text_lines = input_text.split('\n').enumerate().peekable();
+            let input_text_lines = input_text.split('\n').enumerate().peekable();
             let mut block_row = 0;
-            while let Some((wrap_row, input_line)) = input_text_lines.next() {
+            for (wrap_row, input_line) in input_text_lines {
                 let wrap_row = wrap_row as u32;
                 let multibuffer_row = wraps_snapshot
                     .to_point(WrapPoint::new(wrap_row, 0), Bias::Left)
@@ -3230,34 +3249,32 @@ mod tests {
                 let mut is_in_replace_block = false;
                 if let Some((BlockPlacement::Replace(replace_range), block)) =
                     sorted_blocks_iter.peek()
+                    && wrap_row >= replace_range.start().0
                 {
-                    if wrap_row >= replace_range.start().0 {
-                        is_in_replace_block = true;
+                    is_in_replace_block = true;
 
-                        if wrap_row == replace_range.start().0 {
-                            if matches!(block, Block::FoldedBuffer { .. }) {
-                                expected_buffer_rows.push(None);
-                            } else {
-                                expected_buffer_rows
-                                    .push(input_buffer_rows[multibuffer_row as usize]);
-                            }
+                    if wrap_row == replace_range.start().0 {
+                        if matches!(block, Block::FoldedBuffer { .. }) {
+                            expected_buffer_rows.push(None);
+                        } else {
+                            expected_buffer_rows.push(input_buffer_rows[multibuffer_row as usize]);
                         }
+                    }
 
-                        if wrap_row == replace_range.end().0 {
-                            expected_block_positions.push((block_row, block.id()));
-                            let text = "\n".repeat((block.height() - 1) as usize);
-                            if block_row > 0 {
-                                expected_text.push('\n');
-                            }
-                            expected_text.push_str(&text);
-
-                            for _ in 1..block.height() {
-                                expected_buffer_rows.push(None);
-                            }
-                            block_row += block.height();
+                    if wrap_row == replace_range.end().0 {
+                        expected_block_positions.push((block_row, block.id()));
+                        let text = "\n".repeat((block.height() - 1) as usize);
+                        if block_row > 0 {
+                            expected_text.push('\n');
+                        }
+                        expected_text.push_str(&text);
 
-                            sorted_blocks_iter.next();
+                        for _ in 1..block.height() {
+                            expected_buffer_rows.push(None);
                         }
+                        block_row += block.height();
+
+                        sorted_blocks_iter.next();
                     }
                 }
 
@@ -3541,7 +3558,7 @@ mod tests {
                 ..buffer_snapshot.anchor_after(Point::new(1, 0))],
             false,
         );
-        let blocks_snapshot = block_map.read(wraps_snapshot.clone(), Default::default());
+        let blocks_snapshot = block_map.read(wraps_snapshot, Default::default());
         assert_eq!(blocks_snapshot.text(), "abc\n\ndef\nghi\njkl\nmno");
     }
 

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

@@ -77,7 +77,7 @@ fn create_highlight_endpoints(
             let ranges = &text_highlights.1;
 
             let start_ix = match ranges.binary_search_by(|probe| {
-                let cmp = probe.end.cmp(&start, &buffer);
+                let cmp = probe.end.cmp(&start, buffer);
                 if cmp.is_gt() {
                     cmp::Ordering::Greater
                 } else {
@@ -88,18 +88,18 @@ fn create_highlight_endpoints(
             };
 
             for range in &ranges[start_ix..] {
-                if range.start.cmp(&end, &buffer).is_ge() {
+                if range.start.cmp(&end, buffer).is_ge() {
                     break;
                 }
 
                 highlight_endpoints.push(HighlightEndpoint {
-                    offset: range.start.to_offset(&buffer),
+                    offset: range.start.to_offset(buffer),
                     is_start: true,
                     tag,
                     style,
                 });
                 highlight_endpoints.push(HighlightEndpoint {
-                    offset: range.end.to_offset(&buffer),
+                    offset: range.end.to_offset(buffer),
                     is_start: false,
                     tag,
                     style,

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

@@ -289,25 +289,25 @@ impl FoldMapWriter<'_> {
             let ChunkRendererId::Fold(id) = id else {
                 continue;
             };
-            if let Some(metadata) = self.0.snapshot.fold_metadata_by_id.get(&id).cloned() {
-                if Some(new_width) != metadata.width {
-                    let buffer_start = metadata.range.start.to_offset(buffer);
-                    let buffer_end = metadata.range.end.to_offset(buffer);
-                    let inlay_range = inlay_snapshot.to_inlay_offset(buffer_start)
-                        ..inlay_snapshot.to_inlay_offset(buffer_end);
-                    edits.push(InlayEdit {
-                        old: inlay_range.clone(),
-                        new: inlay_range.clone(),
-                    });
+            if let Some(metadata) = self.0.snapshot.fold_metadata_by_id.get(&id).cloned()
+                && Some(new_width) != metadata.width
+            {
+                let buffer_start = metadata.range.start.to_offset(buffer);
+                let buffer_end = metadata.range.end.to_offset(buffer);
+                let inlay_range = inlay_snapshot.to_inlay_offset(buffer_start)
+                    ..inlay_snapshot.to_inlay_offset(buffer_end);
+                edits.push(InlayEdit {
+                    old: inlay_range.clone(),
+                    new: inlay_range.clone(),
+                });
 
-                    self.0.snapshot.fold_metadata_by_id.insert(
-                        id,
-                        FoldMetadata {
-                            range: metadata.range,
-                            width: Some(new_width),
-                        },
-                    );
-                }
+                self.0.snapshot.fold_metadata_by_id.insert(
+                    id,
+                    FoldMetadata {
+                        range: metadata.range,
+                        width: Some(new_width),
+                    },
+                );
             }
         }
 
@@ -417,18 +417,18 @@ impl FoldMap {
             cursor.seek(&InlayOffset(0), Bias::Right);
 
             while let Some(mut edit) = inlay_edits_iter.next() {
-                if let Some(item) = cursor.item() {
-                    if !item.is_fold() {
-                        new_transforms.update_last(
-                            |transform| {
-                                if !transform.is_fold() {
-                                    transform.summary.add_summary(&item.summary, &());
-                                    cursor.next();
-                                }
-                            },
-                            &(),
-                        );
-                    }
+                if let Some(item) = cursor.item()
+                    && !item.is_fold()
+                {
+                    new_transforms.update_last(
+                        |transform| {
+                            if !transform.is_fold() {
+                                transform.summary.add_summary(&item.summary, &());
+                                cursor.next();
+                            }
+                        },
+                        &(),
+                    );
                 }
                 new_transforms.append(cursor.slice(&edit.old.start, Bias::Left), &());
                 edit.new.start -= edit.old.start - *cursor.start();
@@ -491,14 +491,14 @@ impl FoldMap {
 
                 while folds
                     .peek()
-                    .map_or(false, |(_, fold_range)| fold_range.start < edit.new.end)
+                    .is_some_and(|(_, fold_range)| fold_range.start < edit.new.end)
                 {
                     let (fold, mut fold_range) = folds.next().unwrap();
                     let sum = new_transforms.summary();
 
                     assert!(fold_range.start.0 >= sum.input.len);
 
-                    while folds.peek().map_or(false, |(next_fold, next_fold_range)| {
+                    while folds.peek().is_some_and(|(next_fold, next_fold_range)| {
                         next_fold_range.start < fold_range.end
                             || (next_fold_range.start == fold_range.end
                                 && fold.placeholder.merge_adjacent
@@ -575,14 +575,14 @@ impl FoldMap {
 
                 for mut edit in inlay_edits {
                     old_transforms.seek(&edit.old.start, Bias::Left);
-                    if old_transforms.item().map_or(false, |t| t.is_fold()) {
+                    if old_transforms.item().is_some_and(|t| t.is_fold()) {
                         edit.old.start = old_transforms.start().0;
                     }
                     let old_start =
                         old_transforms.start().1.0 + (edit.old.start - old_transforms.start().0).0;
 
                     old_transforms.seek_forward(&edit.old.end, Bias::Right);
-                    if old_transforms.item().map_or(false, |t| t.is_fold()) {
+                    if old_transforms.item().is_some_and(|t| t.is_fold()) {
                         old_transforms.next();
                         edit.old.end = old_transforms.start().0;
                     }
@@ -590,14 +590,14 @@ impl FoldMap {
                         old_transforms.start().1.0 + (edit.old.end - old_transforms.start().0).0;
 
                     new_transforms.seek(&edit.new.start, Bias::Left);
-                    if new_transforms.item().map_or(false, |t| t.is_fold()) {
+                    if new_transforms.item().is_some_and(|t| t.is_fold()) {
                         edit.new.start = new_transforms.start().0;
                     }
                     let new_start =
                         new_transforms.start().1.0 + (edit.new.start - new_transforms.start().0).0;
 
                     new_transforms.seek_forward(&edit.new.end, Bias::Right);
-                    if new_transforms.item().map_or(false, |t| t.is_fold()) {
+                    if new_transforms.item().is_some_and(|t| t.is_fold()) {
                         new_transforms.next();
                         edit.new.end = new_transforms.start().0;
                     }
@@ -709,7 +709,7 @@ impl FoldSnapshot {
             .transforms
             .cursor::<Dimensions<InlayPoint, FoldPoint>>(&());
         cursor.seek(&point, Bias::Right);
-        if cursor.item().map_or(false, |t| t.is_fold()) {
+        if cursor.item().is_some_and(|t| t.is_fold()) {
             if bias == Bias::Left || point == cursor.start().0 {
                 cursor.start().1
             } else {
@@ -788,7 +788,7 @@ impl FoldSnapshot {
         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().map_or(false, |t| t.placeholder.is_some())
+        cursor.item().is_some_and(|t| t.placeholder.is_some())
     }
 
     pub fn is_line_folded(&self, buffer_row: MultiBufferRow) -> bool {
@@ -839,7 +839,7 @@ impl FoldSnapshot {
 
         let inlay_end = if transform_cursor
             .item()
-            .map_or(true, |transform| transform.is_fold())
+            .is_none_or(|transform| transform.is_fold())
         {
             inlay_start
         } else if range.end < transform_end.0 {
@@ -1348,7 +1348,7 @@ impl FoldChunks<'_> {
         let inlay_end = if self
             .transform_cursor
             .item()
-            .map_or(true, |transform| transform.is_fold())
+            .is_none_or(|transform| transform.is_fold())
         {
             inlay_start
         } else if range.end < transform_end.0 {
@@ -1463,7 +1463,7 @@ impl FoldOffset {
             .transforms
             .cursor::<Dimensions<FoldOffset, TransformSummary>>(&());
         cursor.seek(&self, Bias::Right);
-        let overshoot = if cursor.item().map_or(true, |t| t.is_fold()) {
+        let overshoot = if cursor.item().is_none_or(|t| t.is_fold()) {
             Point::new(0, (self.0 - cursor.start().0.0) as u32)
         } else {
             let inlay_offset = cursor.start().1.input.len + self.0 - cursor.start().0.0;
@@ -1557,7 +1557,7 @@ mod tests {
         let buffer = MultiBuffer::build_simple(&sample_text(5, 6, 'a'), cx);
         let subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
         let buffer_snapshot = buffer.read(cx).snapshot(cx);
-        let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+        let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot);
         let mut map = FoldMap::new(inlay_snapshot.clone()).0;
 
         let (mut writer, _, _) = map.write(inlay_snapshot, vec![]);
@@ -1636,7 +1636,7 @@ mod tests {
         let buffer = MultiBuffer::build_simple("abcdefghijkl", cx);
         let subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
         let buffer_snapshot = buffer.read(cx).snapshot(cx);
-        let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+        let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot);
 
         {
             let mut map = FoldMap::new(inlay_snapshot.clone()).0;
@@ -1712,7 +1712,7 @@ mod tests {
         let buffer = MultiBuffer::build_simple(&sample_text(5, 6, 'a'), cx);
         let subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
         let buffer_snapshot = buffer.read(cx).snapshot(cx);
-        let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+        let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot);
         let mut map = FoldMap::new(inlay_snapshot.clone()).0;
 
         let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]);
@@ -1720,7 +1720,7 @@ mod tests {
             (Point::new(0, 2)..Point::new(2, 2), FoldPlaceholder::test()),
             (Point::new(3, 1)..Point::new(4, 1), FoldPlaceholder::test()),
         ]);
-        let (snapshot, _) = map.read(inlay_snapshot.clone(), vec![]);
+        let (snapshot, _) = map.read(inlay_snapshot, vec![]);
         assert_eq!(snapshot.text(), "aa⋯cccc\nd⋯eeeee");
 
         let buffer_snapshot = buffer.update(cx, |buffer, cx| {
@@ -1747,7 +1747,7 @@ mod tests {
             (Point::new(1, 2)..Point::new(3, 2), FoldPlaceholder::test()),
             (Point::new(3, 1)..Point::new(4, 1), FoldPlaceholder::test()),
         ]);
-        let (snapshot, _) = map.read(inlay_snapshot.clone(), vec![]);
+        let (snapshot, _) = map.read(inlay_snapshot, vec![]);
         let fold_ranges = snapshot
             .folds_in_range(Point::new(1, 0)..Point::new(1, 3))
             .map(|fold| {
@@ -1782,7 +1782,7 @@ mod tests {
         let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
         let mut map = FoldMap::new(inlay_snapshot.clone()).0;
 
-        let (mut initial_snapshot, _) = map.read(inlay_snapshot.clone(), vec![]);
+        let (mut initial_snapshot, _) = map.read(inlay_snapshot, vec![]);
         let mut snapshot_edits = Vec::new();
 
         let mut next_inlay_id = 0;

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

@@ -48,7 +48,7 @@ pub struct Inlay {
 impl Inlay {
     pub fn hint(id: usize, position: Anchor, hint: &project::InlayHint) -> Self {
         let mut text = hint.text();
-        if hint.padding_right && text.chars_at(text.len().saturating_sub(1)).next() != Some(' ') {
+        if hint.padding_right && text.reversed_chars_at(text.len()).next() != Some(' ') {
             text.push(" ");
         }
         if hint.padding_left && text.chars_at(0).next() != Some(' ') {
@@ -557,11 +557,11 @@ impl InlayMap {
             let mut buffer_edits_iter = buffer_edits.iter().peekable();
             while let Some(buffer_edit) = buffer_edits_iter.next() {
                 new_transforms.append(cursor.slice(&buffer_edit.old.start, Bias::Left), &());
-                if let Some(Transform::Isomorphic(transform)) = cursor.item() {
-                    if cursor.end().0 == buffer_edit.old.start {
-                        push_isomorphic(&mut new_transforms, *transform);
-                        cursor.next();
-                    }
+                if let Some(Transform::Isomorphic(transform)) = cursor.item()
+                    && cursor.end().0 == buffer_edit.old.start
+                {
+                    push_isomorphic(&mut new_transforms, *transform);
+                    cursor.next();
                 }
 
                 // Remove all the inlays and transforms contained by the edit.
@@ -625,7 +625,7 @@ impl InlayMap {
                 // we can push its remainder.
                 if buffer_edits_iter
                     .peek()
-                    .map_or(true, |edit| edit.old.start >= cursor.end().0)
+                    .is_none_or(|edit| edit.old.start >= cursor.end().0)
                 {
                     let transform_start = new_transforms.summary().input.len;
                     let transform_end =
@@ -1305,6 +1305,29 @@ mod tests {
         );
     }
 
+    #[gpui::test]
+    fn test_inlay_hint_padding_with_multibyte_chars() {
+        assert_eq!(
+            Inlay::hint(
+                0,
+                Anchor::min(),
+                &InlayHint {
+                    label: InlayHintLabel::String("🎨".to_string()),
+                    position: text::Anchor::default(),
+                    padding_left: true,
+                    padding_right: true,
+                    tooltip: None,
+                    kind: None,
+                    resolve_state: ResolveState::Resolved,
+                },
+            )
+            .text
+            .to_string(),
+            " 🎨 ",
+            "Should pad single emoji correctly"
+        );
+    }
+
     #[gpui::test]
     fn test_basic_inlays(cx: &mut App) {
         let buffer = MultiBuffer::build_simple("abcdefghi", cx);

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

@@ -36,8 +36,8 @@ pub fn is_invisible(c: char) -> bool {
     } else if c >= '\u{7f}' {
         c <= '\u{9f}'
             || (c.is_whitespace() && c != IDEOGRAPHIC_SPACE)
-            || contains(c, &FORMAT)
-            || contains(c, &OTHER)
+            || contains(c, FORMAT)
+            || contains(c, OTHER)
     } else {
         false
     }
@@ -50,7 +50,7 @@ pub fn replacement(c: char) -> Option<&'static str> {
         Some(C0_SYMBOLS[c as usize])
     } else if c == '\x7f' {
         Some(DEL)
-    } else if contains(c, &PRESERVE) {
+    } else if contains(c, PRESERVE) {
         None
     } else {
         Some("\u{2007}") // fixed width space
@@ -61,14 +61,14 @@ pub fn replacement(c: char) -> Option<&'static str> {
 // but could if we tracked state in the classifier.
 const IDEOGRAPHIC_SPACE: char = '\u{3000}';
 
-const C0_SYMBOLS: &'static [&'static str] = &[
+const C0_SYMBOLS: &[&str] = &[
     "␀", "␁", "␂", "␃", "␄", "␅", "␆", "␇", "␈", "␉", "␊", "␋", "␌", "␍", "␎", "␏", "␐", "␑", "␒",
     "␓", "␔", "␕", "␖", "␗", "␘", "␙", "␚", "␛", "␜", "␝", "␞", "␟",
 ];
-const DEL: &'static str = "␡";
+const DEL: &str = "␡";
 
 // generated using ucd-generate: ucd-generate general-category --include Format --chars ucd-16.0.0
-pub const FORMAT: &'static [(char, char)] = &[
+pub const FORMAT: &[(char, char)] = &[
     ('\u{ad}', '\u{ad}'),
     ('\u{600}', '\u{605}'),
     ('\u{61c}', '\u{61c}'),
@@ -93,7 +93,7 @@ pub const FORMAT: &'static [(char, char)] = &[
 ];
 
 // hand-made base on https://invisible-characters.com (Excluding Cf)
-pub const OTHER: &'static [(char, char)] = &[
+pub const OTHER: &[(char, char)] = &[
     ('\u{034f}', '\u{034f}'),
     ('\u{115F}', '\u{1160}'),
     ('\u{17b4}', '\u{17b5}'),
@@ -107,7 +107,7 @@ pub const OTHER: &'static [(char, char)] = &[
 ];
 
 // a subset of FORMAT/OTHER that may appear within glyphs
-const PRESERVE: &'static [(char, char)] = &[
+const PRESERVE: &[(char, char)] = &[
     ('\u{034f}', '\u{034f}'),
     ('\u{200d}', '\u{200d}'),
     ('\u{17b4}', '\u{17b5}'),

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

@@ -116,7 +116,7 @@ impl TabMap {
                             state.new.end = edit.new.end;
                             Some(None) // Skip this edit, it's merged
                         } else {
-                            let new_state = edit.clone();
+                            let new_state = edit;
                             let result = Some(Some(state.clone())); // Yield the previous edit
                             **state = new_state;
                             result
@@ -611,7 +611,7 @@ mod tests {
     fn test_expand_tabs(cx: &mut gpui::App) {
         let buffer = MultiBuffer::build_simple("", cx);
         let buffer_snapshot = buffer.read(cx).snapshot(cx);
-        let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+        let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot);
         let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
         let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
 
@@ -628,7 +628,7 @@ mod tests {
 
         let buffer = MultiBuffer::build_simple(input, cx);
         let buffer_snapshot = buffer.read(cx).snapshot(cx);
-        let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+        let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot);
         let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
         let (_, mut tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
 
@@ -675,7 +675,7 @@ mod tests {
 
         let buffer = MultiBuffer::build_simple(input, cx);
         let buffer_snapshot = buffer.read(cx).snapshot(cx);
-        let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+        let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot);
         let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
         let (_, mut tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
 
@@ -689,7 +689,7 @@ mod tests {
 
         let buffer = MultiBuffer::build_simple(input, cx);
         let buffer_snapshot = buffer.read(cx).snapshot(cx);
-        let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+        let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot);
         let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
         let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
 
@@ -749,7 +749,7 @@ mod tests {
         let buffer_snapshot = buffer.read(cx).snapshot(cx);
         log::info!("Buffer text: {:?}", buffer_snapshot.text());
 
-        let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+        let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot);
         log::info!("InlayMap text: {:?}", inlay_snapshot.text());
         let (mut fold_map, _) = FoldMap::new(inlay_snapshot.clone());
         fold_map.randomly_mutate(&mut rng);
@@ -758,7 +758,7 @@ mod tests {
         let (inlay_snapshot, _) = inlay_map.randomly_mutate(&mut 0, &mut rng);
         log::info!("InlayMap text: {:?}", inlay_snapshot.text());
 
-        let (mut tab_map, _) = TabMap::new(fold_snapshot.clone(), tab_size);
+        let (mut tab_map, _) = TabMap::new(fold_snapshot, tab_size);
         let tabs_snapshot = tab_map.set_max_expansion_column(32);
 
         let text = text::Rope::from(tabs_snapshot.text().as_str());

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

@@ -74,10 +74,10 @@ impl WrapRows<'_> {
         self.transforms
             .seek(&WrapPoint::new(start_row, 0), Bias::Left);
         let mut input_row = self.transforms.start().1.row();
-        if self.transforms.item().map_or(false, |t| t.is_isomorphic()) {
+        if self.transforms.item().is_some_and(|t| t.is_isomorphic()) {
             input_row += start_row - self.transforms.start().0.row();
         }
-        self.soft_wrapped = self.transforms.item().map_or(false, |t| !t.is_isomorphic());
+        self.soft_wrapped = self.transforms.item().is_some_and(|t| !t.is_isomorphic());
         self.input_buffer_rows.seek(input_row);
         self.input_buffer_row = self.input_buffer_rows.next().unwrap();
         self.output_row = start_row;
@@ -249,48 +249,48 @@ impl WrapMap {
             return;
         }
 
-        if let Some(wrap_width) = self.wrap_width {
-            if self.background_task.is_none() {
-                let pending_edits = self.pending_edits.clone();
-                let mut snapshot = self.snapshot.clone();
-                let text_system = cx.text_system().clone();
-                let (font, font_size) = self.font_with_size.clone();
-                let update_task = cx.background_spawn(async move {
-                    let mut edits = Patch::default();
-                    let mut line_wrapper = text_system.line_wrapper(font, font_size);
-                    for (tab_snapshot, tab_edits) in pending_edits {
-                        let wrap_edits = snapshot
-                            .update(tab_snapshot, &tab_edits, wrap_width, &mut line_wrapper)
-                            .await;
-                        edits = edits.compose(&wrap_edits);
-                    }
-                    (snapshot, edits)
-                });
+        if let Some(wrap_width) = self.wrap_width
+            && self.background_task.is_none()
+        {
+            let pending_edits = self.pending_edits.clone();
+            let mut snapshot = self.snapshot.clone();
+            let text_system = cx.text_system().clone();
+            let (font, font_size) = self.font_with_size.clone();
+            let update_task = cx.background_spawn(async move {
+                let mut edits = Patch::default();
+                let mut line_wrapper = text_system.line_wrapper(font, font_size);
+                for (tab_snapshot, tab_edits) in pending_edits {
+                    let wrap_edits = snapshot
+                        .update(tab_snapshot, &tab_edits, wrap_width, &mut line_wrapper)
+                        .await;
+                    edits = edits.compose(&wrap_edits);
+                }
+                (snapshot, edits)
+            });
 
-                match cx
-                    .background_executor()
-                    .block_with_timeout(Duration::from_millis(1), update_task)
-                {
-                    Ok((snapshot, output_edits)) => {
-                        self.snapshot = snapshot;
-                        self.edits_since_sync = self.edits_since_sync.compose(&output_edits);
-                    }
-                    Err(update_task) => {
-                        self.background_task = Some(cx.spawn(async move |this, cx| {
-                            let (snapshot, edits) = update_task.await;
-                            this.update(cx, |this, cx| {
-                                this.snapshot = snapshot;
-                                this.edits_since_sync = this
-                                    .edits_since_sync
-                                    .compose(mem::take(&mut this.interpolated_edits).invert())
-                                    .compose(&edits);
-                                this.background_task = None;
-                                this.flush_edits(cx);
-                                cx.notify();
-                            })
-                            .ok();
-                        }));
-                    }
+            match cx
+                .background_executor()
+                .block_with_timeout(Duration::from_millis(1), update_task)
+            {
+                Ok((snapshot, output_edits)) => {
+                    self.snapshot = snapshot;
+                    self.edits_since_sync = self.edits_since_sync.compose(&output_edits);
+                }
+                Err(update_task) => {
+                    self.background_task = Some(cx.spawn(async move |this, cx| {
+                        let (snapshot, edits) = update_task.await;
+                        this.update(cx, |this, cx| {
+                            this.snapshot = snapshot;
+                            this.edits_since_sync = this
+                                .edits_since_sync
+                                .compose(mem::take(&mut this.interpolated_edits).invert())
+                                .compose(&edits);
+                            this.background_task = None;
+                            this.flush_edits(cx);
+                            cx.notify();
+                        })
+                        .ok();
+                    }));
                 }
             }
         }
@@ -603,7 +603,7 @@ impl WrapSnapshot {
             .cursor::<Dimensions<WrapPoint, TabPoint>>(&());
         transforms.seek(&output_start, Bias::Right);
         let mut input_start = TabPoint(transforms.start().1.0);
-        if transforms.item().map_or(false, |t| t.is_isomorphic()) {
+        if transforms.item().is_some_and(|t| t.is_isomorphic()) {
             input_start.0 += output_start.0 - transforms.start().0.0;
         }
         let input_end = self
@@ -634,7 +634,7 @@ impl WrapSnapshot {
         cursor.seek(&WrapPoint::new(row + 1, 0), Bias::Left);
         if cursor
             .item()
-            .map_or(false, |transform| transform.is_isomorphic())
+            .is_some_and(|transform| transform.is_isomorphic())
         {
             let overshoot = row - cursor.start().0.row();
             let tab_row = cursor.start().1.row() + overshoot;
@@ -732,10 +732,10 @@ impl WrapSnapshot {
             .cursor::<Dimensions<WrapPoint, TabPoint>>(&());
         transforms.seek(&WrapPoint::new(start_row, 0), Bias::Left);
         let mut input_row = transforms.start().1.row();
-        if transforms.item().map_or(false, |t| t.is_isomorphic()) {
+        if transforms.item().is_some_and(|t| t.is_isomorphic()) {
             input_row += start_row - transforms.start().0.row();
         }
-        let soft_wrapped = transforms.item().map_or(false, |t| !t.is_isomorphic());
+        let soft_wrapped = transforms.item().is_some_and(|t| !t.is_isomorphic());
         let mut input_buffer_rows = self.tab_snapshot.rows(input_row);
         let input_buffer_row = input_buffer_rows.next().unwrap();
         WrapRows {
@@ -754,7 +754,7 @@ impl WrapSnapshot {
             .cursor::<Dimensions<WrapPoint, TabPoint>>(&());
         cursor.seek(&point, Bias::Right);
         let mut tab_point = cursor.start().1.0;
-        if cursor.item().map_or(false, |t| t.is_isomorphic()) {
+        if cursor.item().is_some_and(|t| t.is_isomorphic()) {
             tab_point += point.0 - cursor.start().0.0;
         }
         TabPoint(tab_point)
@@ -780,7 +780,7 @@ impl WrapSnapshot {
         if bias == Bias::Left {
             let mut cursor = self.transforms.cursor::<WrapPoint>(&());
             cursor.seek(&point, Bias::Right);
-            if cursor.item().map_or(false, |t| !t.is_isomorphic()) {
+            if cursor.item().is_some_and(|t| !t.is_isomorphic()) {
                 point = *cursor.start();
                 *point.column_mut() -= 1;
             }
@@ -901,7 +901,7 @@ impl WrapChunks<'_> {
         let output_end = WrapPoint::new(rows.end, 0);
         self.transforms.seek(&output_start, Bias::Right);
         let mut input_start = TabPoint(self.transforms.start().1.0);
-        if self.transforms.item().map_or(false, |t| t.is_isomorphic()) {
+        if self.transforms.item().is_some_and(|t| t.is_isomorphic()) {
             input_start.0 += output_start.0 - self.transforms.start().0.0;
         }
         let input_end = self
@@ -993,7 +993,7 @@ impl Iterator for WrapRows<'_> {
         self.output_row += 1;
         self.transforms
             .seek_forward(&WrapPoint::new(self.output_row, 0), Bias::Left);
-        if self.transforms.item().map_or(false, |t| t.is_isomorphic()) {
+        if self.transforms.item().is_some_and(|t| t.is_isomorphic()) {
             self.input_buffer_row = self.input_buffer_rows.next().unwrap();
             self.soft_wrapped = false;
         } else {
@@ -1065,12 +1065,12 @@ impl sum_tree::Item for Transform {
 }
 
 fn push_isomorphic(transforms: &mut Vec<Transform>, summary: TextSummary) {
-    if let Some(last_transform) = transforms.last_mut() {
-        if last_transform.is_isomorphic() {
-            last_transform.summary.input += &summary;
-            last_transform.summary.output += &summary;
-            return;
-        }
+    if let Some(last_transform) = transforms.last_mut()
+        && last_transform.is_isomorphic()
+    {
+        last_transform.summary.input += &summary;
+        last_transform.summary.output += &summary;
+        return;
     }
     transforms.push(Transform::isomorphic(summary));
 }
@@ -1223,7 +1223,7 @@ mod tests {
         let tab_size = NonZeroU32::new(rng.gen_range(1..=4)).unwrap();
 
         let font = test_font();
-        let _font_id = text_system.font_id(&font);
+        let _font_id = text_system.resolve_font(&font);
         let font_size = px(14.0);
 
         log::info!("Tab size: {}", tab_size);
@@ -1461,7 +1461,7 @@ mod tests {
                 }
 
                 let mut prev_ix = 0;
-                for boundary in line_wrapper.wrap_line(&[LineFragment::text(&line)], wrap_width) {
+                for boundary in line_wrapper.wrap_line(&[LineFragment::text(line)], wrap_width) {
                     wrapped_text.push_str(&line[prev_ix..boundary.ix]);
                     wrapped_text.push('\n');
                     wrapped_text.push_str(&" ".repeat(boundary.next_indent as usize));

crates/editor/src/edit_prediction_tests.rs 🔗

@@ -7,7 +7,9 @@ use std::ops::Range;
 use text::{Point, ToOffset};
 
 use crate::{
-    EditPrediction, editor_tests::init_test, test::editor_test_context::EditorTestContext,
+    EditPrediction,
+    editor_tests::{init_test, update_test_language_settings},
+    test::editor_test_context::EditorTestContext,
 };
 
 #[gpui::test]
@@ -271,6 +273,44 @@ async fn test_edit_prediction_jump_disabled_for_non_zed_providers(cx: &mut gpui:
     });
 }
 
+#[gpui::test]
+async fn test_edit_predictions_disabled_in_scope(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+
+    update_test_language_settings(cx, |settings| {
+        settings.defaults.edit_predictions_disabled_in = Some(vec!["string".to_string()]);
+    });
+
+    let mut cx = EditorTestContext::new(cx).await;
+    let provider = cx.new(|_| FakeEditPredictionProvider::default());
+    assign_editor_completion_provider(provider.clone(), &mut cx);
+
+    let language = languages::language("javascript", tree_sitter_typescript::LANGUAGE_TSX.into());
+    cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
+
+    // Test disabled inside of string
+    cx.set_state("const x = \"hello ˇworld\";");
+    propose_edits(&provider, vec![(17..17, "beautiful ")], &mut cx);
+    cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
+    cx.editor(|editor, _, _| {
+        assert!(
+            editor.active_edit_prediction.is_none(),
+            "Edit predictions should be disabled in string scopes when configured in edit_predictions_disabled_in"
+        );
+    });
+
+    // Test enabled outside of string
+    cx.set_state("const x = \"hello world\"; ˇ");
+    propose_edits(&provider, vec![(24..24, "// comment")], &mut cx);
+    cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
+    cx.editor(|editor, _, _| {
+        assert!(
+            editor.active_edit_prediction.is_some(),
+            "Edit predictions should work outside of disabled scopes"
+        );
+    });
+}
+
 fn assert_editor_active_edit_completion(
     cx: &mut EditorTestContext,
     assert: impl FnOnce(MultiBufferSnapshot, &Vec<(Range<Anchor>, String)>),

crates/editor/src/editor.rs 🔗

@@ -250,6 +250,24 @@ pub type RenderDiffHunkControlsFn = Arc<
     ) -> AnyElement,
 >;
 
+enum ReportEditorEvent {
+    Saved { auto_saved: bool },
+    EditorOpened,
+    ZetaTosClicked,
+    Closed,
+}
+
+impl ReportEditorEvent {
+    pub fn event_type(&self) -> &'static str {
+        match self {
+            Self::Saved { .. } => "Editor Saved",
+            Self::EditorOpened => "Editor Opened",
+            Self::ZetaTosClicked => "Edit Prediction Provider ToS Clicked",
+            Self::Closed => "Editor Closed",
+        }
+    }
+}
+
 struct InlineValueCache {
     enabled: bool,
     inlays: Vec<InlayId>,
@@ -764,10 +782,7 @@ impl MinimapVisibility {
     }
 
     fn disabled(&self) -> bool {
-        match *self {
-            Self::Disabled => true,
-            _ => false,
-        }
+        matches!(*self, Self::Disabled)
     }
 
     fn settings_visibility(&self) -> bool {
@@ -924,10 +939,10 @@ impl ChangeList {
     }
 
     pub fn invert_last_group(&mut self) {
-        if let Some(last) = self.changes.last_mut() {
-            if let Some(current) = last.current.as_mut() {
-                mem::swap(&mut last.original, current);
-            }
+        if let Some(last) = self.changes.last_mut()
+            && let Some(current) = last.current.as_mut()
+        {
+            mem::swap(&mut last.original, current);
         }
     }
 }
@@ -1021,9 +1036,7 @@ pub struct Editor {
     inline_diagnostics: Vec<(Anchor, InlineDiagnostic)>,
     soft_wrap_mode_override: Option<language_settings::SoftWrap>,
     hard_wrap: Option<usize>,
-
-    // TODO: make this a access method
-    pub project: Option<Entity<Project>>,
+    project: Option<Entity<Project>>,
     semantics_provider: Option<Rc<dyn SemanticsProvider>>,
     completion_provider: Option<Rc<dyn CompletionProvider>>,
     collaboration_hub: Option<Box<dyn CollaborationHub>>,
@@ -1413,7 +1426,7 @@ impl SelectionHistory {
         if self
             .undo_stack
             .back()
-            .map_or(true, |e| e.selections != entry.selections)
+            .is_none_or(|e| e.selections != entry.selections)
         {
             self.undo_stack.push_back(entry);
             if self.undo_stack.len() > MAX_SELECTION_HISTORY_LEN {
@@ -1426,7 +1439,7 @@ impl SelectionHistory {
         if self
             .redo_stack
             .back()
-            .map_or(true, |e| e.selections != entry.selections)
+            .is_none_or(|e| e.selections != entry.selections)
         {
             self.redo_stack.push_back(entry);
             if self.redo_stack.len() > MAX_SELECTION_HISTORY_LEN {
@@ -1841,118 +1854,166 @@ impl Editor {
             blink_manager
         });
 
-        let soft_wrap_mode_override = matches!(mode, EditorMode::SingleLine { .. })
-            .then(|| language_settings::SoftWrap::None);
+        let soft_wrap_mode_override =
+            matches!(mode, EditorMode::SingleLine).then(|| language_settings::SoftWrap::None);
 
         let mut project_subscriptions = Vec::new();
-        if full_mode {
-            if let Some(project) = project.as_ref() {
-                project_subscriptions.push(cx.subscribe_in(
-                    project,
-                    window,
-                    |editor, _, event, window, cx| match event {
-                        project::Event::RefreshCodeLens => {
-                            // we always query lens with actions, without storing them, always refreshing them
-                        }
-                        project::Event::RefreshInlayHints => {
-                            editor
-                                .refresh_inlay_hints(InlayHintRefreshReason::RefreshRequested, cx);
-                        }
-                        project::Event::LanguageServerAdded(..)
-                        | project::Event::LanguageServerRemoved(..) => {
-                            if editor.tasks_update_task.is_none() {
-                                editor.tasks_update_task =
-                                    Some(editor.refresh_runnables(window, cx));
-                            }
+        if full_mode && let Some(project) = project.as_ref() {
+            project_subscriptions.push(cx.subscribe_in(
+                project,
+                window,
+                |editor, _, event, window, cx| match event {
+                    project::Event::RefreshCodeLens => {
+                        // we always query lens with actions, without storing them, always refreshing them
+                    }
+                    project::Event::RefreshInlayHints => {
+                        editor.refresh_inlay_hints(InlayHintRefreshReason::RefreshRequested, cx);
+                    }
+                    project::Event::LanguageServerAdded(..)
+                    | project::Event::LanguageServerRemoved(..) => {
+                        if editor.tasks_update_task.is_none() {
+                            editor.tasks_update_task = Some(editor.refresh_runnables(window, cx));
                         }
-                        project::Event::SnippetEdit(id, snippet_edits) => {
-                            if let Some(buffer) = editor.buffer.read(cx).buffer(*id) {
-                                let focus_handle = editor.focus_handle(cx);
-                                if focus_handle.is_focused(window) {
-                                    let snapshot = buffer.read(cx).snapshot();
-                                    for (range, snippet) in snippet_edits {
-                                        let editor_range =
-                                            language::range_from_lsp(*range).to_offset(&snapshot);
-                                        editor
-                                            .insert_snippet(
-                                                &[editor_range],
-                                                snippet.clone(),
-                                                window,
-                                                cx,
-                                            )
-                                            .ok();
-                                    }
+                    }
+                    project::Event::SnippetEdit(id, snippet_edits) => {
+                        if let Some(buffer) = editor.buffer.read(cx).buffer(*id) {
+                            let focus_handle = editor.focus_handle(cx);
+                            if focus_handle.is_focused(window) {
+                                let snapshot = buffer.read(cx).snapshot();
+                                for (range, snippet) in snippet_edits {
+                                    let editor_range =
+                                        language::range_from_lsp(*range).to_offset(&snapshot);
+                                    editor
+                                        .insert_snippet(
+                                            &[editor_range],
+                                            snippet.clone(),
+                                            window,
+                                            cx,
+                                        )
+                                        .ok();
                                 }
                             }
                         }
-                        project::Event::LanguageServerBufferRegistered { buffer_id, .. } => {
-                            if editor.buffer().read(cx).buffer(*buffer_id).is_some() {
-                                editor.update_lsp_data(false, Some(*buffer_id), window, cx);
-                            }
+                    }
+                    project::Event::LanguageServerBufferRegistered { buffer_id, .. } => {
+                        if editor.buffer().read(cx).buffer(*buffer_id).is_some() {
+                            editor.update_lsp_data(false, Some(*buffer_id), window, cx);
                         }
-                        _ => {}
-                    },
-                ));
-                if let Some(task_inventory) = project
-                    .read(cx)
-                    .task_store()
-                    .read(cx)
-                    .task_inventory()
-                    .cloned()
-                {
-                    project_subscriptions.push(cx.observe_in(
-                        &task_inventory,
-                        window,
-                        |editor, _, window, cx| {
-                            editor.tasks_update_task = Some(editor.refresh_runnables(window, cx));
-                        },
-                    ));
-                };
+                    }
 
-                project_subscriptions.push(cx.subscribe_in(
-                    &project.read(cx).breakpoint_store(),
-                    window,
-                    |editor, _, event, window, cx| match event {
-                        BreakpointStoreEvent::ClearDebugLines => {
-                            editor.clear_row_highlights::<ActiveDebugLine>();
-                            editor.refresh_inline_values(cx);
-                        }
-                        BreakpointStoreEvent::SetDebugLine => {
-                            if editor.go_to_active_debug_line(window, cx) {
-                                cx.stop_propagation();
-                            }
+                    project::Event::EntryRenamed(transaction) => {
+                        let Some(workspace) = editor.workspace() else {
+                            return;
+                        };
+                        let Some(active_editor) = workspace.read(cx).active_item_as::<Self>(cx)
+                        else {
+                            return;
+                        };
+                        if active_editor.entity_id() == cx.entity_id() {
+                            let edited_buffers_already_open = {
+                                let other_editors: Vec<Entity<Editor>> = workspace
+                                    .read(cx)
+                                    .panes()
+                                    .iter()
+                                    .flat_map(|pane| pane.read(cx).items_of_type::<Editor>())
+                                    .filter(|editor| editor.entity_id() != cx.entity_id())
+                                    .collect();
+
+                                transaction.0.keys().all(|buffer| {
+                                    other_editors.iter().any(|editor| {
+                                        let multi_buffer = editor.read(cx).buffer();
+                                        multi_buffer.read(cx).is_singleton()
+                                            && multi_buffer.read(cx).as_singleton().map_or(
+                                                false,
+                                                |singleton| {
+                                                    singleton.entity_id() == buffer.entity_id()
+                                                },
+                                            )
+                                    })
+                                })
+                            };
 
-                            editor.refresh_inline_values(cx);
+                            if !edited_buffers_already_open {
+                                let workspace = workspace.downgrade();
+                                let transaction = transaction.clone();
+                                cx.defer_in(window, move |_, window, cx| {
+                                    cx.spawn_in(window, async move |editor, cx| {
+                                        Self::open_project_transaction(
+                                            &editor,
+                                            workspace,
+                                            transaction,
+                                            "Rename".to_string(),
+                                            cx,
+                                        )
+                                        .await
+                                        .ok()
+                                    })
+                                    .detach();
+                                });
+                            }
                         }
-                        _ => {}
+                    }
+
+                    _ => {}
+                },
+            ));
+            if let Some(task_inventory) = project
+                .read(cx)
+                .task_store()
+                .read(cx)
+                .task_inventory()
+                .cloned()
+            {
+                project_subscriptions.push(cx.observe_in(
+                    &task_inventory,
+                    window,
+                    |editor, _, window, cx| {
+                        editor.tasks_update_task = Some(editor.refresh_runnables(window, cx));
                     },
                 ));
-                let git_store = project.read(cx).git_store().clone();
-                let project = project.clone();
-                project_subscriptions.push(cx.subscribe(&git_store, move |this, _, event, cx| {
-                    match event {
-                        GitStoreEvent::RepositoryUpdated(
-                            _,
-                            RepositoryEvent::Updated {
-                                new_instance: true, ..
-                            },
-                            _,
-                        ) => {
-                            this.load_diff_task = Some(
-                                update_uncommitted_diff_for_buffer(
-                                    cx.entity(),
-                                    &project,
-                                    this.buffer.read(cx).all_buffers(),
-                                    this.buffer.clone(),
-                                    cx,
-                                )
-                                .shared(),
-                            );
+            };
+
+            project_subscriptions.push(cx.subscribe_in(
+                &project.read(cx).breakpoint_store(),
+                window,
+                |editor, _, event, window, cx| match event {
+                    BreakpointStoreEvent::ClearDebugLines => {
+                        editor.clear_row_highlights::<ActiveDebugLine>();
+                        editor.refresh_inline_values(cx);
+                    }
+                    BreakpointStoreEvent::SetDebugLine => {
+                        if editor.go_to_active_debug_line(window, cx) {
+                            cx.stop_propagation();
                         }
-                        _ => {}
+
+                        editor.refresh_inline_values(cx);
                     }
-                }));
-            }
+                    _ => {}
+                },
+            ));
+            let 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
+                {
+                    this.load_diff_task = Some(
+                        update_uncommitted_diff_for_buffer(
+                            cx.entity(),
+                            &project,
+                            this.buffer.read(cx).all_buffers(),
+                            this.buffer.clone(),
+                            cx,
+                        )
+                        .shared(),
+                    );
+                }
+            }));
         }
 
         let buffer_snapshot = buffer.read(cx).snapshot(cx);
@@ -1973,14 +2034,12 @@ impl Editor {
                 .detach();
         }
 
-        let show_indent_guides = if matches!(
-            mode,
-            EditorMode::SingleLine { .. } | EditorMode::Minimap { .. }
-        ) {
-            Some(false)
-        } else {
-            None
-        };
+        let show_indent_guides =
+            if matches!(mode, EditorMode::SingleLine | EditorMode::Minimap { .. }) {
+                Some(false)
+            } else {
+                None
+            };
 
         let breakpoint_store = match (&mode, project.as_ref()) {
             (EditorMode::Full { .. }, Some(project)) => Some(project.read(cx).breakpoint_store()),
@@ -2040,7 +2099,7 @@ impl Editor {
                 vertical: full_mode,
             },
             minimap_visibility: MinimapVisibility::for_mode(&mode, cx),
-            offset_content: !matches!(mode, EditorMode::SingleLine { .. }),
+            offset_content: !matches!(mode, EditorMode::SingleLine),
             show_breadcrumbs: EditorSettings::get_global(cx).toolbar.breadcrumbs,
             show_gutter: full_mode,
             show_line_numbers: (!full_mode).then_some(false),
@@ -2307,15 +2366,15 @@ impl Editor {
 
             editor.go_to_active_debug_line(window, cx);
 
-            if let Some(buffer) = buffer.read(cx).as_singleton() {
-                if let Some(project) = editor.project.as_ref() {
-                    let handle = project.update(cx, |project, cx| {
-                        project.register_buffer_with_language_servers(&buffer, cx)
-                    });
-                    editor
-                        .registered_buffers
-                        .insert(buffer.read(cx).remote_id(), handle);
-                }
+            if let Some(buffer) = buffer.read(cx).as_singleton()
+                && let Some(project) = editor.project()
+            {
+                let handle = project.update(cx, |project, cx| {
+                    project.register_buffer_with_language_servers(&buffer, cx)
+                });
+                editor
+                    .registered_buffers
+                    .insert(buffer.read(cx).remote_id(), handle);
             }
 
             editor.minimap =
@@ -2325,7 +2384,7 @@ impl Editor {
         }
 
         if editor.mode.is_full() {
-            editor.report_editor_event("Editor Opened", None, cx);
+            editor.report_editor_event(ReportEditorEvent::EditorOpened, None, cx);
         }
 
         editor
@@ -2353,6 +2412,34 @@ impl Editor {
             .is_some_and(|menu| menu.context_menu.focus_handle(cx).is_focused(window))
     }
 
+    pub fn is_range_selected(&mut self, range: &Range<Anchor>, cx: &mut Context<Self>) -> bool {
+        if self
+            .selections
+            .pending
+            .as_ref()
+            .is_some_and(|pending_selection| {
+                let snapshot = self.buffer().read(cx).snapshot(cx);
+                pending_selection
+                    .selection
+                    .range()
+                    .includes(range, &snapshot)
+            })
+        {
+            return true;
+        }
+
+        self.selections
+            .disjoint_in_range::<usize>(range.clone(), cx)
+            .into_iter()
+            .any(|selection| {
+                // This is needed to cover a corner case, if we just check for an existing
+                // selection in the fold range, having a cursor at the start of the fold
+                // marks it as selected. Non-empty selections don't cause this.
+                let length = selection.end - selection.start;
+                length > 0
+            })
+    }
+
     pub fn key_context(&self, window: &Window, cx: &App) -> KeyContext {
         self.key_context_internal(self.has_active_edit_prediction(), window, cx)
     }
@@ -2366,7 +2453,7 @@ impl Editor {
         let mut key_context = KeyContext::new_with_defaults();
         key_context.add("Editor");
         let mode = match self.mode {
-            EditorMode::SingleLine { .. } => "single_line",
+            EditorMode::SingleLine => "single_line",
             EditorMode::AutoHeight { .. } => "auto_height",
             EditorMode::Minimap { .. } => "minimap",
             EditorMode::Full { .. } => "full",
@@ -2472,9 +2559,7 @@ impl Editor {
             .context_menu
             .borrow()
             .as_ref()
-            .map_or(false, |context| {
-                matches!(context, CodeContextMenu::Completions(_))
-            });
+            .is_some_and(|context| matches!(context, CodeContextMenu::Completions(_)));
 
         showing_completions
             || self.edit_prediction_requires_modifier()
@@ -2505,7 +2590,7 @@ impl Editor {
                 || binding
                     .keystrokes()
                     .first()
-                    .map_or(false, |keystroke| keystroke.modifiers.modified())
+                    .is_some_and(|keystroke| keystroke.modifiers.modified())
         }))
     }
 
@@ -2608,6 +2693,10 @@ impl Editor {
         &self.buffer
     }
 
+    pub fn project(&self) -> Option<&Entity<Project>> {
+        self.project.as_ref()
+    }
+
     pub fn workspace(&self) -> Option<Entity<Workspace>> {
         self.workspace.as_ref()?.0.upgrade()
     }
@@ -2897,7 +2986,7 @@ impl Editor {
             return false;
         };
 
-        scope.override_name().map_or(false, |scope_name| {
+        scope.override_name().is_some_and(|scope_name| {
             settings
                 .edit_predictions_disabled_in
                 .iter()
@@ -2987,20 +3076,19 @@ impl Editor {
         }
 
         if local {
-            if let Some(buffer_id) = new_cursor_position.buffer_id {
-                if !self.registered_buffers.contains_key(&buffer_id) {
-                    if let Some(project) = self.project.as_ref() {
-                        project.update(cx, |project, cx| {
-                            let Some(buffer) = self.buffer.read(cx).buffer(buffer_id) else {
-                                return;
-                            };
-                            self.registered_buffers.insert(
-                                buffer_id,
-                                project.register_buffer_with_language_servers(&buffer, cx),
-                            );
-                        })
-                    }
-                }
+            if let Some(buffer_id) = new_cursor_position.buffer_id
+                && !self.registered_buffers.contains_key(&buffer_id)
+                && let Some(project) = self.project.as_ref()
+            {
+                project.update(cx, |project, cx| {
+                    let Some(buffer) = self.buffer.read(cx).buffer(buffer_id) else {
+                        return;
+                    };
+                    self.registered_buffers.insert(
+                        buffer_id,
+                        project.register_buffer_with_language_servers(&buffer, cx),
+                    );
+                })
             }
 
             let mut context_menu = self.context_menu.borrow_mut();
@@ -3015,28 +3103,28 @@ impl Editor {
             let completion_position = completion_menu.map(|menu| menu.initial_position);
             drop(context_menu);
 
-            if effects.completions {
-                if let Some(completion_position) = completion_position {
-                    let start_offset = selection_start.to_offset(buffer);
-                    let position_matches = start_offset == completion_position.to_offset(buffer);
-                    let continue_showing = if position_matches {
-                        if self.snippet_stack.is_empty() {
-                            buffer.char_kind_before(start_offset, true) == Some(CharKind::Word)
-                        } else {
-                            // Snippet choices can be shown even when the cursor is in whitespace.
-                            // Dismissing the menu with actions like backspace is handled by
-                            // invalidation regions.
-                            true
-                        }
-                    } else {
-                        false
-                    };
-
-                    if continue_showing {
-                        self.show_completions(&ShowCompletions { trigger: None }, window, cx);
+            if effects.completions
+                && let Some(completion_position) = completion_position
+            {
+                let start_offset = selection_start.to_offset(buffer);
+                let position_matches = start_offset == completion_position.to_offset(buffer);
+                let continue_showing = if position_matches {
+                    if self.snippet_stack.is_empty() {
+                        buffer.char_kind_before(start_offset, true) == Some(CharKind::Word)
                     } else {
-                        self.hide_context_menu(window, cx);
+                        // Snippet choices can be shown even when the cursor is in whitespace.
+                        // Dismissing the menu with actions like backspace is handled by
+                        // invalidation regions.
+                        true
                     }
+                } else {
+                    false
+                };
+
+                if continue_showing {
+                    self.show_completions(&ShowCompletions { trigger: None }, window, cx);
+                } else {
+                    self.hide_context_menu(window, cx);
                 }
             }
 
@@ -3067,30 +3155,27 @@ impl Editor {
         if selections.len() == 1 {
             cx.emit(SearchEvent::ActiveMatchChanged)
         }
-        if local {
-            if let Some((_, _, buffer_snapshot)) = buffer.as_singleton() {
-                let inmemory_selections = selections
-                    .iter()
-                    .map(|s| {
-                        text::ToPoint::to_point(&s.range().start.text_anchor, buffer_snapshot)
-                            ..text::ToPoint::to_point(&s.range().end.text_anchor, buffer_snapshot)
-                    })
-                    .collect();
-                self.update_restoration_data(cx, |data| {
-                    data.selections = inmemory_selections;
-                });
+        if local && let Some((_, _, buffer_snapshot)) = buffer.as_singleton() {
+            let inmemory_selections = selections
+                .iter()
+                .map(|s| {
+                    text::ToPoint::to_point(&s.range().start.text_anchor, buffer_snapshot)
+                        ..text::ToPoint::to_point(&s.range().end.text_anchor, buffer_snapshot)
+                })
+                .collect();
+            self.update_restoration_data(cx, |data| {
+                data.selections = inmemory_selections;
+            });
 
-                if WorkspaceSettings::get(None, cx).restore_on_startup
-                    != RestoreOnStartupBehavior::None
-                {
-                    if let Some(workspace_id) =
-                        self.workspace.as_ref().and_then(|workspace| workspace.1)
-                    {
-                        let snapshot = self.buffer().read(cx).snapshot(cx);
-                        let selections = selections.clone();
-                        let background_executor = cx.background_executor().clone();
-                        let editor_id = cx.entity().entity_id().as_u64() as ItemId;
-                        self.serialize_selections = cx.background_spawn(async move {
+            if WorkspaceSettings::get(None, cx).restore_on_startup != RestoreOnStartupBehavior::None
+                && let Some(workspace_id) =
+                    self.workspace.as_ref().and_then(|workspace| workspace.1)
+            {
+                let snapshot = self.buffer().read(cx).snapshot(cx);
+                let selections = selections.clone();
+                let background_executor = cx.background_executor().clone();
+                let editor_id = cx.entity().entity_id().as_u64() as ItemId;
+                self.serialize_selections = cx.background_spawn(async move {
                             background_executor.timer(SERIALIZATION_THROTTLE_TIME).await;
                             let db_selections = selections
                                 .iter()
@@ -3107,8 +3192,6 @@ impl Editor {
                                 .with_context(|| format!("persisting editor selections for editor {editor_id}, workspace {workspace_id:?}"))
                                 .log_err();
                         });
-                    }
-                }
             }
         }
 
@@ -3185,35 +3268,31 @@ impl Editor {
             selections.select_anchors(other_selections);
         });
 
-        let other_subscription =
-            cx.subscribe(&other, |this, other, other_evt, cx| match other_evt {
-                EditorEvent::SelectionsChanged { local: true } => {
-                    let other_selections = other.read(cx).selections.disjoint.to_vec();
-                    if other_selections.is_empty() {
-                        return;
-                    }
-                    this.selections.change_with(cx, |selections| {
-                        selections.select_anchors(other_selections);
-                    });
+        let other_subscription = cx.subscribe(&other, |this, other, other_evt, cx| {
+            if let EditorEvent::SelectionsChanged { local: true } = other_evt {
+                let other_selections = other.read(cx).selections.disjoint.to_vec();
+                if other_selections.is_empty() {
+                    return;
                 }
-                _ => {}
-            });
+                this.selections.change_with(cx, |selections| {
+                    selections.select_anchors(other_selections);
+                });
+            }
+        });
 
-        let this_subscription =
-            cx.subscribe_self::<EditorEvent>(move |this, this_evt, cx| match this_evt {
-                EditorEvent::SelectionsChanged { local: true } => {
-                    let these_selections = this.selections.disjoint.to_vec();
-                    if these_selections.is_empty() {
-                        return;
-                    }
-                    other.update(cx, |other_editor, cx| {
-                        other_editor.selections.change_with(cx, |selections| {
-                            selections.select_anchors(these_selections);
-                        })
-                    });
+        let this_subscription = cx.subscribe_self::<EditorEvent>(move |this, this_evt, cx| {
+            if let EditorEvent::SelectionsChanged { local: true } = this_evt {
+                let these_selections = this.selections.disjoint.to_vec();
+                if these_selections.is_empty() {
+                    return;
                 }
-                _ => {}
-            });
+                other.update(cx, |other_editor, cx| {
+                    other_editor.selections.change_with(cx, |selections| {
+                        selections.select_anchors(these_selections);
+                    })
+                });
+            }
+        });
 
         Subscription::join(other_subscription, this_subscription)
     }
@@ -3294,9 +3373,9 @@ impl Editor {
 
             let old_cursor_position = &state.old_cursor_position;
 
-            self.selections_did_change(true, &old_cursor_position, state.effects, window, cx);
+            self.selections_did_change(true, old_cursor_position, state.effects, window, cx);
 
-            if self.should_open_signature_help_automatically(&old_cursor_position, cx) {
+            if self.should_open_signature_help_automatically(old_cursor_position, cx) {
                 self.show_signature_help(&ShowSignatureHelp, window, cx);
             }
         }
@@ -3716,9 +3795,9 @@ impl Editor {
             ColumnarSelectionState::FromMouse {
                 selection_tail,
                 display_point,
-            } => display_point.unwrap_or_else(|| selection_tail.to_display_point(&display_map)),
+            } => display_point.unwrap_or_else(|| selection_tail.to_display_point(display_map)),
             ColumnarSelectionState::FromSelection { selection_tail } => {
-                selection_tail.to_display_point(&display_map)
+                selection_tail.to_display_point(display_map)
             }
         };
 
@@ -3995,18 +4074,18 @@ impl Editor {
                             let following_text_allows_autoclose = snapshot
                                 .chars_at(selection.start)
                                 .next()
-                                .map_or(true, |c| scope.should_autoclose_before(c));
+                                .is_none_or(|c| scope.should_autoclose_before(c));
 
                             let preceding_text_allows_autoclose = selection.start.column == 0
-                                || snapshot.reversed_chars_at(selection.start).next().map_or(
-                                    true,
-                                    |c| {
+                                || snapshot
+                                    .reversed_chars_at(selection.start)
+                                    .next()
+                                    .is_none_or(|c| {
                                         bracket_pair.start != bracket_pair.end
                                             || !snapshot
                                                 .char_classifier_at(selection.start)
                                                 .is_word(c)
-                                    },
-                                );
+                                    });
 
                             let is_closing_quote = if bracket_pair.end == bracket_pair.start
                                 && bracket_pair.start.len() == 1
@@ -4106,42 +4185,38 @@ impl Editor {
             if self.auto_replace_emoji_shortcode
                 && selection.is_empty()
                 && text.as_ref().ends_with(':')
-            {
-                if let Some(possible_emoji_short_code) =
+                && let Some(possible_emoji_short_code) =
                     Self::find_possible_emoji_shortcode_at_position(&snapshot, selection.start)
-                {
-                    if !possible_emoji_short_code.is_empty() {
-                        if let Some(emoji) = emojis::get_by_shortcode(&possible_emoji_short_code) {
-                            let emoji_shortcode_start = Point::new(
-                                selection.start.row,
-                                selection.start.column - possible_emoji_short_code.len() as u32 - 1,
-                            );
+                && !possible_emoji_short_code.is_empty()
+                && let Some(emoji) = emojis::get_by_shortcode(&possible_emoji_short_code)
+            {
+                let emoji_shortcode_start = Point::new(
+                    selection.start.row,
+                    selection.start.column - possible_emoji_short_code.len() as u32 - 1,
+                );
 
-                            // Remove shortcode from buffer
-                            edits.push((
-                                emoji_shortcode_start..selection.start,
-                                "".to_string().into(),
-                            ));
-                            new_selections.push((
-                                Selection {
-                                    id: selection.id,
-                                    start: snapshot.anchor_after(emoji_shortcode_start),
-                                    end: snapshot.anchor_before(selection.start),
-                                    reversed: selection.reversed,
-                                    goal: selection.goal,
-                                },
-                                0,
-                            ));
+                // Remove shortcode from buffer
+                edits.push((
+                    emoji_shortcode_start..selection.start,
+                    "".to_string().into(),
+                ));
+                new_selections.push((
+                    Selection {
+                        id: selection.id,
+                        start: snapshot.anchor_after(emoji_shortcode_start),
+                        end: snapshot.anchor_before(selection.start),
+                        reversed: selection.reversed,
+                        goal: selection.goal,
+                    },
+                    0,
+                ));
 
-                            // Insert emoji
-                            let selection_start_anchor = snapshot.anchor_after(selection.start);
-                            new_selections.push((selection.map(|_| selection_start_anchor), 0));
-                            edits.push((selection.start..selection.end, emoji.to_string().into()));
+                // Insert emoji
+                let selection_start_anchor = snapshot.anchor_after(selection.start);
+                new_selections.push((selection.map(|_| selection_start_anchor), 0));
+                edits.push((selection.start..selection.end, emoji.to_string().into()));
 
-                            continue;
-                        }
-                    }
-                }
+                continue;
             }
 
             // If not handling any auto-close operation, then just replace the selected
@@ -4151,7 +4226,7 @@ impl Editor {
             if !self.linked_edit_ranges.is_empty() {
                 let start_anchor = snapshot.anchor_before(selection.start);
 
-                let is_word_char = text.chars().next().map_or(true, |char| {
+                let is_word_char = text.chars().next().is_none_or(|char| {
                     let classifier = snapshot
                         .char_classifier_at(start_anchor.to_offset(&snapshot))
                         .ignore_punctuation(true);
@@ -4255,12 +4330,11 @@ impl Editor {
                 |s| s.select(new_selections),
             );
 
-            if !bracket_inserted {
-                if let Some(on_type_format_task) =
+            if !bracket_inserted
+                && let Some(on_type_format_task) =
                     this.trigger_on_type_formatting(text.to_string(), window, cx)
-                {
-                    on_type_format_task.detach_and_log_err(cx);
-                }
+            {
+                on_type_format_task.detach_and_log_err(cx);
             }
 
             let editor_settings = EditorSettings::get_global(cx);
@@ -4506,7 +4580,7 @@ impl Editor {
                                     let mut char_position = 0u32;
                                     let mut end_tag_offset = None;
 
-                                    'outer: for chunk in snapshot.text_for_range(range.clone()) {
+                                    'outer: for chunk in snapshot.text_for_range(range) {
                                         if let Some(byte_pos) = chunk.find(&**end_tag) {
                                             let chars_before_match =
                                                 chunk[..byte_pos].chars().count() as u32;
@@ -4856,11 +4930,7 @@ impl Editor {
         cx: &mut Context<Self>,
     ) -> bool {
         let position = self.selections.newest_anchor().head();
-        let multibuffer = self.buffer.read(cx);
-        let Some(buffer) = position
-            .buffer_id
-            .and_then(|buffer_id| multibuffer.buffer(buffer_id).clone())
-        else {
+        let Some(buffer) = self.buffer.read(cx).buffer_for_anchor(position, cx) else {
             return false;
         };
 
@@ -5194,7 +5264,7 @@ impl Editor {
         restrict_to_languages: Option<&HashSet<Arc<Language>>>,
         cx: &mut Context<Editor>,
     ) -> HashMap<ExcerptId, (Entity<Buffer>, clock::Global, Range<usize>)> {
-        let Some(project) = self.project.as_ref() else {
+        let Some(project) = self.project() else {
             return HashMap::default();
         };
         let project = project.read(cx);
@@ -5226,10 +5296,10 @@ impl Editor {
                 }
 
                 let language = buffer.language()?;
-                if let Some(restrict_to_languages) = restrict_to_languages {
-                    if !restrict_to_languages.contains(language) {
-                        return None;
-                    }
+                if let Some(restrict_to_languages) = restrict_to_languages
+                    && !restrict_to_languages.contains(language)
+                {
+                    return None;
                 }
                 Some((
                     excerpt_id,
@@ -5276,7 +5346,7 @@ impl Editor {
             return None;
         }
 
-        let project = self.project.as_ref()?;
+        let project = self.project()?;
         let position = self.selections.newest_anchor().head();
         let (buffer, buffer_position) = self
             .buffer
@@ -5394,11 +5464,11 @@ impl Editor {
 
         let sort_completions = provider
             .as_ref()
-            .map_or(false, |provider| provider.sort_completions());
+            .is_some_and(|provider| provider.sort_completions());
 
         let filter_completions = provider
             .as_ref()
-            .map_or(true, |provider| provider.filter_completions());
+            .is_none_or(|provider| provider.filter_completions());
 
         let trigger_kind = match trigger {
             Some(trigger) if buffer.read(cx).completion_triggers().contains(trigger) => {
@@ -5504,7 +5574,7 @@ impl Editor {
 
         let skip_digits = query
             .as_ref()
-            .map_or(true, |query| !query.chars().any(|c| c.is_digit(10)));
+            .is_none_or(|query| !query.chars().any(|c| c.is_digit(10)));
 
         let (mut words, provider_responses) = match &provider {
             Some(provider) => {
@@ -5557,15 +5627,15 @@ impl Editor {
             // that having one source with `is_incomplete: true` doesn't cause all to be re-queried.
             let mut completions = Vec::new();
             let mut is_incomplete = false;
-            if let Some(provider_responses) = provider_responses.await.log_err() {
-                if !provider_responses.is_empty() {
-                    for response in provider_responses {
-                        completions.extend(response.completions);
-                        is_incomplete = is_incomplete || response.is_incomplete;
-                    }
-                    if completion_settings.words == WordsCompletionMode::Fallback {
-                        words = Task::ready(BTreeMap::default());
-                    }
+            if let Some(provider_responses) = provider_responses.await.log_err()
+                && !provider_responses.is_empty()
+            {
+                for response in provider_responses {
+                    completions.extend(response.completions);
+                    is_incomplete = is_incomplete || response.is_incomplete;
+                }
+                if completion_settings.words == WordsCompletionMode::Fallback {
+                    words = Task::ready(BTreeMap::default());
                 }
             }
 
@@ -5630,34 +5700,31 @@ impl Editor {
 
                 let Ok(()) = editor.update_in(cx, |editor, window, cx| {
                     // Newer menu already set, so exit.
-                    match editor.context_menu.borrow().as_ref() {
-                        Some(CodeContextMenu::Completions(prev_menu)) => {
-                            if prev_menu.id > id {
-                                return;
-                            }
-                        }
-                        _ => {}
+                    if let Some(CodeContextMenu::Completions(prev_menu)) =
+                        editor.context_menu.borrow().as_ref()
+                        && prev_menu.id > id
+                    {
+                        return;
                     };
 
                     // Only valid to take prev_menu because it the new menu is immediately set
                     // below, or the menu is hidden.
-                    match editor.context_menu.borrow_mut().take() {
-                        Some(CodeContextMenu::Completions(prev_menu)) => {
-                            let position_matches =
-                                if prev_menu.initial_position == menu.initial_position {
-                                    true
-                                } else {
-                                    let snapshot = editor.buffer.read(cx).read(cx);
-                                    prev_menu.initial_position.to_offset(&snapshot)
-                                        == menu.initial_position.to_offset(&snapshot)
-                                };
-                            if position_matches {
-                                // Preserve markdown cache before `set_filter_results` because it will
-                                // try to populate the documentation cache.
-                                menu.preserve_markdown_cache(prev_menu);
-                            }
+                    if let Some(CodeContextMenu::Completions(prev_menu)) =
+                        editor.context_menu.borrow_mut().take()
+                    {
+                        let position_matches =
+                            if prev_menu.initial_position == menu.initial_position {
+                                true
+                            } else {
+                                let snapshot = editor.buffer.read(cx).read(cx);
+                                prev_menu.initial_position.to_offset(&snapshot)
+                                    == menu.initial_position.to_offset(&snapshot)
+                            };
+                        if position_matches {
+                            // Preserve markdown cache before `set_filter_results` because it will
+                            // try to populate the documentation cache.
+                            menu.preserve_markdown_cache(prev_menu);
                         }
-                        _ => {}
                     };
 
                     menu.set_filter_results(matches, provider, window, cx);
@@ -5670,21 +5737,21 @@ impl Editor {
 
             editor
                 .update_in(cx, |editor, window, cx| {
-                    if editor.focus_handle.is_focused(window) {
-                        if let Some(menu) = menu {
-                            *editor.context_menu.borrow_mut() =
-                                Some(CodeContextMenu::Completions(menu));
-
-                            crate::hover_popover::hide_hover(editor, cx);
-                            if editor.show_edit_predictions_in_menu() {
-                                editor.update_visible_edit_prediction(window, cx);
-                            } else {
-                                editor.discard_edit_prediction(false, cx);
-                            }
+                    if editor.focus_handle.is_focused(window)
+                        && let Some(menu) = menu
+                    {
+                        *editor.context_menu.borrow_mut() =
+                            Some(CodeContextMenu::Completions(menu));
 
-                            cx.notify();
-                            return;
+                        crate::hover_popover::hide_hover(editor, cx);
+                        if editor.show_edit_predictions_in_menu() {
+                            editor.update_visible_edit_prediction(window, cx);
+                        } else {
+                            editor.discard_edit_prediction(false, cx);
                         }
+
+                        cx.notify();
+                        return;
                     }
 
                     if editor.completion_tasks.len() <= 1 {

crates/editor/src/editor_settings.rs 🔗

@@ -132,6 +132,10 @@ pub struct StatusBar {
     ///
     /// Default: true
     pub active_language_button: bool,
+    /// Whether to show the cursor position button in the status bar.
+    ///
+    /// Default: true
+    pub cursor_position_button: bool,
 }
 
 #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
@@ -585,6 +589,10 @@ pub struct StatusBarContent {
     ///
     /// Default: true
     pub active_language_button: Option<bool>,
+    /// Whether to show the cursor position button in the status bar.
+    ///
+    /// Default: true
+    pub cursor_position_button: Option<bool>,
 }
 
 // Toolbar related settings
@@ -802,10 +810,8 @@ impl Settings for EditorSettings {
             if gutter.line_numbers.is_some() {
                 old_gutter.line_numbers = gutter.line_numbers
             }
-        } else {
-            if gutter != GutterContent::default() {
-                current.gutter = Some(gutter)
-            }
+        } else if gutter != GutterContent::default() {
+            current.gutter = Some(gutter)
         }
         if let Some(b) = vscode.read_bool("editor.scrollBeyondLastLine") {
             current.scroll_beyond_last_line = Some(if b {

crates/editor/src/editor_settings_controls.rs 🔗

@@ -88,7 +88,7 @@ impl RenderOnce for BufferFontFamilyControl {
             .child(Icon::new(IconName::Font))
             .child(DropdownMenu::new(
                 "buffer-font-family",
-                value.clone(),
+                value,
                 ContextMenu::build(window, cx, |mut menu, _, cx| {
                     let font_family_cache = FontFamilyCache::global(cx);
 

crates/editor/src/editor_tests.rs 🔗

@@ -74,7 +74,7 @@ fn test_edit_events(cx: &mut TestAppContext) {
     let editor1 = cx.add_window({
         let events = events.clone();
         |window, cx| {
-            let entity = cx.entity().clone();
+            let entity = cx.entity();
             cx.subscribe_in(
                 &entity,
                 window,
@@ -95,7 +95,7 @@ fn test_edit_events(cx: &mut TestAppContext) {
         let events = events.clone();
         |window, cx| {
             cx.subscribe_in(
-                &cx.entity().clone(),
+                &cx.entity(),
                 window,
                 move |_, _, event: &EditorEvent, _, _| match event {
                     EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
@@ -708,7 +708,7 @@ async fn test_navigation_history(cx: &mut TestAppContext) {
     _ = workspace.update(cx, |_v, window, cx| {
         cx.new(|cx| {
             let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
-            let mut editor = build_editor(buffer.clone(), window, cx);
+            let mut editor = build_editor(buffer, window, cx);
             let handle = cx.entity();
             editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
 
@@ -898,7 +898,7 @@ fn test_fold_action(cx: &mut TestAppContext) {
             .unindent(),
             cx,
         );
-        build_editor(buffer.clone(), window, cx)
+        build_editor(buffer, window, cx)
     });
 
     _ = editor.update(cx, |editor, window, cx| {
@@ -989,7 +989,7 @@ fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
             .unindent(),
             cx,
         );
-        build_editor(buffer.clone(), window, cx)
+        build_editor(buffer, window, cx)
     });
 
     _ = editor.update(cx, |editor, window, cx| {
@@ -1074,7 +1074,7 @@ fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
             .unindent(),
             cx,
         );
-        build_editor(buffer.clone(), window, cx)
+        build_editor(buffer, window, cx)
     });
 
     _ = editor.update(cx, |editor, window, cx| {
@@ -1173,7 +1173,7 @@ fn test_fold_at_level(cx: &mut TestAppContext) {
             .unindent(),
             cx,
         );
-        build_editor(buffer.clone(), window, cx)
+        build_editor(buffer, window, cx)
     });
 
     _ = editor.update(cx, |editor, window, cx| {
@@ -1335,7 +1335,7 @@ fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
 
     let editor = cx.add_window(|window, cx| {
         let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
-        build_editor(buffer.clone(), window, cx)
+        build_editor(buffer, window, cx)
     });
 
     assert_eq!('🟥'.len_utf8(), 4);
@@ -1452,7 +1452,7 @@ fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
 
     let editor = cx.add_window(|window, cx| {
         let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
-        build_editor(buffer.clone(), window, cx)
+        build_editor(buffer, window, cx)
     });
     _ = editor.update(cx, |editor, window, cx| {
         editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
@@ -1901,6 +1901,51 @@ fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
     });
 }
 
+#[gpui::test]
+fn test_beginning_of_line_with_cursor_between_line_start_and_indent(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
+
+    let move_to_beg = MoveToBeginningOfLine {
+        stop_at_soft_wraps: true,
+        stop_at_indent: true,
+    };
+
+    let editor = cx.add_window(|window, cx| {
+        let buffer = MultiBuffer::build_simple("    hello\nworld", cx);
+        build_editor(buffer, window, cx)
+    });
+
+    _ = editor.update(cx, |editor, window, cx| {
+        // test cursor between line_start and indent_start
+        editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
+            s.select_display_ranges([
+                DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3)
+            ]);
+        });
+
+        // cursor should move to line_start
+        editor.move_to_beginning_of_line(&move_to_beg, window, cx);
+        assert_eq!(
+            editor.selections.display_ranges(cx),
+            &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
+        );
+
+        // cursor should move to indent_start
+        editor.move_to_beginning_of_line(&move_to_beg, window, cx);
+        assert_eq!(
+            editor.selections.display_ranges(cx),
+            &[DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 4)]
+        );
+
+        // cursor should move to back to line_start
+        editor.move_to_beginning_of_line(&move_to_beg, window, cx);
+        assert_eq!(
+            editor.selections.display_ranges(cx),
+            &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
+        );
+    });
+}
+
 #[gpui::test]
 fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
     init_test(cx, |_| {});
@@ -2434,7 +2479,7 @@ fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
 
     let editor = cx.add_window(|window, cx| {
         let buffer = MultiBuffer::build_simple("one two three four", cx);
-        build_editor(buffer.clone(), window, cx)
+        build_editor(buffer, window, cx)
     });
 
     _ = editor.update(cx, |editor, window, cx| {
@@ -2482,7 +2527,7 @@ fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
 
     let editor = cx.add_window(|window, cx| {
         let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
-        build_editor(buffer.clone(), window, cx)
+        build_editor(buffer, window, cx)
     });
     let del_to_prev_word_start = DeleteToPreviousWordStart {
         ignore_newlines: false,
@@ -2518,7 +2563,7 @@ fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
 
     let editor = cx.add_window(|window, cx| {
         let buffer = MultiBuffer::build_simple("\none\n   two\nthree\n   four", cx);
-        build_editor(buffer.clone(), window, cx)
+        build_editor(buffer, window, cx)
     });
     let del_to_next_word_end = DeleteToNextWordEnd {
         ignore_newlines: false,
@@ -2563,7 +2608,7 @@ fn test_newline(cx: &mut TestAppContext) {
 
     let editor = cx.add_window(|window, cx| {
         let buffer = MultiBuffer::build_simple("aaaa\n    bbbb\n", cx);
-        build_editor(buffer.clone(), window, cx)
+        build_editor(buffer, window, cx)
     });
 
     _ = editor.update(cx, |editor, window, cx| {
@@ -2599,7 +2644,7 @@ fn test_newline_with_old_selections(cx: &mut TestAppContext) {
             .as_str(),
             cx,
         );
-        let mut editor = build_editor(buffer.clone(), window, cx);
+        let mut editor = build_editor(buffer, window, cx);
         editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
             s.select_ranges([
                 Point::new(2, 4)..Point::new(2, 5),
@@ -3130,7 +3175,7 @@ fn test_insert_with_old_selections(cx: &mut TestAppContext) {
 
     let editor = cx.add_window(|window, cx| {
         let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
-        let mut editor = build_editor(buffer.clone(), window, cx);
+        let mut editor = build_editor(buffer, window, cx);
         editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
             s.select_ranges([3..4, 11..12, 19..20])
         });
@@ -5517,7 +5562,7 @@ async fn test_rewrap(cx: &mut TestAppContext) {
             # ˇThis is a long comment using a pound
             # sign.
         "},
-        python_language.clone(),
+        python_language,
         &mut cx,
     );
 
@@ -5624,7 +5669,7 @@ async fn test_rewrap(cx: &mut TestAppContext) {
               also very long and should not merge
               with the numbered item.ˇ»
         "},
-        markdown_language.clone(),
+        markdown_language,
         &mut cx,
     );
 
@@ -5655,7 +5700,7 @@ async fn test_rewrap(cx: &mut TestAppContext) {
                // This is the second long comment block
                // to be wrapped.ˇ»
            "},
-        rust_language.clone(),
+        rust_language,
         &mut cx,
     );
 
@@ -5678,7 +5723,7 @@ async fn test_rewrap(cx: &mut TestAppContext) {
             «\tThis is a very long indented line
             \tthat will be wrapped.ˇ»
          "},
-        plaintext_language.clone(),
+        plaintext_language,
         &mut cx,
     );
 
@@ -6356,7 +6401,7 @@ async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
     fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
         cx.set_state(initial_state);
         cx.update_editor(|e, window, cx| {
-            e.split_selection_into_lines(&SplitSelectionIntoLines, window, cx)
+            e.split_selection_into_lines(&Default::default(), window, cx)
         });
         cx.assert_editor_state(expected_state);
     }
@@ -6444,7 +6489,7 @@ async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestA
                 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
             ])
         });
-        editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
+        editor.split_selection_into_lines(&Default::default(), window, cx);
         assert_eq!(
             editor.display_text(cx),
             "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
@@ -6460,7 +6505,7 @@ async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestA
                 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
             ])
         });
-        editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
+        editor.split_selection_into_lines(&Default::default(), window, cx);
         assert_eq!(
             editor.display_text(cx),
             "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
@@ -7970,7 +8015,7 @@ async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppConte
 }
 
 #[gpui::test]
-async fn test_unwrap_syntax_node(cx: &mut gpui::TestAppContext) {
+async fn test_unwrap_syntax_nodes(cx: &mut gpui::TestAppContext) {
     init_test(cx, |_| {});
 
     let mut cx = EditorTestContext::new(cx).await;
@@ -7984,21 +8029,12 @@ async fn test_unwrap_syntax_node(cx: &mut gpui::TestAppContext) {
         buffer.set_language(Some(language), cx);
     });
 
-    cx.set_state(
-        &r#"
-            use mod1::mod2::{«mod3ˇ», mod4};
-        "#
-        .unindent(),
-    );
+    cx.set_state(indoc! { r#"use mod1::{mod2::{«mod3ˇ», mod4}, mod5::{mod6, «mod7ˇ»}};"# });
     cx.update_editor(|editor, window, cx| {
         editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
     });
-    cx.assert_editor_state(
-        &r#"
-            use mod1::mod2::«mod3ˇ»;
-        "#
-        .unindent(),
-    );
+
+    cx.assert_editor_state(indoc! { r#"use mod1::{mod2::«mod3ˇ», mod5::«mod7ˇ»};"# });
 }
 
 #[gpui::test]
@@ -8169,6 +8205,216 @@ async fn test_autoindent(cx: &mut TestAppContext) {
     });
 }
 
+#[gpui::test]
+async fn test_autoindent_disabled(cx: &mut TestAppContext) {
+    init_test(cx, |settings| settings.defaults.auto_indent = Some(false));
+
+    let language = Arc::new(
+        Language::new(
+            LanguageConfig {
+                brackets: BracketPairConfig {
+                    pairs: vec![
+                        BracketPair {
+                            start: "{".to_string(),
+                            end: "}".to_string(),
+                            close: false,
+                            surround: false,
+                            newline: true,
+                        },
+                        BracketPair {
+                            start: "(".to_string(),
+                            end: ")".to_string(),
+                            close: false,
+                            surround: false,
+                            newline: true,
+                        },
+                    ],
+                    ..Default::default()
+                },
+                ..Default::default()
+            },
+            Some(tree_sitter_rust::LANGUAGE.into()),
+        )
+        .with_indents_query(
+            r#"
+                (_ "(" ")" @end) @indent
+                (_ "{" "}" @end) @indent
+            "#,
+        )
+        .unwrap(),
+    );
+
+    let text = "fn a() {}";
+
+    let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
+    let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
+    let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
+    editor
+        .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
+        .await;
+
+    editor.update_in(cx, |editor, window, cx| {
+        editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
+            s.select_ranges([5..5, 8..8, 9..9])
+        });
+        editor.newline(&Newline, window, cx);
+        assert_eq!(
+            editor.text(cx),
+            indoc!(
+                "
+                fn a(
+
+                ) {
+
+                }
+                "
+            )
+        );
+        assert_eq!(
+            editor.selections.ranges(cx),
+            &[
+                Point::new(1, 0)..Point::new(1, 0),
+                Point::new(3, 0)..Point::new(3, 0),
+                Point::new(5, 0)..Point::new(5, 0)
+            ]
+        );
+    });
+}
+
+#[gpui::test]
+async fn test_autoindent_disabled_with_nested_language(cx: &mut TestAppContext) {
+    init_test(cx, |settings| {
+        settings.defaults.auto_indent = Some(true);
+        settings.languages.0.insert(
+            "python".into(),
+            LanguageSettingsContent {
+                auto_indent: Some(false),
+                ..Default::default()
+            },
+        );
+    });
+
+    let mut cx = EditorTestContext::new(cx).await;
+
+    let injected_language = Arc::new(
+        Language::new(
+            LanguageConfig {
+                brackets: BracketPairConfig {
+                    pairs: vec![
+                        BracketPair {
+                            start: "{".to_string(),
+                            end: "}".to_string(),
+                            close: false,
+                            surround: false,
+                            newline: true,
+                        },
+                        BracketPair {
+                            start: "(".to_string(),
+                            end: ")".to_string(),
+                            close: true,
+                            surround: false,
+                            newline: true,
+                        },
+                    ],
+                    ..Default::default()
+                },
+                name: "python".into(),
+                ..Default::default()
+            },
+            Some(tree_sitter_python::LANGUAGE.into()),
+        )
+        .with_indents_query(
+            r#"
+                (_ "(" ")" @end) @indent
+                (_ "{" "}" @end) @indent
+            "#,
+        )
+        .unwrap(),
+    );
+
+    let language = Arc::new(
+        Language::new(
+            LanguageConfig {
+                brackets: BracketPairConfig {
+                    pairs: vec![
+                        BracketPair {
+                            start: "{".to_string(),
+                            end: "}".to_string(),
+                            close: false,
+                            surround: false,
+                            newline: true,
+                        },
+                        BracketPair {
+                            start: "(".to_string(),
+                            end: ")".to_string(),
+                            close: true,
+                            surround: false,
+                            newline: true,
+                        },
+                    ],
+                    ..Default::default()
+                },
+                name: LanguageName::new("rust"),
+                ..Default::default()
+            },
+            Some(tree_sitter_rust::LANGUAGE.into()),
+        )
+        .with_indents_query(
+            r#"
+                (_ "(" ")" @end) @indent
+                (_ "{" "}" @end) @indent
+            "#,
+        )
+        .unwrap()
+        .with_injection_query(
+            r#"
+            (macro_invocation
+                macro: (identifier) @_macro_name
+                (token_tree) @injection.content
+                (#set! injection.language "python"))
+           "#,
+        )
+        .unwrap(),
+    );
+
+    cx.language_registry().add(injected_language);
+    cx.language_registry().add(language.clone());
+
+    cx.update_buffer(|buffer, cx| {
+        buffer.set_language(Some(language), cx);
+    });
+
+    cx.set_state(r#"struct A {ˇ}"#);
+
+    cx.update_editor(|editor, window, cx| {
+        editor.newline(&Default::default(), window, cx);
+    });
+
+    cx.assert_editor_state(indoc!(
+        "struct A {
+            ˇ
+        }"
+    ));
+
+    cx.set_state(r#"select_biased!(ˇ)"#);
+
+    cx.update_editor(|editor, window, cx| {
+        editor.newline(&Default::default(), window, cx);
+        editor.handle_input("def ", window, cx);
+        editor.handle_input("(", window, cx);
+        editor.newline(&Default::default(), window, cx);
+        editor.handle_input("a", window, cx);
+    });
+
+    cx.assert_editor_state(indoc!(
+        "select_biased!(
+        def (
+        aˇ
+        )
+        )"
+    ));
+}
+
 #[gpui::test]
 async fn test_autoindent_selections(cx: &mut TestAppContext) {
     init_test(cx, |_| {});
@@ -8643,7 +8889,7 @@ async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
     ));
 
     cx.language_registry().add(html_language.clone());
-    cx.language_registry().add(javascript_language.clone());
+    cx.language_registry().add(javascript_language);
     cx.executor().run_until_parked();
 
     cx.update_buffer(|buffer, cx| {
@@ -9387,7 +9633,7 @@ async fn test_snippets(cx: &mut TestAppContext) {
             .selections
             .all(cx)
             .iter()
-            .map(|s| s.range().clone())
+            .map(|s| s.range())
             .collect::<Vec<_>>();
         editor
             .insert_snippet(&insertion_ranges, snippet, window, cx)
@@ -9467,7 +9713,7 @@ async fn test_snippet_indentation(cx: &mut TestAppContext) {
             .selections
             .all(cx)
             .iter()
-            .map(|s| s.range().clone())
+            .map(|s| s.range())
             .collect::<Vec<_>>();
         editor
             .insert_snippet(&insertion_ranges, snippet, window, cx)
@@ -10536,7 +10782,7 @@ async fn test_multiple_formatters(cx: &mut TestAppContext) {
                     kind: Some("code-action-2".into()),
                     edit: Some(lsp::WorkspaceEdit::new(
                         [(
-                            uri.clone(),
+                            uri,
                             vec![lsp::TextEdit::new(
                                 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
                                 "applied-code-action-2-edit\n".to_string(),
@@ -12064,7 +12310,7 @@ async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext)
     let counter = Arc::new(AtomicUsize::new(0));
     handle_completion_request_with_insert_and_replace(
         &mut cx,
-        &buffer_marked_text,
+        buffer_marked_text,
         vec![(completion_text, completion_text)],
         counter.clone(),
     )
@@ -12078,7 +12324,7 @@ async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext)
             .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
             .unwrap()
     });
-    cx.assert_editor_state(&expected_with_replace_mode);
+    cx.assert_editor_state(expected_with_replace_mode);
     handle_resolve_completion_request(&mut cx, None).await;
     apply_additional_edits.await.unwrap();
 
@@ -12098,7 +12344,7 @@ async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext)
     });
     handle_completion_request_with_insert_and_replace(
         &mut cx,
-        &buffer_marked_text,
+        buffer_marked_text,
         vec![(completion_text, completion_text)],
         counter.clone(),
     )
@@ -12112,7 +12358,7 @@ async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext)
             .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
             .unwrap()
     });
-    cx.assert_editor_state(&expected_with_insert_mode);
+    cx.assert_editor_state(expected_with_insert_mode);
     handle_resolve_completion_request(&mut cx, None).await;
     apply_additional_edits.await.unwrap();
 }
@@ -12886,7 +13132,7 @@ async fn test_word_completion(cx: &mut TestAppContext) {
         if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
         {
             assert_eq!(
-                completion_menu_entries(&menu),
+                completion_menu_entries(menu),
                 &["first", "last"],
                 "When LSP server is fast to reply, no fallback word completions are used"
             );
@@ -12909,7 +13155,7 @@ async fn test_word_completion(cx: &mut TestAppContext) {
     cx.update_editor(|editor, _, _| {
         if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
         {
-            assert_eq!(completion_menu_entries(&menu), &["one", "three", "two"],
+            assert_eq!(completion_menu_entries(menu), &["one", "three", "two"],
                 "When LSP server is slow, document words can be shown instead, if configured accordingly");
         } else {
             panic!("expected completion menu to be open");
@@ -12970,7 +13216,7 @@ async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext
         if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
         {
             assert_eq!(
-                completion_menu_entries(&menu),
+                completion_menu_entries(menu),
                 &["first", "last", "second"],
                 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
             );
@@ -13026,7 +13272,7 @@ async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
         if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
         {
             assert_eq!(
-                completion_menu_entries(&menu),
+                completion_menu_entries(menu),
                 &["first", "last", "second"],
                 "`ShowWordCompletions` action should show word completions"
             );
@@ -13043,7 +13289,7 @@ async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
         if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
         {
             assert_eq!(
-                completion_menu_entries(&menu),
+                completion_menu_entries(menu),
                 &["last"],
                 "After showing word completions, further editing should filter them and not query the LSP"
             );
@@ -13082,7 +13328,7 @@ async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
         if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
         {
             assert_eq!(
-                completion_menu_entries(&menu),
+                completion_menu_entries(menu),
                 &["let"],
                 "With no digits in the completion query, no digits should be in the word completions"
             );
@@ -13107,7 +13353,7 @@ async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
     cx.update_editor(|editor, _, _| {
         if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
         {
-            assert_eq!(completion_menu_entries(&menu), &["33", "35f32"], "The digit is in the completion query, \
+            assert_eq!(completion_menu_entries(menu), &["33", "35f32"], "The digit is in the completion query, \
                 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
         } else {
             panic!("expected completion menu to be open");
@@ -13344,7 +13590,7 @@ async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
     cx.update_editor(|editor, _, _| {
         if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
         {
-            assert_eq!(completion_menu_entries(&menu), &["first", "last"]);
+            assert_eq!(completion_menu_entries(menu), &["first", "last"]);
         } else {
             panic!("expected completion menu to be open");
         }
@@ -14120,7 +14366,7 @@ async fn test_toggle_block_comment(cx: &mut TestAppContext) {
     ));
 
     cx.language_registry().add(html_language.clone());
-    cx.language_registry().add(javascript_language.clone());
+    cx.language_registry().add(javascript_language);
     cx.update_buffer(|buffer, cx| {
         buffer.set_language(Some(html_language), cx);
     });
@@ -14297,7 +14543,7 @@ fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
     );
     let excerpt_ranges = markers.into_iter().map(|marker| {
         let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
-        ExcerptRange::new(context.clone())
+        ExcerptRange::new(context)
     });
     let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
     let multibuffer = cx.new(|cx| {
@@ -14582,7 +14828,7 @@ fn test_highlighted_ranges(cx: &mut TestAppContext) {
 
     let editor = cx.add_window(|window, cx| {
         let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
-        build_editor(buffer.clone(), window, cx)
+        build_editor(buffer, window, cx)
     });
 
     _ = editor.update(cx, |editor, window, cx| {
@@ -15037,7 +15283,7 @@ async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mu
 
     let mut cx = EditorTestContext::new(cx).await;
     let lsp_store =
-        cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
+        cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
 
     cx.set_state(indoc! {"
         ˇfn func(abc def: i32) -> u32 {
@@ -15504,8 +15750,7 @@ async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppCon
     cx.simulate_keystroke("\n");
     cx.run_until_parked();
 
-    let buffer_cloned =
-        cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap().clone());
+    let buffer_cloned = cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap());
     let mut request =
         cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
             let buffer_cloned = buffer_cloned.clone();
@@ -16447,7 +16692,7 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestA
         if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
         {
             assert_eq!(
-                completion_menu_entries(&menu),
+                completion_menu_entries(menu),
                 &["bg-blue", "bg-red", "bg-yellow"]
             );
         } else {
@@ -16460,7 +16705,7 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestA
     cx.update_editor(|editor, _, _| {
         if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
         {
-            assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
+            assert_eq!(completion_menu_entries(menu), &["bg-blue", "bg-yellow"]);
         } else {
             panic!("expected completion menu to be open");
         }
@@ -16474,7 +16719,7 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestA
     cx.update_editor(|editor, _, _| {
         if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
         {
-            assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]);
+            assert_eq!(completion_menu_entries(menu), &["bg-yellow"]);
         } else {
             panic!("expected completion menu to be open");
         }
@@ -17043,7 +17288,7 @@ async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
             (buffer_2.clone(), base_text_2),
             (buffer_3.clone(), base_text_3),
         ] {
-            let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
+            let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
             editor
                 .buffer
                 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
@@ -17664,7 +17909,7 @@ async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
                 (buffer_2.clone(), file_2_old),
                 (buffer_3.clone(), file_3_old),
             ] {
-                let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
+                let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
                 editor
                     .buffer
                     .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
@@ -19209,7 +19454,7 @@ async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestApp
         let buffer_id = hunks[0].buffer_id;
         hunks
             .into_iter()
-            .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
+            .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
             .collect::<Vec<_>>()
     });
     assert_eq!(hunk_ranges.len(), 2);
@@ -19300,7 +19545,7 @@ async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestApp
         let buffer_id = hunks[0].buffer_id;
         hunks
             .into_iter()
-            .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
+            .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
             .collect::<Vec<_>>()
     });
     assert_eq!(hunk_ranges.len(), 2);
@@ -19366,7 +19611,7 @@ async fn test_toggle_deletion_hunk_at_start_of_file(
         let buffer_id = hunks[0].buffer_id;
         hunks
             .into_iter()
-            .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
+            .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
             .collect::<Vec<_>>()
     });
     assert_eq!(hunk_ranges.len(), 1);
@@ -19389,7 +19634,7 @@ async fn test_toggle_deletion_hunk_at_start_of_file(
     });
     executor.run_until_parked();
 
-    cx.assert_state_with_diff(hunk_expanded.clone());
+    cx.assert_state_with_diff(hunk_expanded);
 }
 
 #[gpui::test]
@@ -19589,13 +19834,8 @@ fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
 
             editor.insert_creases(Some(crease), cx);
             let snapshot = editor.snapshot(window, cx);
-            let _div = snapshot.render_crease_toggle(
-                MultiBufferRow(1),
-                false,
-                cx.entity().clone(),
-                window,
-                cx,
-            );
+            let _div =
+                snapshot.render_crease_toggle(MultiBufferRow(1), false, cx.entity(), window, cx);
             snapshot
         })
         .unwrap();
@@ -20774,7 +21014,7 @@ async fn assert_highlighted_edits(
 
     cx.update(|_window, cx| {
         let highlighted_edits = edit_prediction_edit_text(
-            &snapshot.as_singleton().unwrap().2,
+            snapshot.as_singleton().unwrap().2,
             &edits,
             &edit_preview,
             include_deletions,
@@ -20790,13 +21030,13 @@ fn assert_breakpoint(
     path: &Arc<Path>,
     expected: Vec<(u32, Breakpoint)>,
 ) {
-    if expected.len() == 0usize {
+    if expected.is_empty() {
         assert!(!breakpoints.contains_key(path), "{}", path.display());
     } else {
         let mut breakpoint = breakpoints
             .get(path)
             .unwrap()
-            .into_iter()
+            .iter()
             .map(|breakpoint| {
                 (
                     breakpoint.row,
@@ -20825,13 +21065,7 @@ fn add_log_breakpoint_at_cursor(
     let (anchor, bp) = editor
         .breakpoints_at_cursors(window, cx)
         .first()
-        .and_then(|(anchor, bp)| {
-            if let Some(bp) = bp {
-                Some((*anchor, bp.clone()))
-            } else {
-                None
-            }
-        })
+        .and_then(|(anchor, bp)| bp.as_ref().map(|bp| (*anchor, bp.clone())))
         .unwrap_or_else(|| {
             let cursor_position: Point = editor.selections.newest(cx).head();
 
@@ -20841,7 +21075,7 @@ fn add_log_breakpoint_at_cursor(
                 .buffer_snapshot
                 .anchor_before(Point::new(cursor_position.row, 0));
 
-            (breakpoint_position, Breakpoint::new_log(&log_message))
+            (breakpoint_position, Breakpoint::new_log(log_message))
         });
 
     editor.edit_breakpoint_at_anchor(
@@ -20909,7 +21143,7 @@ async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
     let abs_path = project.read_with(cx, |project, cx| {
         project
             .absolute_path(&project_path, cx)
-            .map(|path_buf| Arc::from(path_buf.to_owned()))
+            .map(Arc::from)
             .unwrap()
     });
 
@@ -20927,7 +21161,6 @@ async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
             .unwrap()
             .read(cx)
             .all_source_breakpoints(cx)
-            .clone()
     });
 
     assert_eq!(1, breakpoints.len());
@@ -20952,7 +21185,6 @@ async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
             .unwrap()
             .read(cx)
             .all_source_breakpoints(cx)
-            .clone()
     });
 
     assert_eq!(1, breakpoints.len());
@@ -20974,7 +21206,6 @@ async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
             .unwrap()
             .read(cx)
             .all_source_breakpoints(cx)
-            .clone()
     });
 
     assert_eq!(0, breakpoints.len());
@@ -21026,7 +21257,7 @@ async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
     let abs_path = project.read_with(cx, |project, cx| {
         project
             .absolute_path(&project_path, cx)
-            .map(|path_buf| Arc::from(path_buf.to_owned()))
+            .map(Arc::from)
             .unwrap()
     });
 
@@ -21041,7 +21272,6 @@ async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
             .unwrap()
             .read(cx)
             .all_source_breakpoints(cx)
-            .clone()
     });
 
     assert_breakpoint(
@@ -21062,7 +21292,6 @@ async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
             .unwrap()
             .read(cx)
             .all_source_breakpoints(cx)
-            .clone()
     });
 
     assert_breakpoint(&breakpoints, &abs_path, vec![]);
@@ -21082,7 +21311,6 @@ async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
             .unwrap()
             .read(cx)
             .all_source_breakpoints(cx)
-            .clone()
     });
 
     assert_breakpoint(
@@ -21105,7 +21333,6 @@ async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
             .unwrap()
             .read(cx)
             .all_source_breakpoints(cx)
-            .clone()
     });
 
     assert_breakpoint(
@@ -21128,7 +21355,6 @@ async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
             .unwrap()
             .read(cx)
             .all_source_breakpoints(cx)
-            .clone()
     });
 
     assert_breakpoint(
@@ -21201,7 +21427,7 @@ async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
     let abs_path = project.read_with(cx, |project, cx| {
         project
             .absolute_path(&project_path, cx)
-            .map(|path_buf| Arc::from(path_buf.to_owned()))
+            .map(Arc::from)
             .unwrap()
     });
 
@@ -21221,7 +21447,6 @@ async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
             .unwrap()
             .read(cx)
             .all_source_breakpoints(cx)
-            .clone()
     });
 
     assert_eq!(1, breakpoints.len());
@@ -21253,7 +21478,6 @@ async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
             .unwrap()
             .read(cx)
             .all_source_breakpoints(cx)
-            .clone()
     });
 
     let disable_breakpoint = {
@@ -21289,7 +21513,6 @@ async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
             .unwrap()
             .read(cx)
             .all_source_breakpoints(cx)
-            .clone()
     });
 
     assert_eq!(1, breakpoints.len());
@@ -22268,10 +22491,7 @@ async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
         let closing_range =
             buffer.anchor_before(Point::new(0, 6))..buffer.anchor_after(Point::new(0, 8));
         let mut linked_ranges = HashMap::default();
-        linked_ranges.insert(
-            buffer_id,
-            vec![(opening_range.clone(), vec![closing_range.clone()])],
-        );
+        linked_ranges.insert(buffer_id, vec![(opening_range, vec![closing_range])]);
         editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
     });
     let mut completion_handle =
@@ -22456,7 +22676,7 @@ async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
     );
 
     cx.update(|_, cx| {
-        workspace::reload(&workspace::Reload::default(), cx);
+        workspace::reload(cx);
     });
     assert_language_servers_count(
         1,
@@ -23381,7 +23601,7 @@ pub fn handle_completion_request(
                     complete_from_position
                 );
                 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
-                    is_incomplete: is_incomplete,
+                    is_incomplete,
                     item_defaults: None,
                     items: completions
                         .iter()

crates/editor/src/element.rs 🔗

@@ -40,14 +40,15 @@ use git::{
 };
 use gpui::{
     Action, Along, AnyElement, App, AppContext, AvailableSpace, Axis as ScrollbarAxis, BorderStyle,
-    Bounds, ClickEvent, ContentMask, Context, Corner, Corners, CursorStyle, DispatchPhase, Edges,
-    Element, ElementInputHandler, Entity, Focusable as _, FontId, GlobalElementId, Hitbox,
-    HitboxBehavior, Hsla, InteractiveElement, IntoElement, IsZero, Keystroke, Length,
-    ModifiersChangedEvent, MouseButton, MouseClickEvent, MouseDownEvent, MouseMoveEvent,
-    MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta, ScrollHandle, ScrollWheelEvent,
-    ShapedLine, SharedString, Size, StatefulInteractiveElement, Style, Styled, TextRun,
-    TextStyleRefinement, WeakEntity, Window, anchored, deferred, div, fill, linear_color_stop,
-    linear_gradient, outline, point, px, quad, relative, size, solid_background, transparent_black,
+    Bounds, ClickEvent, ClipboardItem, ContentMask, Context, Corner, Corners, CursorStyle,
+    DispatchPhase, Edges, Element, ElementInputHandler, Entity, Focusable as _, FontId,
+    GlobalElementId, Hitbox, HitboxBehavior, Hsla, InteractiveElement, IntoElement, IsZero,
+    Keystroke, Length, ModifiersChangedEvent, MouseButton, MouseClickEvent, MouseDownEvent,
+    MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta, ScrollHandle,
+    ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement, Style, Styled,
+    TextRun, TextStyleRefinement, WeakEntity, Window, anchored, deferred, div, fill,
+    linear_color_stop, linear_gradient, outline, point, px, quad, relative, size, solid_background,
+    transparent_black,
 };
 use itertools::Itertools;
 use language::language_settings::{
@@ -60,7 +61,7 @@ use multi_buffer::{
 };
 
 use project::{
-    ProjectPath,
+    Entry, ProjectPath,
     debugger::breakpoint_store::{Breakpoint, BreakpointSessionState},
     project_settings::{GitGutterSetting, GitHunkStyleSetting, ProjectSettings},
 };
@@ -80,11 +81,17 @@ use std::{
 use sum_tree::Bias;
 use text::{BufferId, SelectionGoal};
 use theme::{ActiveTheme, Appearance, BufferLineHeight, PlayerColor};
-use ui::{ButtonLike, KeyBinding, POPOVER_Y_PADDING, Tooltip, h_flex, prelude::*};
+use ui::{
+    ButtonLike, ContextMenu, Indicator, KeyBinding, POPOVER_Y_PADDING, Tooltip, h_flex, prelude::*,
+    right_click_menu,
+};
 use unicode_segmentation::UnicodeSegmentation;
 use util::post_inc;
 use util::{RangeExt, ResultExt, debug_panic};
-use workspace::{CollaboratorId, Workspace, item::Item, notifications::NotifyTaskExt};
+use workspace::{
+    CollaboratorId, OpenInTerminal, OpenTerminal, RevealInProjectPanel, Workspace, item::Item,
+    notifications::NotifyTaskExt,
+};
 
 /// Determines what kinds of highlights should be applied to a lines background.
 #[derive(Clone, Copy, Default)]
@@ -717,7 +724,7 @@ impl EditorElement {
                         ColumnarMode::FromMouse => true,
                         ColumnarMode::FromSelection => false,
                     },
-                    mode: mode,
+                    mode,
                     goal_column: point_for_position.exact_unclipped.column(),
                 },
                 window,
@@ -910,6 +917,11 @@ impl EditorElement {
         } else if cfg!(any(target_os = "linux", target_os = "freebsd"))
             && event.button == MouseButton::Middle
         {
+            #[allow(
+                clippy::collapsible_if,
+                clippy::needless_return,
+                reason = "The cfg-block below makes this a false positive"
+            )]
             if !text_hitbox.is_hovered(window) || editor.read_only(cx) {
                 return;
             }
@@ -1115,26 +1127,24 @@ impl EditorElement {
 
         let hovered_diff_hunk_row = if let Some(control_row) = hovered_diff_control {
             Some(control_row)
-        } else {
-            if text_hovered {
-                let current_row = valid_point.row();
-                position_map.display_hunks.iter().find_map(|(hunk, _)| {
-                    if let DisplayDiffHunk::Unfolded {
-                        display_row_range, ..
-                    } = hunk
-                    {
-                        if display_row_range.contains(&current_row) {
-                            Some(display_row_range.start)
-                        } else {
-                            None
-                        }
+        } else if text_hovered {
+            let current_row = valid_point.row();
+            position_map.display_hunks.iter().find_map(|(hunk, _)| {
+                if let DisplayDiffHunk::Unfolded {
+                    display_row_range, ..
+                } = hunk
+                {
+                    if display_row_range.contains(&current_row) {
+                        Some(display_row_range.start)
                     } else {
                         None
                     }
-                })
-            } else {
-                None
-            }
+                } else {
+                    None
+                }
+            })
+        } else {
+            None
         };
 
         if hovered_diff_hunk_row != editor.hovered_diff_hunk_row {
@@ -1148,14 +1158,14 @@ impl EditorElement {
                 .inline_blame_popover
                 .as_ref()
                 .and_then(|state| state.popover_bounds)
-                .map_or(false, |bounds| bounds.contains(&event.position));
+                .is_some_and(|bounds| bounds.contains(&event.position));
             let keyboard_grace = editor
                 .inline_blame_popover
                 .as_ref()
-                .map_or(false, |state| state.keyboard_grace);
+                .is_some_and(|state| state.keyboard_grace);
 
             if mouse_over_inline_blame || mouse_over_popover {
-                editor.show_blame_popover(&blame_entry, event.position, false, cx);
+                editor.show_blame_popover(blame_entry, event.position, false, cx);
             } else if !keyboard_grace {
                 editor.hide_blame_popover(cx);
             }
@@ -1179,10 +1189,10 @@ impl EditorElement {
                 let is_visible = editor
                     .gutter_breakpoint_indicator
                     .0
-                    .map_or(false, |indicator| indicator.is_active);
+                    .is_some_and(|indicator| indicator.is_active);
 
                 let has_existing_breakpoint =
-                    editor.breakpoint_store.as_ref().map_or(false, |store| {
+                    editor.breakpoint_store.as_ref().is_some_and(|store| {
                         let Some(project) = &editor.project else {
                             return false;
                         };
@@ -1380,29 +1390,27 @@ impl EditorElement {
                     ref drop_cursor,
                     ref hide_drop_cursor,
                 } = editor.selection_drag_state
+                    && !hide_drop_cursor
+                    && (drop_cursor
+                        .start
+                        .cmp(&selection.start, &snapshot.buffer_snapshot)
+                        .eq(&Ordering::Less)
+                        || drop_cursor
+                            .end
+                            .cmp(&selection.end, &snapshot.buffer_snapshot)
+                            .eq(&Ordering::Greater))
                 {
-                    if !hide_drop_cursor
-                        && (drop_cursor
-                            .start
-                            .cmp(&selection.start, &snapshot.buffer_snapshot)
-                            .eq(&Ordering::Less)
-                            || drop_cursor
-                                .end
-                                .cmp(&selection.end, &snapshot.buffer_snapshot)
-                                .eq(&Ordering::Greater))
-                    {
-                        let drag_cursor_layout = SelectionLayout::new(
-                            drop_cursor.clone(),
-                            false,
-                            CursorShape::Bar,
-                            &snapshot.display_snapshot,
-                            false,
-                            false,
-                            None,
-                        );
-                        let absent_color = cx.theme().players().absent();
-                        selections.push((absent_color, vec![drag_cursor_layout]));
-                    }
+                    let drag_cursor_layout = SelectionLayout::new(
+                        drop_cursor.clone(),
+                        false,
+                        CursorShape::Bar,
+                        &snapshot.display_snapshot,
+                        false,
+                        false,
+                        None,
+                    );
+                    let absent_color = cx.theme().players().absent();
+                    selections.push((absent_color, vec![drag_cursor_layout]));
                 }
             }
 
@@ -1413,19 +1421,15 @@ impl EditorElement {
                         CollaboratorId::PeerId(peer_id) => {
                             if let Some(collaborator) =
                                 collaboration_hub.collaborators(cx).get(&peer_id)
-                            {
-                                if let Some(participant_index) = collaboration_hub
+                                && let Some(participant_index) = collaboration_hub
                                     .user_participant_indices(cx)
                                     .get(&collaborator.user_id)
-                                {
-                                    if let Some((local_selection_style, _)) = selections.first_mut()
-                                    {
-                                        *local_selection_style = cx
-                                            .theme()
-                                            .players()
-                                            .color_for_participant(participant_index.0);
-                                    }
-                                }
+                                && let Some((local_selection_style, _)) = selections.first_mut()
+                            {
+                                *local_selection_style = cx
+                                    .theme()
+                                    .players()
+                                    .color_for_participant(participant_index.0);
                             }
                         }
                         CollaboratorId::Agent => {
@@ -2168,11 +2172,13 @@ impl EditorElement {
         };
 
         let padding = ProjectSettings::get_global(cx).diagnostics.inline.padding as f32 * em_width;
-        let min_x = ProjectSettings::get_global(cx)
-            .diagnostics
-            .inline
-            .min_column as f32
-            * em_width;
+        let min_x = self.column_pixels(
+            ProjectSettings::get_global(cx)
+                .diagnostics
+                .inline
+                .min_column as usize,
+            window,
+        );
 
         let mut elements = HashMap::default();
         for (row, mut diagnostics) in diagnostics_by_rows {
@@ -2213,12 +2219,11 @@ impl EditorElement {
                 cmp::max(padded_line, min_start)
             };
 
-            let behind_edit_prediction_popover = edit_prediction_popover_origin.as_ref().map_or(
-                false,
-                |edit_prediction_popover_origin| {
+            let behind_edit_prediction_popover = edit_prediction_popover_origin
+                .as_ref()
+                .is_some_and(|edit_prediction_popover_origin| {
                     (pos_y..pos_y + line_height).contains(&edit_prediction_popover_origin.y)
-                },
-            );
+                });
             let opacity = if behind_edit_prediction_popover {
                 0.5
             } else {
@@ -2284,9 +2289,7 @@ impl EditorElement {
                         None
                     }
                 })
-                .map_or(false, |source| {
-                    matches!(source, CodeActionSource::Indicator(..))
-                });
+                .is_some_and(|source| matches!(source, CodeActionSource::Indicator(..)));
             Some(editor.render_inline_code_actions(icon_size, display_point.row(), active, cx))
         })?;
 
@@ -2434,14 +2437,13 @@ impl EditorElement {
                 .unwrap_or_default()
                 .padding as f32;
 
-            if let Some(edit_prediction) = editor.active_edit_prediction.as_ref() {
-                match &edit_prediction.completion {
-                    EditPrediction::Edit {
-                        display_mode: EditDisplayMode::TabAccept,
-                        ..
-                    } => padding += INLINE_ACCEPT_SUGGESTION_EM_WIDTHS,
-                    _ => {}
-                }
+            if let Some(edit_prediction) = editor.active_edit_prediction.as_ref()
+                && let EditPrediction::Edit {
+                    display_mode: EditDisplayMode::TabAccept,
+                    ..
+                } = &edit_prediction.completion
+            {
+                padding += INLINE_ACCEPT_SUGGESTION_EM_WIDTHS
             }
 
             padding * em_width
@@ -2747,7 +2749,10 @@ impl EditorElement {
         let mut block_offset = 0;
         let mut found_excerpt_header = false;
         for (_, block) in snapshot.blocks_in_range(prev_line..row_range.start) {
-            if matches!(block, Block::ExcerptBoundary { .. }) {
+            if matches!(
+                block,
+                Block::ExcerptBoundary { .. } | Block::BufferHeader { .. }
+            ) {
                 found_excerpt_header = true;
                 break;
             }
@@ -2764,7 +2769,10 @@ impl EditorElement {
         let mut block_height = 0;
         let mut found_excerpt_header = false;
         for (_, block) in snapshot.blocks_in_range(row_range.end..cons_line) {
-            if matches!(block, Block::ExcerptBoundary { .. }) {
+            if matches!(
+                block,
+                Block::ExcerptBoundary { .. } | Block::BufferHeader { .. }
+            ) {
                 found_excerpt_header = true;
             }
             block_height += block.height();
@@ -2811,7 +2819,7 @@ impl EditorElement {
                     }
 
                     let row =
-                        MultiBufferRow(DisplayPoint::new(display_row, 0).to_point(&snapshot).row);
+                        MultiBufferRow(DisplayPoint::new(display_row, 0).to_point(snapshot).row);
                     if snapshot.is_line_folded(row) {
                         return None;
                     }
@@ -2902,7 +2910,7 @@ impl EditorElement {
                         if multibuffer_row
                             .0
                             .checked_sub(1)
-                            .map_or(false, |previous_row| {
+                            .is_some_and(|previous_row| {
                                 snapshot.is_line_folded(MultiBufferRow(previous_row))
                             })
                         {
@@ -2975,8 +2983,8 @@ impl EditorElement {
             .ilog10()
             + 1;
 
-        let elements = buffer_rows
-            .into_iter()
+        buffer_rows
+            .iter()
             .enumerate()
             .map(|(ix, row_info)| {
                 let ExpandInfo {
@@ -3011,7 +3019,7 @@ impl EditorElement {
                     .icon_color(Color::Custom(cx.theme().colors().editor_line_number))
                     .selected_icon_color(Color::Custom(cx.theme().colors().editor_foreground))
                     .icon_size(IconSize::Custom(rems(editor_font_size / window.rem_size())))
-                    .width(width.into())
+                    .width(width)
                     .on_click(move |_, window, cx| {
                         editor.update(cx, |editor, cx| {
                             editor.expand_excerpt(excerpt_id, direction, window, cx);
@@ -3031,9 +3039,7 @@ impl EditorElement {
 
                 Some((toggle, origin))
             })
-            .collect();
-
-        elements
+            .collect()
     }
 
     fn calculate_relative_line_numbers(
@@ -3133,7 +3139,7 @@ impl EditorElement {
         let relative_rows = self.calculate_relative_line_numbers(snapshot, &rows, relative_to);
         let mut line_number = String::new();
         let line_numbers = buffer_rows
-            .into_iter()
+            .iter()
             .enumerate()
             .flat_map(|(ix, row_info)| {
                 let display_row = DisplayRow(rows.start.0 + ix as u32);
@@ -3210,7 +3216,7 @@ impl EditorElement {
             && self.editor.read(cx).is_singleton(cx);
         if include_fold_statuses {
             row_infos
-                .into_iter()
+                .iter()
                 .enumerate()
                 .map(|(ix, info)| {
                     if info.expand_info.is_some() {
@@ -3305,7 +3311,7 @@ impl EditorElement {
             let chunks = snapshot.highlighted_chunks(rows.clone(), true, style);
             LineWithInvisibles::from_chunks(
                 chunks,
-                &style,
+                style,
                 MAX_LINE_LEN,
                 rows.len(),
                 &snapshot.mode,
@@ -3386,7 +3392,7 @@ impl EditorElement {
                 let line_ix = align_to.row().0.checked_sub(rows.start.0);
                 x_position =
                     if let Some(layout) = line_ix.and_then(|ix| line_layouts.get(ix as usize)) {
-                        x_and_width(&layout)
+                        x_and_width(layout)
                     } else {
                         x_and_width(&layout_line(
                             align_to.row(),
@@ -3452,42 +3458,41 @@ impl EditorElement {
                     .into_any_element()
             }
 
-            Block::ExcerptBoundary {
-                excerpt,
-                height,
-                starts_new_buffer,
-                ..
-            } => {
+            Block::ExcerptBoundary { .. } => {
                 let color = cx.theme().colors().clone();
                 let mut result = v_flex().id(block_id).w_full();
 
+                result = result.child(
+                    h_flex().relative().child(
+                        div()
+                            .top(line_height / 2.)
+                            .absolute()
+                            .w_full()
+                            .h_px()
+                            .bg(color.border_variant),
+                    ),
+                );
+
+                result.into_any()
+            }
+
+            Block::BufferHeader { excerpt, height } => {
+                let mut result = v_flex().id(block_id).w_full();
+
                 let jump_data = header_jump_data(snapshot, block_row_start, *height, excerpt);
 
-                if *starts_new_buffer {
-                    if sticky_header_excerpt_id != Some(excerpt.id) {
-                        let selected = selected_buffer_ids.contains(&excerpt.buffer_id);
+                if sticky_header_excerpt_id != Some(excerpt.id) {
+                    let selected = selected_buffer_ids.contains(&excerpt.buffer_id);
 
-                        result = result.child(div().pr(editor_margins.right).child(
-                            self.render_buffer_header(
-                                excerpt, false, selected, false, jump_data, window, cx,
-                            ),
-                        ));
-                    } else {
-                        result =
-                            result.child(div().h(FILE_HEADER_HEIGHT as f32 * window.line_height()));
-                    }
-                } else {
-                    result = result.child(
-                        h_flex().relative().child(
-                            div()
-                                .top(line_height / 2.)
-                                .absolute()
-                                .w_full()
-                                .h_px()
-                                .bg(color.border_variant),
+                    result = result.child(div().pr(editor_margins.right).child(
+                        self.render_buffer_header(
+                            excerpt, false, selected, false, jump_data, window, cx,
                         ),
-                    );
-                };
+                    ));
+                } else {
+                    result =
+                        result.child(div().h(FILE_HEADER_HEIGHT as f32 * window.line_height()));
+                }
 
                 result.into_any()
             }
@@ -3511,33 +3516,33 @@ impl EditorElement {
         let mut x_offset = px(0.);
         let mut is_block = true;
 
-        if let BlockId::Custom(custom_block_id) = block_id {
-            if block.has_height() {
-                if block.place_near() {
-                    if let Some((x_target, line_width)) = x_position {
-                        let margin = em_width * 2;
-                        if line_width + final_size.width + margin
-                            < editor_width + editor_margins.gutter.full_width()
-                            && !row_block_types.contains_key(&(row - 1))
-                            && element_height_in_lines == 1
-                        {
-                            x_offset = line_width + margin;
-                            row = row - 1;
-                            is_block = false;
-                            element_height_in_lines = 0;
-                            row_block_types.insert(row, is_block);
-                        } else {
-                            let max_offset = editor_width + editor_margins.gutter.full_width()
-                                - final_size.width;
-                            let min_offset = (x_target + em_width - final_size.width)
-                                .max(editor_margins.gutter.full_width());
-                            x_offset = x_target.min(max_offset).max(min_offset);
-                        }
-                    }
-                };
-                if element_height_in_lines != block.height() {
-                    resized_blocks.insert(custom_block_id, element_height_in_lines);
+        if let BlockId::Custom(custom_block_id) = block_id
+            && block.has_height()
+        {
+            if block.place_near()
+                && let Some((x_target, line_width)) = x_position
+            {
+                let margin = em_width * 2;
+                if line_width + final_size.width + margin
+                    < editor_width + editor_margins.gutter.full_width()
+                    && !row_block_types.contains_key(&(row - 1))
+                    && element_height_in_lines == 1
+                {
+                    x_offset = line_width + margin;
+                    row = row - 1;
+                    is_block = false;
+                    element_height_in_lines = 0;
+                    row_block_types.insert(row, is_block);
+                } else {
+                    let max_offset =
+                        editor_width + editor_margins.gutter.full_width() - final_size.width;
+                    let min_offset = (x_target + em_width - final_size.width)
+                        .max(editor_margins.gutter.full_width());
+                    x_offset = x_target.min(max_offset).max(min_offset);
                 }
+            };
+            if element_height_in_lines != block.height() {
+                resized_blocks.insert(custom_block_id, element_height_in_lines);
             }
         }
         for i in 0..element_height_in_lines {
@@ -3556,11 +3561,10 @@ impl EditorElement {
         jump_data: JumpData,
         window: &mut Window,
         cx: &mut App,
-    ) -> Div {
+    ) -> impl IntoElement {
         let editor = self.editor.read(cx);
-        let file_status = editor
-            .buffer
-            .read(cx)
+        let multi_buffer = editor.buffer.read(cx);
+        let file_status = multi_buffer
             .all_diff_hunks_expanded()
             .then(|| {
                 editor
@@ -3570,6 +3574,17 @@ impl EditorElement {
                     .status_for_buffer_id(for_excerpt.buffer_id, cx)
             })
             .flatten();
+        let indicator = multi_buffer
+            .buffer(for_excerpt.buffer_id)
+            .and_then(|buffer| {
+                let buffer = buffer.read(cx);
+                let indicator_color = match (buffer.has_conflict(), buffer.is_dirty()) {
+                    (true, _) => Some(Color::Warning),
+                    (_, true) => Some(Color::Accent),
+                    (false, false) => None,
+                };
+                indicator_color.map(|indicator_color| Indicator::dot().color(indicator_color))
+            });
 
         let include_root = editor
             .project
@@ -3577,126 +3592,126 @@ impl EditorElement {
             .map(|project| project.read(cx).visible_worktrees(cx).count() > 1)
             .unwrap_or_default();
         let can_open_excerpts = Editor::can_open_excerpts_in_file(for_excerpt.buffer.file());
-        let path = for_excerpt.buffer.resolve_file_path(cx, include_root);
-        let filename = path
+        let relative_path = for_excerpt.buffer.resolve_file_path(cx, include_root);
+        let filename = relative_path
             .as_ref()
             .and_then(|path| Some(path.file_name()?.to_string_lossy().to_string()));
-        let parent_path = path.as_ref().and_then(|path| {
+        let parent_path = relative_path.as_ref().and_then(|path| {
             Some(path.parent()?.to_string_lossy().to_string() + std::path::MAIN_SEPARATOR_STR)
         });
         let focus_handle = editor.focus_handle(cx);
         let colors = cx.theme().colors();
 
-        div()
-            .p_1()
-            .w_full()
-            .h(FILE_HEADER_HEIGHT as f32 * window.line_height())
-            .child(
-                h_flex()
-                    .size_full()
-                    .gap_2()
-                    .flex_basis(Length::Definite(DefiniteLength::Fraction(0.667)))
-                    .pl_0p5()
-                    .pr_5()
-                    .rounded_sm()
-                    .when(is_sticky, |el| el.shadow_md())
-                    .border_1()
-                    .map(|div| {
-                        let border_color = if is_selected
-                            && is_folded
-                            && focus_handle.contains_focused(window, cx)
-                        {
-                            colors.border_focused
-                        } else {
-                            colors.border
-                        };
-                        div.border_color(border_color)
-                    })
-                    .bg(colors.editor_subheader_background)
-                    .hover(|style| style.bg(colors.element_hover))
-                    .map(|header| {
-                        let editor = self.editor.clone();
-                        let buffer_id = for_excerpt.buffer_id;
-                        let toggle_chevron_icon =
-                            FileIcons::get_chevron_icon(!is_folded, cx).map(Icon::from_path);
-                        header.child(
-                            div()
-                                .hover(|style| style.bg(colors.element_selected))
-                                .rounded_xs()
-                                .child(
-                                    ButtonLike::new("toggle-buffer-fold")
-                                        .style(ui::ButtonStyle::Transparent)
-                                        .height(px(28.).into())
-                                        .width(px(28.).into())
-                                        .children(toggle_chevron_icon)
-                                        .tooltip({
-                                            let focus_handle = focus_handle.clone();
-                                            move |window, cx| {
-                                                Tooltip::with_meta_in(
-                                                    "Toggle Excerpt Fold",
-                                                    Some(&ToggleFold),
-                                                    "Alt+click to toggle all",
-                                                    &focus_handle,
-                                                    window,
-                                                    cx,
-                                                )
-                                            }
-                                        })
-                                        .on_click(move |event, window, cx| {
-                                            if event.modifiers().alt {
-                                                // Alt+click toggles all buffers
-                                                editor.update(cx, |editor, cx| {
-                                                    editor.toggle_fold_all(
-                                                        &ToggleFoldAll,
+        let header =
+            div()
+                .p_1()
+                .w_full()
+                .h(FILE_HEADER_HEIGHT as f32 * window.line_height())
+                .child(
+                    h_flex()
+                        .size_full()
+                        .gap_2()
+                        .flex_basis(Length::Definite(DefiniteLength::Fraction(0.667)))
+                        .pl_0p5()
+                        .pr_5()
+                        .rounded_sm()
+                        .when(is_sticky, |el| el.shadow_md())
+                        .border_1()
+                        .map(|div| {
+                            let border_color = if is_selected
+                                && is_folded
+                                && focus_handle.contains_focused(window, cx)
+                            {
+                                colors.border_focused
+                            } else {
+                                colors.border
+                            };
+                            div.border_color(border_color)
+                        })
+                        .bg(colors.editor_subheader_background)
+                        .hover(|style| style.bg(colors.element_hover))
+                        .map(|header| {
+                            let editor = self.editor.clone();
+                            let buffer_id = for_excerpt.buffer_id;
+                            let toggle_chevron_icon =
+                                FileIcons::get_chevron_icon(!is_folded, cx).map(Icon::from_path);
+                            header.child(
+                                div()
+                                    .hover(|style| style.bg(colors.element_selected))
+                                    .rounded_xs()
+                                    .child(
+                                        ButtonLike::new("toggle-buffer-fold")
+                                            .style(ui::ButtonStyle::Transparent)
+                                            .height(px(28.).into())
+                                            .width(px(28.))
+                                            .children(toggle_chevron_icon)
+                                            .tooltip({
+                                                let focus_handle = focus_handle.clone();
+                                                move |window, cx| {
+                                                    Tooltip::with_meta_in(
+                                                        "Toggle Excerpt Fold",
+                                                        Some(&ToggleFold),
+                                                        "Alt+click to toggle all",
+                                                        &focus_handle,
                                                         window,
                                                         cx,
-                                                    );
-                                                });
-                                            } else {
-                                                // Regular click toggles single buffer
-                                                if is_folded {
+                                                    )
+                                                }
+                                            })
+                                            .on_click(move |event, window, cx| {
+                                                if event.modifiers().alt {
+                                                    // Alt+click toggles all buffers
                                                     editor.update(cx, |editor, cx| {
-                                                        editor.unfold_buffer(buffer_id, cx);
+                                                        editor.toggle_fold_all(
+                                                            &ToggleFoldAll,
+                                                            window,
+                                                            cx,
+                                                        );
                                                     });
                                                 } else {
-                                                    editor.update(cx, |editor, cx| {
-                                                        editor.fold_buffer(buffer_id, cx);
-                                                    });
+                                                    // Regular click toggles single buffer
+                                                    if is_folded {
+                                                        editor.update(cx, |editor, cx| {
+                                                            editor.unfold_buffer(buffer_id, cx);
+                                                        });
+                                                    } else {
+                                                        editor.update(cx, |editor, cx| {
+                                                            editor.fold_buffer(buffer_id, cx);
+                                                        });
+                                                    }
                                                 }
-                                            }
-                                        }),
-                                ),
+                                            }),
+                                    ),
+                            )
+                        })
+                        .children(
+                            editor
+                                .addons
+                                .values()
+                                .filter_map(|addon| {
+                                    addon.render_buffer_header_controls(for_excerpt, window, cx)
+                                })
+                                .take(1),
                         )
-                    })
-                    .children(
-                        editor
-                            .addons
-                            .values()
-                            .filter_map(|addon| {
-                                addon.render_buffer_header_controls(for_excerpt, window, cx)
-                            })
-                            .take(1),
-                    )
-                    .child(
-                        h_flex()
-                            .cursor_pointer()
-                            .id("path header block")
-                            .size_full()
-                            .justify_between()
-                            .overflow_hidden()
-                            .child(
-                                h_flex()
-                                    .gap_2()
-                                    .child(
-                                        Label::new(
-                                            filename
-                                                .map(SharedString::from)
-                                                .unwrap_or_else(|| "untitled".into()),
-                                        )
-                                        .single_line()
-                                        .when_some(
-                                            file_status,
-                                            |el, status| {
+                        .children(indicator)
+                        .child(
+                            h_flex()
+                                .cursor_pointer()
+                                .id("path header block")
+                                .size_full()
+                                .justify_between()
+                                .overflow_hidden()
+                                .child(
+                                    h_flex()
+                                        .gap_2()
+                                        .child(
+                                            Label::new(
+                                                filename
+                                                    .map(SharedString::from)
+                                                    .unwrap_or_else(|| "untitled".into()),
+                                            )
+                                            .single_line()
+                                            .when_some(file_status, |el, status| {
                                                 el.color(if status.is_conflicted() {
                                                     Color::Conflict
                                                 } else if status.is_modified() {
@@ -3707,49 +3722,145 @@ impl EditorElement {
                                                     Color::Created
                                                 })
                                                 .when(status.is_deleted(), |el| el.strikethrough())
-                                            },
-                                        ),
-                                    )
-                                    .when_some(parent_path, |then, path| {
-                                        then.child(div().child(path).text_color(
-                                            if file_status.is_some_and(FileStatus::is_deleted) {
-                                                colors.text_disabled
-                                            } else {
-                                                colors.text_muted
-                                            },
-                                        ))
+                                            }),
+                                        )
+                                        .when_some(parent_path, |then, path| {
+                                            then.child(div().child(path).text_color(
+                                                if file_status.is_some_and(FileStatus::is_deleted) {
+                                                    colors.text_disabled
+                                                } else {
+                                                    colors.text_muted
+                                                },
+                                            ))
+                                        }),
+                                )
+                                .when(
+                                    can_open_excerpts && is_selected && relative_path.is_some(),
+                                    |el| {
+                                        el.child(
+                                            h_flex()
+                                                .id("jump-to-file-button")
+                                                .gap_2p5()
+                                                .child(Label::new("Jump To File"))
+                                                .children(
+                                                    KeyBinding::for_action_in(
+                                                        &OpenExcerpts,
+                                                        &focus_handle,
+                                                        window,
+                                                        cx,
+                                                    )
+                                                    .map(|binding| binding.into_any_element()),
+                                                ),
+                                        )
+                                    },
+                                )
+                                .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
+                                .on_click(window.listener_for(&self.editor, {
+                                    move |editor, e: &ClickEvent, window, cx| {
+                                        editor.open_excerpts_common(
+                                            Some(jump_data.clone()),
+                                            e.modifiers().secondary(),
+                                            window,
+                                            cx,
+                                        );
+                                    }
+                                })),
+                        ),
+                );
+
+        let file = for_excerpt.buffer.file().cloned();
+        let editor = self.editor.clone();
+        right_click_menu("buffer-header-context-menu")
+            .trigger(move |_, _, _| header)
+            .menu(move |window, cx| {
+                let menu_context = focus_handle.clone();
+                let editor = editor.clone();
+                let file = file.clone();
+                ContextMenu::build(window, cx, move |mut menu, window, cx| {
+                    if let Some(file) = file
+                        && let Some(project) = editor.read(cx).project()
+                        && let Some(worktree) =
+                            project.read(cx).worktree_for_id(file.worktree_id(cx), cx)
+                    {
+                        let relative_path = file.path();
+                        let entry_for_path = worktree.read(cx).entry_for_path(relative_path);
+                        let abs_path = entry_for_path.and_then(|e| e.canonical_path.as_deref());
+                        let has_relative_path =
+                            worktree.read(cx).root_entry().is_some_and(Entry::is_dir);
+
+                        let parent_abs_path =
+                            abs_path.and_then(|abs_path| Some(abs_path.parent()?.to_path_buf()));
+                        let relative_path = has_relative_path
+                            .then_some(relative_path)
+                            .map(ToOwned::to_owned);
+
+                        let visible_in_project_panel =
+                            relative_path.is_some() && worktree.read(cx).is_visible();
+                        let reveal_in_project_panel = entry_for_path
+                            .filter(|_| visible_in_project_panel)
+                            .map(|entry| entry.id);
+                        menu = menu
+                            .when_some(abs_path.map(ToOwned::to_owned), |menu, abs_path| {
+                                menu.entry(
+                                    "Copy Path",
+                                    Some(Box::new(zed_actions::workspace::CopyPath)),
+                                    window.handler_for(&editor, move |_, _, cx| {
+                                        cx.write_to_clipboard(ClipboardItem::new_string(
+                                            abs_path.to_string_lossy().to_string(),
+                                        ));
+                                    }),
+                                )
+                            })
+                            .when_some(relative_path, |menu, relative_path| {
+                                menu.entry(
+                                    "Copy Relative Path",
+                                    Some(Box::new(zed_actions::workspace::CopyRelativePath)),
+                                    window.handler_for(&editor, move |_, _, cx| {
+                                        cx.write_to_clipboard(ClipboardItem::new_string(
+                                            relative_path.to_string_lossy().to_string(),
+                                        ));
                                     }),
+                                )
+                            })
+                            .when(
+                                reveal_in_project_panel.is_some() || parent_abs_path.is_some(),
+                                |menu| menu.separator(),
                             )
-                            .when(can_open_excerpts && is_selected && path.is_some(), |el| {
-                                el.child(
-                                    h_flex()
-                                        .id("jump-to-file-button")
-                                        .gap_2p5()
-                                        .child(Label::new("Jump To File"))
-                                        .children(
-                                            KeyBinding::for_action_in(
-                                                &OpenExcerpts,
-                                                &focus_handle,
-                                                window,
-                                                cx,
-                                            )
-                                            .map(|binding| binding.into_any_element()),
-                                        ),
+                            .when_some(reveal_in_project_panel, |menu, entry_id| {
+                                menu.entry(
+                                    "Reveal In Project Panel",
+                                    Some(Box::new(RevealInProjectPanel::default())),
+                                    window.handler_for(&editor, move |editor, _, cx| {
+                                        if let Some(project) = &mut editor.project {
+                                            project.update(cx, |_, cx| {
+                                                cx.emit(project::Event::RevealInProjectPanel(
+                                                    entry_id,
+                                                ))
+                                            });
+                                        }
+                                    }),
                                 )
                             })
-                            .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
-                            .on_click(window.listener_for(&self.editor, {
-                                move |editor, e: &ClickEvent, window, cx| {
-                                    editor.open_excerpts_common(
-                                        Some(jump_data.clone()),
-                                        e.modifiers().secondary(),
-                                        window,
-                                        cx,
-                                    );
-                                }
-                            })),
-                    ),
-            )
+                            .when_some(parent_abs_path, |menu, parent_abs_path| {
+                                menu.entry(
+                                    "Open in Terminal",
+                                    Some(Box::new(OpenInTerminal)),
+                                    window.handler_for(&editor, move |_, window, cx| {
+                                        window.dispatch_action(
+                                            OpenTerminal {
+                                                working_directory: parent_abs_path.clone(),
+                                            }
+                                            .boxed_clone(),
+                                            cx,
+                                        );
+                                    }),
+                                )
+                            });
+                    }
+
+                    menu.context(menu_context)
+                })
+            })
     }
 
     fn render_blocks(
@@ -3787,7 +3898,7 @@ impl EditorElement {
         for (row, block) in fixed_blocks {
             let block_id = block.id();
 
-            if focused_block.as_ref().map_or(false, |b| b.id == block_id) {
+            if focused_block.as_ref().is_some_and(|b| b.id == block_id) {
                 focused_block = None;
             }
 
@@ -3844,7 +3955,7 @@ impl EditorElement {
             };
             let block_id = block.id();
 
-            if focused_block.as_ref().map_or(false, |b| b.id == block_id) {
+            if focused_block.as_ref().is_some_and(|b| b.id == block_id) {
                 focused_block = None;
             }
 

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

@@ -213,8 +213,8 @@ impl GitBlame {
         let project_subscription = cx.subscribe(&project, {
             let buffer = buffer.clone();
 
-            move |this, _, event, cx| match event {
-                project::Event::WorktreeUpdatedEntries(_, updated) => {
+            move |this, _, event, cx| {
+                if let project::Event::WorktreeUpdatedEntries(_, updated) = event {
                     let project_entry_id = buffer.read(cx).entry_id(cx);
                     if updated
                         .iter()
@@ -224,7 +224,6 @@ impl GitBlame {
                         this.generate(cx);
                     }
                 }
-                _ => {}
             }
         });
 
@@ -292,7 +291,7 @@ impl GitBlame {
 
         let buffer_id = self.buffer_snapshot.remote_id();
         let mut cursor = self.entries.cursor::<u32>(&());
-        rows.into_iter().map(move |info| {
+        rows.iter().map(move |info| {
             let row = info
                 .buffer_row
                 .filter(|_| info.buffer_id == Some(buffer_id))?;
@@ -312,10 +311,10 @@ impl GitBlame {
                 .as_ref()
                 .and_then(|entry| entry.author.as_ref())
                 .map(|author| author.len());
-            if let Some(author_len) = author_len {
-                if author_len > max_author_length {
-                    max_author_length = author_len;
-                }
+            if let Some(author_len) = author_len
+                && author_len > max_author_length
+            {
+                max_author_length = author_len;
             }
         }
 
@@ -415,21 +414,20 @@ impl GitBlame {
             let old_end = cursor.end();
             if row_edits
                 .peek()
-                .map_or(true, |next_edit| next_edit.old.start >= old_end)
+                .is_none_or(|next_edit| next_edit.old.start >= old_end)
+                && let Some(entry) = cursor.item()
             {
-                if let Some(entry) = cursor.item() {
-                    if old_end > edit.old.end {
-                        new_entries.push(
-                            GitBlameEntry {
-                                rows: cursor.end() - edit.old.end,
-                                blame: entry.blame.clone(),
-                            },
-                            &(),
-                        );
-                    }
-
-                    cursor.next();
+                if old_end > edit.old.end {
+                    new_entries.push(
+                        GitBlameEntry {
+                            rows: cursor.end() - edit.old.end,
+                            blame: entry.blame.clone(),
+                        },
+                        &(),
+                    );
                 }
+
+                cursor.next();
             }
         }
         new_entries.append(cursor.suffix(), &());

crates/editor/src/highlight_matching_bracket.rs 🔗

@@ -1,6 +1,7 @@
 use crate::{Editor, RangeToAnchorExt};
-use gpui::{Context, Window};
+use gpui::{Context, HighlightStyle, Window};
 use language::CursorShape;
+use theme::ActiveTheme;
 
 enum MatchingBracketHighlight {}
 
@@ -9,7 +10,7 @@ pub fn refresh_matching_bracket_highlights(
     window: &mut Window,
     cx: &mut Context<Editor>,
 ) {
-    editor.clear_background_highlights::<MatchingBracketHighlight>(cx);
+    editor.clear_highlights::<MatchingBracketHighlight>(cx);
 
     let newest_selection = editor.selections.newest::<usize>(cx);
     // Don't highlight brackets if the selection isn't empty
@@ -35,12 +36,19 @@ pub fn refresh_matching_bracket_highlights(
         .buffer_snapshot
         .innermost_enclosing_bracket_ranges(head..tail, None)
     {
-        editor.highlight_background::<MatchingBracketHighlight>(
-            &[
+        editor.highlight_text::<MatchingBracketHighlight>(
+            vec![
                 opening_range.to_anchors(&snapshot.buffer_snapshot),
                 closing_range.to_anchors(&snapshot.buffer_snapshot),
             ],
-            |theme| theme.colors().editor_document_highlight_bracket_background,
+            HighlightStyle {
+                background_color: Some(
+                    cx.theme()
+                        .colors()
+                        .editor_document_highlight_bracket_background,
+                ),
+                ..Default::default()
+            },
             cx,
         )
     }
@@ -104,7 +112,7 @@ mod tests {
                 another_test(1, 2, 3);
             }
         "#});
-        cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
+        cx.assert_editor_text_highlights::<MatchingBracketHighlight>(indoc! {r#"
             pub fn test«(»"Test argument"«)» {
                 another_test(1, 2, 3);
             }
@@ -115,7 +123,7 @@ mod tests {
                 another_test(1, ˇ2, 3);
             }
         "#});
-        cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
+        cx.assert_editor_text_highlights::<MatchingBracketHighlight>(indoc! {r#"
             pub fn test("Test argument") {
                 another_test«(»1, 2, 3«)»;
             }
@@ -126,7 +134,7 @@ mod tests {
                 anotherˇ_test(1, 2, 3);
             }
         "#});
-        cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
+        cx.assert_editor_text_highlights::<MatchingBracketHighlight>(indoc! {r#"
             pub fn test("Test argument") «{»
                 another_test(1, 2, 3);
             «}»
@@ -138,7 +146,7 @@ mod tests {
                 another_test(1, 2, 3);
             }
         "#});
-        cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
+        cx.assert_editor_text_highlights::<MatchingBracketHighlight>(indoc! {r#"
             pub fn test("Test argument") {
                 another_test(1, 2, 3);
             }
@@ -150,8 +158,8 @@ mod tests {
                 another_test(1, 2, 3);
             }
         "#});
-        cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
-            pub fn test("Test argument") {
+        cx.assert_editor_text_highlights::<MatchingBracketHighlight>(indoc! {r#"
+            pub fn test«("Test argument") {
                 another_test(1, 2, 3);
             }
         "#});

crates/editor/src/hover_links.rs 🔗

@@ -271,7 +271,7 @@ impl Editor {
             Task::ready(Ok(Navigated::No))
         };
         self.select(SelectPhase::End, window, cx);
-        return navigate_task;
+        navigate_task
     }
 }
 
@@ -321,7 +321,10 @@ pub fn update_inlay_link_and_hover_points(
             if let Some(cached_hint) = inlay_hint_cache.hint_by_id(excerpt_id, hovered_hint.id) {
                 match cached_hint.resolve_state {
                     ResolveState::CanResolve(_, _) => {
-                        if let Some(buffer_id) = previous_valid_anchor.buffer_id {
+                        if let Some(buffer_id) = snapshot
+                            .buffer_snapshot
+                            .buffer_id_for_anchor(previous_valid_anchor)
+                        {
                             inlay_hint_cache.spawn_hint_resolve(
                                 buffer_id,
                                 excerpt_id,
@@ -418,24 +421,22 @@ pub fn update_inlay_link_and_hover_points(
                                     }
                                     if let Some((language_server_id, location)) =
                                         hovered_hint_part.location
+                                        && secondary_held
+                                        && !editor.has_pending_nonempty_selection()
                                     {
-                                        if 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,
-                                            );
-                                        }
+                                        go_to_definition_updated = true;
+                                        show_link_definition(
+                                            shift_held,
+                                            editor,
+                                            TriggerPoint::InlayHint(
+                                                highlight,
+                                                location,
+                                                language_server_id,
+                                            ),
+                                            snapshot,
+                                            window,
+                                            cx,
+                                        );
                                     }
                                 }
                             }
@@ -561,7 +562,7 @@ pub fn show_link_definition(
                             provider.definitions(&buffer, buffer_position, preferred_kind, cx)
                         })?;
                         if let Some(task) = task {
-                            task.await.ok().map(|definition_result| {
+                            task.await.ok().flatten().map(|definition_result| {
                                 (
                                     definition_result.iter().find_map(|link| {
                                         link.origin.as_ref().and_then(|origin| {
@@ -657,11 +658,11 @@ pub fn show_link_definition(
 pub(crate) fn find_url(
     buffer: &Entity<language::Buffer>,
     position: text::Anchor,
-    mut cx: AsyncWindowContext,
+    cx: AsyncWindowContext,
 ) -> Option<(Range<text::Anchor>, String)> {
     const LIMIT: usize = 2048;
 
-    let Ok(snapshot) = buffer.read_with(&mut cx, |buffer, _| buffer.snapshot()) else {
+    let Ok(snapshot) = buffer.read_with(&cx, |buffer, _| buffer.snapshot()) else {
         return None;
     };
 
@@ -719,11 +720,11 @@ pub(crate) fn find_url(
 pub(crate) fn find_url_from_range(
     buffer: &Entity<language::Buffer>,
     range: Range<text::Anchor>,
-    mut cx: AsyncWindowContext,
+    cx: AsyncWindowContext,
 ) -> Option<String> {
     const LIMIT: usize = 2048;
 
-    let Ok(snapshot) = buffer.read_with(&mut cx, |buffer, _| buffer.snapshot()) else {
+    let Ok(snapshot) = buffer.read_with(&cx, |buffer, _| buffer.snapshot()) else {
         return None;
     };
 
@@ -766,10 +767,11 @@ pub(crate) fn find_url_from_range(
     let mut finder = LinkFinder::new();
     finder.kinds(&[LinkKind::Url]);
 
-    if let Some(link) = finder.links(&text).next() {
-        if link.start() == 0 && link.end() == text.len() {
-            return Some(link.as_str().to_string());
-        }
+    if let Some(link) = finder.links(&text).next()
+        && link.start() == 0
+        && link.end() == text.len()
+    {
+        return Some(link.as_str().to_string());
     }
 
     None
@@ -794,7 +796,7 @@ pub(crate) async fn find_file(
     ) -> Option<ResolvedPath> {
         project
             .update(cx, |project, cx| {
-                project.resolve_path_in_buffer(&candidate_file_path, buffer, cx)
+                project.resolve_path_in_buffer(candidate_file_path, buffer, cx)
             })
             .ok()?
             .await
@@ -872,7 +874,7 @@ fn surrounding_filename(
         .peekable();
     while let Some(ch) = forwards.next() {
         // Skip escaped whitespace
-        if ch == '\\' && forwards.peek().map_or(false, |ch| ch.is_whitespace()) {
+        if ch == '\\' && forwards.peek().is_some_and(|ch| ch.is_whitespace()) {
             token_end += ch.len_utf8();
             let whitespace = forwards.next().unwrap();
             token_end += whitespace.len_utf8();

crates/editor/src/hover_popover.rs 🔗

@@ -142,11 +142,11 @@ pub fn hover_at_inlay(
             .info_popovers
             .iter()
             .any(|InfoPopover { symbol_range, .. }| {
-                if let RangeInEditor::Inlay(range) = symbol_range {
-                    if range == &inlay_hover.range {
-                        // Hover triggered from same location as last time. Don't show again.
-                        return true;
-                    }
+                if let RangeInEditor::Inlay(range) = symbol_range
+                    && range == &inlay_hover.range
+                {
+                    // Hover triggered from same location as last time. Don't show again.
+                    return true;
                 }
                 false
             })
@@ -167,17 +167,16 @@ pub fn hover_at_inlay(
 
                 let language_registry = project.read_with(cx, |p, _| p.languages().clone())?;
                 let blocks = vec![inlay_hover.tooltip];
-                let parsed_content = parse_blocks(&blocks, &language_registry, None, cx).await;
+                let parsed_content =
+                    parse_blocks(&blocks, Some(&language_registry), None, cx).await;
 
                 let scroll_handle = ScrollHandle::new();
 
                 let subscription = this
                     .update(cx, |_, cx| {
-                        if let Some(parsed_content) = &parsed_content {
-                            Some(cx.observe(parsed_content, |_, _, cx| cx.notify()))
-                        } else {
-                            None
-                        }
+                        parsed_content.as_ref().map(|parsed_content| {
+                            cx.observe(parsed_content, |_, _, cx| cx.notify())
+                        })
                     })
                     .ok()
                     .flatten();
@@ -251,7 +250,9 @@ fn show_hover(
 
     let (excerpt_id, _, _) = editor.buffer().read(cx).excerpt_containing(anchor, cx)?;
 
-    let language_registry = editor.project.as_ref()?.read(cx).languages().clone();
+    let language_registry = editor
+        .project()
+        .map(|project| project.read(cx).languages().clone());
     let provider = editor.semantics_provider.clone()?;
 
     if !ignore_timeout {
@@ -267,13 +268,12 @@ fn show_hover(
     }
 
     // Don't request again if the location is the same as the previous request
-    if let Some(triggered_from) = &editor.hover_state.triggered_from {
-        if triggered_from
+    if let Some(triggered_from) = &editor.hover_state.triggered_from
+        && triggered_from
             .cmp(&anchor, &snapshot.buffer_snapshot)
             .is_eq()
-        {
-            return None;
-        }
+    {
+        return None;
     }
 
     let hover_popover_delay = EditorSettings::get_global(cx).hover_popover_delay;
@@ -428,7 +428,7 @@ fn show_hover(
             };
 
             let hovers_response = if let Some(hover_request) = hover_request {
-                hover_request.await
+                hover_request.await.unwrap_or_default()
             } else {
                 Vec::new()
             };
@@ -443,15 +443,14 @@ fn show_hover(
                     text: format!("Unicode character U+{:02X}", invisible as u32),
                     kind: HoverBlockKind::PlainText,
                 }];
-                let parsed_content = parse_blocks(&blocks, &language_registry, None, cx).await;
+                let parsed_content =
+                    parse_blocks(&blocks, language_registry.as_ref(), None, cx).await;
                 let scroll_handle = ScrollHandle::new();
                 let subscription = this
                     .update(cx, |_, cx| {
-                        if let Some(parsed_content) = &parsed_content {
-                            Some(cx.observe(parsed_content, |_, _, cx| cx.notify()))
-                        } else {
-                            None
-                        }
+                        parsed_content.as_ref().map(|parsed_content| {
+                            cx.observe(parsed_content, |_, _, cx| cx.notify())
+                        })
                     })
                     .ok()
                     .flatten();
@@ -493,16 +492,15 @@ fn show_hover(
 
                 let blocks = hover_result.contents;
                 let language = hover_result.language;
-                let parsed_content = parse_blocks(&blocks, &language_registry, language, cx).await;
+                let parsed_content =
+                    parse_blocks(&blocks, language_registry.as_ref(), language, cx).await;
                 let scroll_handle = ScrollHandle::new();
                 hover_highlights.push(range.clone());
                 let subscription = this
                     .update(cx, |_, cx| {
-                        if let Some(parsed_content) = &parsed_content {
-                            Some(cx.observe(parsed_content, |_, _, cx| cx.notify()))
-                        } else {
-                            None
-                        }
+                        parsed_content.as_ref().map(|parsed_content| {
+                            cx.observe(parsed_content, |_, _, cx| cx.notify())
+                        })
                     })
                     .ok()
                     .flatten();
@@ -583,7 +581,7 @@ fn same_diagnostic_hover(editor: &Editor, snapshot: &EditorSnapshot, anchor: Anc
 
 async fn parse_blocks(
     blocks: &[HoverBlock],
-    language_registry: &Arc<LanguageRegistry>,
+    language_registry: Option<&Arc<LanguageRegistry>>,
     language: Option<Arc<Language>>,
     cx: &mut AsyncWindowContext,
 ) -> Option<Entity<Markdown>> {
@@ -599,18 +597,15 @@ async fn parse_blocks(
         })
         .join("\n\n");
 
-    let rendered_block = cx
-        .new_window_entity(|_window, cx| {
-            Markdown::new(
-                combined_text.into(),
-                Some(language_registry.clone()),
-                language.map(|language| language.name()),
-                cx,
-            )
-        })
-        .ok();
-
-    rendered_block
+    cx.new_window_entity(|_window, cx| {
+        Markdown::new(
+            combined_text.into(),
+            language_registry.cloned(),
+            language.map(|language| language.name()),
+            cx,
+        )
+    })
+    .ok()
 }
 
 pub fn hover_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
@@ -622,7 +617,7 @@ pub fn hover_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
 
     let mut base_text_style = window.text_style();
     base_text_style.refine(&TextStyleRefinement {
-        font_family: Some(ui_font_family.clone()),
+        font_family: Some(ui_font_family),
         font_fallbacks: ui_font_fallbacks,
         color: Some(cx.theme().colors().editor_foreground),
         ..Default::default()
@@ -671,7 +666,7 @@ pub fn diagnostics_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
 
     let mut base_text_style = window.text_style();
     base_text_style.refine(&TextStyleRefinement {
-        font_family: Some(ui_font_family.clone()),
+        font_family: Some(ui_font_family),
         font_fallbacks: ui_font_fallbacks,
         color: Some(cx.theme().colors().editor_foreground),
         ..Default::default()
@@ -712,59 +707,54 @@ pub fn diagnostics_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
 }
 
 pub fn open_markdown_url(link: SharedString, window: &mut Window, cx: &mut App) {
-    if let Ok(uri) = Url::parse(&link) {
-        if uri.scheme() == "file" {
-            if let Some(workspace) = window.root::<Workspace>().flatten() {
-                workspace.update(cx, |workspace, cx| {
-                    let task = workspace.open_abs_path(
-                        PathBuf::from(uri.path()),
-                        OpenOptions {
-                            visible: Some(OpenVisible::None),
-                            ..Default::default()
-                        },
-                        window,
-                        cx,
-                    );
+    if let Ok(uri) = Url::parse(&link)
+        && uri.scheme() == "file"
+        && let Some(workspace) = window.root::<Workspace>().flatten()
+    {
+        workspace.update(cx, |workspace, cx| {
+            let task = workspace.open_abs_path(
+                PathBuf::from(uri.path()),
+                OpenOptions {
+                    visible: Some(OpenVisible::None),
+                    ..Default::default()
+                },
+                window,
+                cx,
+            );
 
-                    cx.spawn_in(window, async move |_, cx| {
-                        let item = task.await?;
-                        // Ruby LSP uses URLs with #L1,1-4,4
-                        // we'll just take the first number and assume it's a line number
-                        let Some(fragment) = uri.fragment() else {
-                            return anyhow::Ok(());
-                        };
-                        let mut accum = 0u32;
-                        for c in fragment.chars() {
-                            if c >= '0' && c <= '9' && accum < u32::MAX / 2 {
-                                accum *= 10;
-                                accum += c as u32 - '0' as u32;
-                            } else if accum > 0 {
-                                break;
-                            }
-                        }
-                        if accum == 0 {
-                            return Ok(());
-                        }
-                        let Some(editor) = cx.update(|_, cx| item.act_as::<Editor>(cx))? else {
-                            return Ok(());
-                        };
-                        editor.update_in(cx, |editor, window, cx| {
-                            editor.change_selections(
-                                Default::default(),
-                                window,
-                                cx,
-                                |selections| {
-                                    selections.select_ranges([text::Point::new(accum - 1, 0)
-                                        ..text::Point::new(accum - 1, 0)]);
-                                },
-                            );
-                        })
-                    })
-                    .detach_and_log_err(cx);
-                });
-                return;
-            }
-        }
+            cx.spawn_in(window, async move |_, cx| {
+                let item = task.await?;
+                // Ruby LSP uses URLs with #L1,1-4,4
+                // we'll just take the first number and assume it's a line number
+                let Some(fragment) = uri.fragment() else {
+                    return anyhow::Ok(());
+                };
+                let mut accum = 0u32;
+                for c in fragment.chars() {
+                    if c >= '0' && c <= '9' && accum < u32::MAX / 2 {
+                        accum *= 10;
+                        accum += c as u32 - '0' as u32;
+                    } else if accum > 0 {
+                        break;
+                    }
+                }
+                if accum == 0 {
+                    return Ok(());
+                }
+                let Some(editor) = cx.update(|_, cx| item.act_as::<Editor>(cx))? else {
+                    return Ok(());
+                };
+                editor.update_in(cx, |editor, window, cx| {
+                    editor.change_selections(Default::default(), window, cx, |selections| {
+                        selections.select_ranges([
+                            text::Point::new(accum - 1, 0)..text::Point::new(accum - 1, 0)
+                        ]);
+                    });
+                })
+            })
+            .detach_and_log_err(cx);
+        });
+        return;
     }
     cx.open_url(&link);
 }
@@ -834,20 +824,19 @@ impl HoverState {
     pub fn focused(&self, window: &mut Window, cx: &mut Context<Editor>) -> bool {
         let mut hover_popover_is_focused = false;
         for info_popover in &self.info_popovers {
-            if let Some(markdown_view) = &info_popover.parsed_content {
-                if markdown_view.focus_handle(cx).is_focused(window) {
-                    hover_popover_is_focused = true;
-                }
+            if let Some(markdown_view) = &info_popover.parsed_content
+                && markdown_view.focus_handle(cx).is_focused(window)
+            {
+                hover_popover_is_focused = true;
             }
         }
-        if let Some(diagnostic_popover) = &self.diagnostic_popover {
-            if diagnostic_popover
+        if let Some(diagnostic_popover) = &self.diagnostic_popover
+            && diagnostic_popover
                 .markdown
                 .focus_handle(cx)
                 .is_focused(window)
-            {
-                hover_popover_is_focused = true;
-            }
+        {
+            hover_popover_is_focused = true;
         }
         hover_popover_is_focused
     }

crates/editor/src/indent_guides.rs 🔗

@@ -164,15 +164,15 @@ pub fn indent_guides_in_range(
     let end_anchor = snapshot.buffer_snapshot.anchor_after(end_offset);
 
     let mut fold_ranges = Vec::<Range<Point>>::new();
-    let mut folds = snapshot.folds_in_range(start_offset..end_offset).peekable();
-    while let Some(fold) = folds.next() {
+    let folds = snapshot.folds_in_range(start_offset..end_offset).peekable();
+    for fold in folds {
         let start = fold.range.start.to_point(&snapshot.buffer_snapshot);
         let end = fold.range.end.to_point(&snapshot.buffer_snapshot);
-        if let Some(last_range) = fold_ranges.last_mut() {
-            if last_range.end >= start {
-                last_range.end = last_range.end.max(end);
-                continue;
-            }
+        if let Some(last_range) = fold_ranges.last_mut()
+            && last_range.end >= start
+        {
+            last_range.end = last_range.end.max(end);
+            continue;
         }
         fold_ranges.push(start..end);
     }

crates/editor/src/inlay_hint_cache.rs 🔗

@@ -475,10 +475,7 @@ impl InlayHintCache {
             let excerpt_cached_hints = excerpt_cached_hints.read();
             let mut excerpt_cache = excerpt_cached_hints.ordered_hints.iter().fuse().peekable();
             shown_excerpt_hints_to_remove.retain(|(shown_anchor, shown_hint_id)| {
-                let Some(buffer) = shown_anchor
-                    .buffer_id
-                    .and_then(|buffer_id| multi_buffer.buffer(buffer_id))
-                else {
+                let Some(buffer) = multi_buffer.buffer_for_anchor(*shown_anchor, cx) else {
                     return false;
                 };
                 let buffer_snapshot = buffer.read(cx).snapshot();
@@ -498,16 +495,14 @@ impl InlayHintCache {
                                 cmp::Ordering::Less | cmp::Ordering::Equal => {
                                     if !old_kinds.contains(&cached_hint.kind)
                                         && new_kinds.contains(&cached_hint.kind)
-                                    {
-                                        if let Some(anchor) = multi_buffer_snapshot
+                                        && 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,
-                                            ));
-                                        }
+                                    {
+                                        to_insert.push(Inlay::hint(
+                                            cached_hint_id.id(),
+                                            anchor,
+                                            cached_hint,
+                                        ));
                                     }
                                     excerpt_cache.next();
                                 }
@@ -522,16 +517,16 @@ impl InlayHintCache {
             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) {
-                    if let Some(anchor) = multi_buffer_snapshot
+                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,
-                        ));
-                    }
+                {
+                    to_insert.push(Inlay::hint(
+                        cached_hint_id.id(),
+                        anchor,
+                        maybe_missed_cached_hint,
+                    ));
                 }
             }
         }
@@ -620,44 +615,44 @@ impl InlayHintCache {
     ) {
         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) {
-                if let ResolveState::CanResolve(server_id, _) = &cached_hint.resolve_state {
-                    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,
-                                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)
+            if let Some(cached_hint) = guard.hints_by_id.get_mut(&id)
+                && let ResolveState::CanResolve(server_id, _) = &cached_hint.resolve_state
+            {
+                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,
+                            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
                                 {
-                                    let mut guard = excerpt_hints.write();
-                                    if let Some(cached_hint) = guard.hints_by_id.get_mut(&id) {
-                                        if cached_hint.resolve_state == ResolveState::Resolving {
-                                            resolved_hint.resolve_state = ResolveState::Resolved;
-                                            *cached_hint = resolved_hint;
-                                        }
-                                    }
+                                    resolved_hint.resolve_state = ResolveState::Resolved;
+                                    *cached_hint = resolved_hint;
                                 }
-                            })?;
-                        }
+                            }
+                        })?;
+                    }
 
-                        anyhow::Ok(())
-                    })
-                    .detach_and_log_err(cx);
-                }
+                    anyhow::Ok(())
+                })
+                .detach_and_log_err(cx);
             }
         }
     }
@@ -990,8 +985,8 @@ fn fetch_and_update_hints(
 
                 let buffer = editor.buffer().read(cx).buffer(query.buffer_id)?;
 
-                if !editor.registered_buffers.contains_key(&query.buffer_id) {
-                    if let Some(project) = editor.project.as_ref() {
+                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,
@@ -999,7 +994,6 @@ fn fetch_and_update_hints(
                             );
                         })
                     }
-                }
 
                 editor
                     .semantics_provider
@@ -1240,14 +1234,12 @@ fn apply_hint_update(
             .inlay_hint_cache
             .allowed_hint_kinds
             .contains(&new_hint.kind)
-        {
-            if let Some(new_hint_position) =
+            && 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));
-            }
+        {
+            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);

crates/editor/src/items.rs 🔗

@@ -1,7 +1,7 @@
 use crate::{
     Anchor, Autoscroll, Editor, EditorEvent, EditorSettings, ExcerptId, ExcerptRange, FormatTarget,
-    MultiBuffer, MultiBufferSnapshot, NavigationData, SearchWithinRange, SelectionEffects,
-    ToPoint as _,
+    MultiBuffer, MultiBufferSnapshot, NavigationData, ReportEditorEvent, SearchWithinRange,
+    SelectionEffects, ToPoint as _,
     display_map::HighlightKey,
     editor_settings::SeedQuerySetting,
     persistence::{DB, SerializedEditor},
@@ -103,9 +103,9 @@ impl FollowableItem for Editor {
                         multibuffer = MultiBuffer::new(project.read(cx).capability());
                         let mut sorted_excerpts = state.excerpts.clone();
                         sorted_excerpts.sort_by_key(|e| e.id);
-                        let mut sorted_excerpts = sorted_excerpts.into_iter().peekable();
+                        let sorted_excerpts = sorted_excerpts.into_iter().peekable();
 
-                        while let Some(excerpt) = sorted_excerpts.next() {
+                        for excerpt in sorted_excerpts {
                             let Ok(buffer_id) = BufferId::new(excerpt.buffer_id) else {
                                 continue;
                             };
@@ -201,7 +201,7 @@ impl FollowableItem for Editor {
         if buffer
             .as_singleton()
             .and_then(|buffer| buffer.read(cx).file())
-            .map_or(false, |file| file.is_private())
+            .is_some_and(|file| file.is_private())
         {
             return None;
         }
@@ -293,7 +293,7 @@ impl FollowableItem for Editor {
                 EditorEvent::ExcerptsRemoved { ids, .. } => {
                     update
                         .deleted_excerpts
-                        .extend(ids.iter().map(ExcerptId::to_proto));
+                        .extend(ids.iter().copied().map(ExcerptId::to_proto));
                     true
                 }
                 EditorEvent::ScrollPositionChanged { autoscroll, .. } if !autoscroll => {
@@ -524,8 +524,8 @@ fn serialize_selection(
 ) -> proto::Selection {
     proto::Selection {
         id: selection.id as u64,
-        start: Some(serialize_anchor(&selection.start, &buffer)),
-        end: Some(serialize_anchor(&selection.end, &buffer)),
+        start: Some(serialize_anchor(&selection.start, buffer)),
+        end: Some(serialize_anchor(&selection.end, buffer)),
         reversed: selection.reversed,
     }
 }
@@ -654,6 +654,10 @@ impl Item for Editor {
         }
     }
 
+    fn suggested_filename(&self, cx: &App) -> SharedString {
+        self.buffer.read(cx).title(cx).to_string().into()
+    }
+
     fn tab_icon(&self, _: &Window, cx: &App) -> Option<Icon> {
         ItemSettings::get_global(cx)
             .file_icons
@@ -674,7 +678,7 @@ impl Item for Editor {
                     let buffer = buffer.read(cx);
                     let path = buffer.project_path(cx)?;
                     let buffer_id = buffer.remote_id();
-                    let project = self.project.as_ref()?.read(cx);
+                    let project = self.project()?.read(cx);
                     let entry = project.entry_for_path(&path, cx)?;
                     let (repo, repo_path) = project
                         .git_store()
@@ -711,7 +715,7 @@ impl Item for Editor {
             .read(cx)
             .as_singleton()
             .and_then(|buffer| buffer.read(cx).file())
-            .map_or(false, |file| file.disk_state() == DiskState::Deleted);
+            .is_some_and(|file| file.disk_state() == DiskState::Deleted);
 
         h_flex()
             .gap_2()
@@ -776,6 +780,10 @@ impl Item for Editor {
         }
     }
 
+    fn on_removed(&self, cx: &App) {
+        self.report_editor_event(ReportEditorEvent::Closed, None, cx);
+    }
+
     fn deactivated(&mut self, _: &mut Window, cx: &mut Context<Self>) {
         let selection = self.selections.newest_anchor();
         self.push_to_nav_history(selection.head(), None, true, false, cx);
@@ -815,9 +823,9 @@ impl Item for Editor {
     ) -> Task<Result<()>> {
         // Add meta data tracking # of auto saves
         if options.autosave {
-            self.report_editor_event("Editor Autosaved", None, cx);
+            self.report_editor_event(ReportEditorEvent::Saved { auto_saved: true }, None, cx);
         } else {
-            self.report_editor_event("Editor Saved", None, cx);
+            self.report_editor_event(ReportEditorEvent::Saved { auto_saved: false }, None, cx);
         }
 
         let buffers = self.buffer().clone().read(cx).all_buffers();
@@ -896,7 +904,11 @@ impl Item for Editor {
             .path
             .extension()
             .map(|a| a.to_string_lossy().to_string());
-        self.report_editor_event("Editor Saved", file_extension, cx);
+        self.report_editor_event(
+            ReportEditorEvent::Saved { auto_saved: false },
+            file_extension,
+            cx,
+        );
 
         project.update(cx, |project, cx| project.save_buffer_as(buffer, path, cx))
     }
@@ -918,10 +930,10 @@ impl Item for Editor {
             })?;
             buffer
                 .update(cx, |buffer, cx| {
-                    if let Some(transaction) = transaction {
-                        if !buffer.is_singleton() {
-                            buffer.push_transaction(&transaction.0, cx);
-                        }
+                    if let Some(transaction) = transaction
+                        && !buffer.is_singleton()
+                    {
+                        buffer.push_transaction(&transaction.0, cx);
                     }
                 })
                 .ok();
@@ -997,8 +1009,8 @@ impl Item for Editor {
     ) {
         self.workspace = Some((workspace.weak_handle(), workspace.database_id()));
         if let Some(workspace) = &workspace.weak_handle().upgrade() {
-            cx.subscribe(&workspace, |editor, _, event: &workspace::Event, _cx| {
-                if matches!(event, workspace::Event::ModalOpened) {
+            cx.subscribe(workspace, |editor, _, event: &workspace::Event, _cx| {
+                if let workspace::Event::ModalOpened = event {
                     editor.mouse_context_menu.take();
                     editor.inline_blame_popover.take();
                 }
@@ -1024,6 +1036,10 @@ impl Item for Editor {
                 f(ItemEvent::UpdateBreadcrumbs);
             }
 
+            EditorEvent::BreadcrumbsChanged => {
+                f(ItemEvent::UpdateBreadcrumbs);
+            }
+
             EditorEvent::DirtyChanged => {
                 f(ItemEvent::UpdateTab);
             }
@@ -1276,7 +1292,7 @@ impl SerializableItem for Editor {
             project
                 .read(cx)
                 .worktree_for_id(worktree_id, cx)
-                .and_then(|worktree| worktree.read(cx).absolutize(&file.path()).ok())
+                .and_then(|worktree| worktree.read(cx).absolutize(file.path()).ok())
                 .or_else(|| {
                     let full_path = file.full_path(cx);
                     let project_path = project.read(cx).find_project_path(&full_path, cx)?;
@@ -1354,36 +1370,33 @@ impl ProjectItem for Editor {
         let mut editor = Self::for_buffer(buffer.clone(), Some(project), window, cx);
         if let Some((excerpt_id, buffer_id, snapshot)) =
             editor.buffer().read(cx).snapshot(cx).as_singleton()
+            && WorkspaceSettings::get(None, cx).restore_on_file_reopen
+            && let Some(restoration_data) = Self::project_item_kind()
+                .and_then(|kind| pane.as_ref()?.project_item_restoration_data.get(&kind))
+                .and_then(|data| data.downcast_ref::<EditorRestorationData>())
+                .and_then(|data| {
+                    let file = project::File::from_dyn(buffer.read(cx).file())?;
+                    data.entries.get(&file.abs_path(cx))
+                })
         {
-            if WorkspaceSettings::get(None, cx).restore_on_file_reopen {
-                if let Some(restoration_data) = Self::project_item_kind()
-                    .and_then(|kind| pane.as_ref()?.project_item_restoration_data.get(&kind))
-                    .and_then(|data| data.downcast_ref::<EditorRestorationData>())
-                    .and_then(|data| {
-                        let file = project::File::from_dyn(buffer.read(cx).file())?;
-                        data.entries.get(&file.abs_path(cx))
-                    })
-                {
-                    editor.fold_ranges(
-                        clip_ranges(&restoration_data.folds, &snapshot),
-                        false,
-                        window,
-                        cx,
-                    );
-                    if !restoration_data.selections.is_empty() {
-                        editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
-                            s.select_ranges(clip_ranges(&restoration_data.selections, &snapshot));
-                        });
-                    }
-                    let (top_row, offset) = restoration_data.scroll_position;
-                    let anchor = Anchor::in_buffer(
-                        *excerpt_id,
-                        buffer_id,
-                        snapshot.anchor_before(Point::new(top_row, 0)),
-                    );
-                    editor.set_scroll_anchor(ScrollAnchor { anchor, offset }, window, cx);
-                }
+            editor.fold_ranges(
+                clip_ranges(&restoration_data.folds, snapshot),
+                false,
+                window,
+                cx,
+            );
+            if !restoration_data.selections.is_empty() {
+                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
+                    s.select_ranges(clip_ranges(&restoration_data.selections, snapshot));
+                });
             }
+            let (top_row, offset) = restoration_data.scroll_position;
+            let anchor = Anchor::in_buffer(
+                *excerpt_id,
+                buffer_id,
+                snapshot.anchor_before(Point::new(top_row, 0)),
+            );
+            editor.set_scroll_anchor(ScrollAnchor { anchor, offset }, window, cx);
         }
 
         editor
@@ -1825,7 +1838,7 @@ pub fn entry_diagnostic_aware_icon_name_and_color(
     diagnostic_severity: Option<DiagnosticSeverity>,
 ) -> Option<(IconName, Color)> {
     match diagnostic_severity {
-        Some(DiagnosticSeverity::ERROR) => Some((IconName::X, Color::Error)),
+        Some(DiagnosticSeverity::ERROR) => Some((IconName::Close, Color::Error)),
         Some(DiagnosticSeverity::WARNING) => Some((IconName::Triangle, Color::Warning)),
         _ => None,
     }

crates/editor/src/jsx_tag_auto_close.rs 🔗

@@ -37,7 +37,7 @@ pub(crate) fn should_auto_close(
         let text = buffer
             .text_for_range(edited_range.clone())
             .collect::<String>();
-        let edited_range = edited_range.to_offset(&buffer);
+        let edited_range = edited_range.to_offset(buffer);
         if !text.ends_with(">") {
             continue;
         }
@@ -51,12 +51,11 @@ pub(crate) fn should_auto_close(
             continue;
         };
         let mut jsx_open_tag_node = node;
-        if node.grammar_name() != config.open_tag_node_name {
-            if let Some(parent) = node.parent() {
-                if parent.grammar_name() == config.open_tag_node_name {
-                    jsx_open_tag_node = parent;
-                }
-            }
+        if node.grammar_name() != config.open_tag_node_name
+            && let Some(parent) = node.parent()
+            && parent.grammar_name() == config.open_tag_node_name
+        {
+            jsx_open_tag_node = parent;
         }
         if jsx_open_tag_node.grammar_name() != config.open_tag_node_name {
             continue;
@@ -87,9 +86,9 @@ pub(crate) fn should_auto_close(
         });
     }
     if to_auto_edit.is_empty() {
-        return None;
+        None
     } else {
-        return Some(to_auto_edit);
+        Some(to_auto_edit)
     }
 }
 
@@ -182,12 +181,12 @@ pub(crate) fn generate_auto_close_edits(
          */
         {
             let tag_node_name_equals = |node: &Node, name: &str| {
-                let is_empty = name.len() == 0;
+                let is_empty = name.is_empty();
                 if let Some(node_name) = node.named_child(TS_NODE_TAG_NAME_CHILD_INDEX) {
                     let range = node_name.byte_range();
                     return buffer.text_for_range(range).equals_str(name);
                 }
-                return is_empty;
+                is_empty
             };
 
             let tree_root_node = {
@@ -208,7 +207,7 @@ pub(crate) fn generate_auto_close_edits(
                     cur = descendant;
                 }
 
-                assert!(ancestors.len() > 0);
+                assert!(!ancestors.is_empty());
 
                 let mut tree_root_node = open_tag;
 
@@ -228,7 +227,7 @@ pub(crate) fn generate_auto_close_edits(
                             let has_open_tag_with_same_tag_name = ancestor
                                 .named_child(0)
                                 .filter(|n| n.kind() == config.open_tag_node_name)
-                                .map_or(false, |element_open_tag_node| {
+                                .is_some_and(|element_open_tag_node| {
                                     tag_node_name_equals(&element_open_tag_node, &tag_name)
                                 });
                             if has_open_tag_with_same_tag_name {
@@ -264,8 +263,7 @@ pub(crate) fn generate_auto_close_edits(
             }
 
             let is_after_open_tag = |node: &Node| {
-                return node.start_byte() < open_tag.start_byte()
-                    && node.end_byte() < open_tag.start_byte();
+                node.start_byte() < open_tag.start_byte() && node.end_byte() < open_tag.start_byte()
             };
 
             // perf: use cursor for more efficient traversal
@@ -284,10 +282,8 @@ pub(crate) fn generate_auto_close_edits(
                         unclosed_open_tag_count -= 1;
                     }
                 } else if has_erroneous_close_tag && kind == erroneous_close_tag_node_name {
-                    if tag_node_name_equals(&node, &tag_name) {
-                        if !is_after_open_tag(&node) {
-                            unclosed_open_tag_count -= 1;
-                        }
+                    if tag_node_name_equals(&node, &tag_name) && !is_after_open_tag(&node) {
+                        unclosed_open_tag_count -= 1;
                     }
                 } else if kind == config.jsx_element_node_name {
                     // perf: filter only open,close,element,erroneous nodes
@@ -304,7 +300,7 @@ pub(crate) fn generate_auto_close_edits(
         let edit_range = edit_anchor..edit_anchor;
         edits.push((edit_range, format!("</{}>", tag_name)));
     }
-    return Ok(edits);
+    Ok(edits)
 }
 
 pub(crate) fn refresh_enabled_in_any_buffer(
@@ -370,7 +366,7 @@ pub(crate) fn construct_initial_buffer_versions_map<
             initial_buffer_versions.insert(buffer_id, buffer_version);
         }
     }
-    return initial_buffer_versions;
+    initial_buffer_versions
 }
 
 pub(crate) fn handle_from(
@@ -458,12 +454,9 @@ pub(crate) fn handle_from(
             let ensure_no_edits_since_start = || -> Option<()> {
                 let has_edits_since_start = this
                     .read_with(cx, |this, cx| {
-                        this.buffer
-                            .read(cx)
-                            .buffer(buffer_id)
-                            .map_or(true, |buffer| {
-                                buffer.read(cx).has_edits_since(&buffer_version_initial)
-                            })
+                        this.buffer.read(cx).buffer(buffer_id).is_none_or(|buffer| {
+                            buffer.read(cx).has_edits_since(&buffer_version_initial)
+                        })
                     })
                     .ok()?;
 
@@ -514,7 +507,7 @@ pub(crate) fn handle_from(
 
             {
                 let selections = this
-                    .read_with(cx, |this, _| this.selections.disjoint_anchors().clone())
+                    .read_with(cx, |this, _| this.selections.disjoint_anchors())
                     .ok()?;
                 for selection in selections.iter() {
                     let Some(selection_buffer_offset_head) =
@@ -815,10 +808,7 @@ mod jsx_tag_autoclose_tests {
             );
             buf
         });
-        let buffer_c = cx.new(|cx| {
-            let buf = language::Buffer::local("<span", cx);
-            buf
-        });
+        let buffer_c = cx.new(|cx| language::Buffer::local("<span", cx));
         let buffer = cx.new(|cx| {
             let mut buf = MultiBuffer::new(language::Capability::ReadWrite);
             buf.push_excerpts(

crates/editor/src/linked_editing_ranges.rs 🔗

@@ -51,7 +51,7 @@ pub(super) fn refresh_linked_ranges(
     if editor.pending_rename.is_some() {
         return None;
     }
-    let project = editor.project.as_ref()?.downgrade();
+    let project = editor.project()?.downgrade();
 
     editor.linked_editing_range_task = Some(cx.spawn_in(window, async move |editor, cx| {
         cx.background_executor().timer(UPDATE_DEBOUNCE).await;
@@ -72,7 +72,7 @@ pub(super) fn refresh_linked_ranges(
                         // Throw away selections spanning multiple buffers.
                         continue;
                     }
-                    if let Some(buffer) = end_position.buffer_id.and_then(|id| buffer.buffer(id)) {
+                    if let Some(buffer) = buffer.buffer_for_anchor(end_position, cx) {
                         applicable_selections.push((
                             buffer,
                             start_position.text_anchor,

crates/editor/src/lsp_colors.rs 🔗

@@ -207,7 +207,7 @@ impl Editor {
                             .entry(buffer_snapshot.remote_id())
                             .or_insert_with(Vec::new);
                         let excerpt_point_range =
-                            excerpt_range.context.to_point_utf16(&buffer_snapshot);
+                            excerpt_range.context.to_point_utf16(buffer_snapshot);
                         excerpt_data.push((
                             excerpt_id,
                             buffer_snapshot.clone(),

crates/editor/src/lsp_ext.rs 🔗

@@ -76,7 +76,7 @@ async fn lsp_task_context(
 
     let project_env = project
         .update(cx, |project, cx| {
-            project.buffer_environment(&buffer, &worktree_store, cx)
+            project.buffer_environment(buffer, &worktree_store, cx)
         })
         .ok()?
         .await;
@@ -147,16 +147,15 @@ pub fn lsp_tasks(
                             },
                             cx,
                         )
-                    }) {
-                        if let Some(new_runnables) = runnables_task.await.log_err() {
-                            new_lsp_tasks.extend(new_runnables.runnables.into_iter().filter_map(
-                                |(location, runnable)| {
-                                    let resolved_task =
-                                        runnable.resolve_task(&id_base, &lsp_buffer_context)?;
-                                    Some((location, resolved_task))
-                                },
-                            ));
-                        }
+                    }) && let Some(new_runnables) = runnables_task.await.log_err()
+                    {
+                        new_lsp_tasks.extend(new_runnables.runnables.into_iter().filter_map(
+                            |(location, runnable)| {
+                                let resolved_task =
+                                    runnable.resolve_task(&id_base, &lsp_buffer_context)?;
+                                Some((location, resolved_task))
+                            },
+                        ));
                     }
                     lsp_tasks
                         .entry(source_kind)

crates/editor/src/mouse_context_menu.rs 🔗

@@ -61,13 +61,13 @@ impl MouseContextMenu {
             source,
             offset: position - (source_position + content_origin),
         };
-        return Some(MouseContextMenu::new(
+        Some(MouseContextMenu::new(
             editor,
             menu_position,
             context_menu,
             window,
             cx,
-        ));
+        ))
     }
 
     pub(crate) fn new(
@@ -102,11 +102,11 @@ impl MouseContextMenu {
                 let display_snapshot = &editor
                     .display_map
                     .update(cx, |display_map, cx| display_map.snapshot(cx));
-                let selection_init_range = selection_init.display_range(&display_snapshot);
+                let selection_init_range = selection_init.display_range(display_snapshot);
                 let selection_now_range = editor
                     .selections
                     .newest_anchor()
-                    .display_range(&display_snapshot);
+                    .display_range(display_snapshot);
                 if selection_now_range == selection_init_range {
                     return;
                 }
@@ -190,14 +190,16 @@ pub fn deploy_context_menu(
             .all::<PointUtf16>(cx)
             .into_iter()
             .any(|s| !s.is_empty());
-        let has_git_repo = anchor.buffer_id.is_some_and(|buffer_id| {
-            project
-                .read(cx)
-                .git_store()
-                .read(cx)
-                .repository_and_path_for_buffer_id(buffer_id, cx)
-                .is_some()
-        });
+        let has_git_repo = buffer
+            .buffer_id_for_anchor(anchor)
+            .is_some_and(|buffer_id| {
+                project
+                    .read(cx)
+                    .git_store()
+                    .read(cx)
+                    .repository_and_path_for_buffer_id(buffer_id, cx)
+                    .is_some()
+            });
 
         let evaluate_selection = window.is_action_available(&EvaluateSelectedText, cx);
         let run_to_cursor = window.is_action_available(&RunToCursor, cx);

crates/editor/src/movement.rs 🔗

@@ -230,7 +230,7 @@ pub fn indented_line_beginning(
     if stop_at_soft_boundaries && soft_line_start > indent_start && display_point != soft_line_start
     {
         soft_line_start
-    } else if stop_at_indent && display_point != indent_start {
+    } else if stop_at_indent && (display_point > indent_start || display_point == line_start) {
         indent_start
     } else {
         line_start
@@ -439,17 +439,17 @@ pub fn start_of_excerpt(
     };
     match direction {
         Direction::Prev => {
-            let mut start = excerpt.start_anchor().to_display_point(&map);
+            let mut start = excerpt.start_anchor().to_display_point(map);
             if start >= display_point && start.row() > DisplayRow(0) {
                 let Some(excerpt) = map.buffer_snapshot.excerpt_before(excerpt.id()) else {
                     return display_point;
                 };
-                start = excerpt.start_anchor().to_display_point(&map);
+                start = excerpt.start_anchor().to_display_point(map);
             }
             start
         }
         Direction::Next => {
-            let mut end = excerpt.end_anchor().to_display_point(&map);
+            let mut end = excerpt.end_anchor().to_display_point(map);
             *end.row_mut() += 1;
             map.clip_point(end, Bias::Right)
         }
@@ -467,7 +467,7 @@ pub fn end_of_excerpt(
     };
     match direction {
         Direction::Prev => {
-            let mut start = excerpt.start_anchor().to_display_point(&map);
+            let mut start = excerpt.start_anchor().to_display_point(map);
             if start.row() > DisplayRow(0) {
                 *start.row_mut() -= 1;
             }
@@ -476,7 +476,7 @@ pub fn end_of_excerpt(
             start
         }
         Direction::Next => {
-            let mut end = excerpt.end_anchor().to_display_point(&map);
+            let mut end = excerpt.end_anchor().to_display_point(map);
             *end.column_mut() = 0;
             if end <= display_point {
                 *end.row_mut() += 1;
@@ -485,7 +485,7 @@ pub fn end_of_excerpt(
                 else {
                     return display_point;
                 };
-                end = excerpt.end_anchor().to_display_point(&map);
+                end = excerpt.end_anchor().to_display_point(map);
                 *end.column_mut() = 0;
             }
             end
@@ -510,10 +510,10 @@ pub fn find_preceding_boundary_point(
         if find_range == FindRange::SingleLine && ch == '\n' {
             break;
         }
-        if let Some(prev_ch) = prev_ch {
-            if is_boundary(ch, prev_ch) {
-                break;
-            }
+        if let Some(prev_ch) = prev_ch
+            && is_boundary(ch, prev_ch)
+        {
+            break;
         }
 
         offset -= ch.len_utf8();
@@ -562,13 +562,13 @@ pub fn find_boundary_point(
         if find_range == FindRange::SingleLine && ch == '\n' {
             break;
         }
-        if let Some(prev_ch) = prev_ch {
-            if is_boundary(prev_ch, ch) {
-                if return_point_before_boundary {
-                    return map.clip_point(prev_offset.to_display_point(map), Bias::Right);
-                } else {
-                    break;
-                }
+        if let Some(prev_ch) = prev_ch
+            && is_boundary(prev_ch, ch)
+        {
+            if return_point_before_boundary {
+                return map.clip_point(prev_offset.to_display_point(map), Bias::Right);
+            } else {
+                break;
             }
         }
         prev_offset = offset;
@@ -603,13 +603,13 @@ pub fn find_preceding_boundary_trail(
     // Find the boundary
     let start_offset = offset;
     for ch in forward {
-        if let Some(prev_ch) = prev_ch {
-            if is_boundary(prev_ch, ch) {
-                if start_offset == offset {
-                    trail_offset = Some(offset);
-                } else {
-                    break;
-                }
+        if let Some(prev_ch) = prev_ch
+            && is_boundary(prev_ch, ch)
+        {
+            if start_offset == offset {
+                trail_offset = Some(offset);
+            } else {
+                break;
             }
         }
         offset -= ch.len_utf8();
@@ -651,13 +651,13 @@ pub fn find_boundary_trail(
     // Find the boundary
     let start_offset = offset;
     for ch in forward {
-        if let Some(prev_ch) = prev_ch {
-            if is_boundary(prev_ch, ch) {
-                if start_offset == offset {
-                    trail_offset = Some(offset);
-                } else {
-                    break;
-                }
+        if let Some(prev_ch) = prev_ch
+            && is_boundary(prev_ch, ch)
+        {
+            if start_offset == offset {
+                trail_offset = Some(offset);
+            } else {
+                break;
             }
         }
         offset += ch.len_utf8();

crates/editor/src/proposed_changes_editor.rs 🔗

@@ -241,24 +241,13 @@ impl ProposedChangesEditor {
         event: &BufferEvent,
         _cx: &mut Context<Self>,
     ) {
-        match event {
-            BufferEvent::Operation { .. } => {
-                self.recalculate_diffs_tx
-                    .unbounded_send(RecalculateDiff {
-                        buffer,
-                        debounce: true,
-                    })
-                    .ok();
-            }
-            // BufferEvent::DiffBaseChanged => {
-            //     self.recalculate_diffs_tx
-            //         .unbounded_send(RecalculateDiff {
-            //             buffer,
-            //             debounce: false,
-            //         })
-            //         .ok();
-            // }
-            _ => (),
+        if let BufferEvent::Operation { .. } = event {
+            self.recalculate_diffs_tx
+                .unbounded_send(RecalculateDiff {
+                    buffer,
+                    debounce: true,
+                })
+                .ok();
         }
     }
 }
@@ -442,7 +431,7 @@ impl SemanticsProvider for BranchBufferSemanticsProvider {
         buffer: &Entity<Buffer>,
         position: text::Anchor,
         cx: &mut App,
-    ) -> Option<Task<Vec<project::Hover>>> {
+    ) -> Option<Task<Option<Vec<project::Hover>>>> {
         let buffer = self.to_base(buffer, &[position], cx)?;
         self.0.hover(&buffer, position, cx)
     }
@@ -478,7 +467,7 @@ impl SemanticsProvider for BranchBufferSemanticsProvider {
     }
 
     fn supports_inlay_hints(&self, buffer: &Entity<Buffer>, cx: &mut App) -> bool {
-        if let Some(buffer) = self.to_base(&buffer, &[], cx) {
+        if let Some(buffer) = self.to_base(buffer, &[], cx) {
             self.0.supports_inlay_hints(&buffer, cx)
         } else {
             false
@@ -491,7 +480,7 @@ impl SemanticsProvider for BranchBufferSemanticsProvider {
         position: text::Anchor,
         cx: &mut App,
     ) -> Option<Task<anyhow::Result<Vec<project::DocumentHighlight>>>> {
-        let buffer = self.to_base(&buffer, &[position], cx)?;
+        let buffer = self.to_base(buffer, &[position], cx)?;
         self.0.document_highlights(&buffer, position, cx)
     }
 
@@ -501,8 +490,8 @@ impl SemanticsProvider for BranchBufferSemanticsProvider {
         position: text::Anchor,
         kind: crate::GotoDefinitionKind,
         cx: &mut App,
-    ) -> Option<Task<anyhow::Result<Vec<project::LocationLink>>>> {
-        let buffer = self.to_base(&buffer, &[position], cx)?;
+    ) -> Option<Task<anyhow::Result<Option<Vec<project::LocationLink>>>>> {
+        let buffer = self.to_base(buffer, &[position], cx)?;
         self.0.definitions(&buffer, position, kind, cx)
     }
 

crates/editor/src/rust_analyzer_ext.rs 🔗

@@ -35,12 +35,12 @@ pub fn apply_related_actions(editor: &Entity<Editor>, window: &mut Window, cx: &
         .filter_map(|buffer| buffer.read(cx).language())
         .any(|language| is_rust_language(language))
     {
-        register_action(&editor, window, go_to_parent_module);
-        register_action(&editor, window, expand_macro_recursively);
-        register_action(&editor, window, open_docs);
-        register_action(&editor, window, cancel_flycheck_action);
-        register_action(&editor, window, run_flycheck_action);
-        register_action(&editor, window, clear_flycheck_action);
+        register_action(editor, window, go_to_parent_module);
+        register_action(editor, window, expand_macro_recursively);
+        register_action(editor, window, open_docs);
+        register_action(editor, window, cancel_flycheck_action);
+        register_action(editor, window, run_flycheck_action);
+        register_action(editor, window, clear_flycheck_action);
     }
 }
 
@@ -285,11 +285,11 @@ pub fn open_docs(editor: &mut Editor, _: &OpenDocs, window: &mut Window, cx: &mu
         workspace.update(cx, |_workspace, cx| {
             // Check if the local document exists, otherwise fallback to the online document.
             // Open with the default browser.
-            if let Some(local_url) = docs_urls.local {
-                if fs::metadata(Path::new(&local_url[8..])).is_ok() {
-                    cx.open_url(&local_url);
-                    return;
-                }
+            if let Some(local_url) = docs_urls.local
+                && fs::metadata(Path::new(&local_url[8..])).is_ok()
+            {
+                cx.open_url(&local_url);
+                return;
             }
 
             if let Some(web_url) = docs_urls.web {

crates/editor/src/scroll.rs 🔗

@@ -675,7 +675,7 @@ impl Editor {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        if matches!(self.mode, EditorMode::SingleLine { .. }) {
+        if matches!(self.mode, EditorMode::SingleLine) {
             cx.propagate();
             return;
         }
@@ -703,20 +703,20 @@ impl Editor {
         if matches!(
             settings.defaults.soft_wrap,
             SoftWrap::PreferredLineLength | SoftWrap::Bounded
-        ) {
-            if (settings.defaults.preferred_line_length as f32) < visible_column_count {
-                visible_column_count = settings.defaults.preferred_line_length as f32;
-            }
+        ) && (settings.defaults.preferred_line_length as f32) < visible_column_count
+        {
+            visible_column_count = settings.defaults.preferred_line_length as f32;
         }
 
         // If the scroll position is currently at the left edge of the document
         // (x == 0.0) and the intent is to scroll right, the gutter's margin
         // should first be added to the current position, otherwise the cursor
         // will end at the column position minus the margin, which looks off.
-        if current_position.x == 0.0 && amount.columns(visible_column_count) > 0. {
-            if let Some(last_position_map) = &self.last_position_map {
-                current_position.x += self.gutter_dimensions.margin / last_position_map.em_advance;
-            }
+        if current_position.x == 0.0
+            && amount.columns(visible_column_count) > 0.
+            && let Some(last_position_map) = &self.last_position_map
+        {
+            current_position.x += self.gutter_dimensions.margin / last_position_map.em_advance;
         }
         let new_position = current_position
             + point(
@@ -749,12 +749,10 @@ impl Editor {
 
         if let (Some(visible_lines), Some(visible_columns)) =
             (self.visible_line_count(), self.visible_column_count())
+            && newest_head.row() <= DisplayRow(screen_top.row().0 + visible_lines as u32)
+            && newest_head.column() <= screen_top.column() + visible_columns as u32
         {
-            if newest_head.row() <= DisplayRow(screen_top.row().0 + visible_lines as u32)
-                && newest_head.column() <= screen_top.column() + visible_columns as u32
-            {
-                return Ordering::Equal;
-            }
+            return Ordering::Equal;
         }
 
         Ordering::Greater

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

@@ -16,7 +16,7 @@ impl Editor {
             return;
         }
 
-        if matches!(self.mode, EditorMode::SingleLine { .. }) {
+        if matches!(self.mode, EditorMode::SingleLine) {
             cx.propagate();
             return;
         }

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

@@ -116,12 +116,12 @@ impl Editor {
         let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
         let mut scroll_position = self.scroll_manager.scroll_position(&display_map);
         let original_y = scroll_position.y;
-        if let Some(last_bounds) = self.expect_bounds_change.take() {
-            if scroll_position.y != 0. {
-                scroll_position.y += (bounds.top() - last_bounds.top()) / line_height;
-                if scroll_position.y < 0. {
-                    scroll_position.y = 0.;
-                }
+        if let Some(last_bounds) = self.expect_bounds_change.take()
+            && scroll_position.y != 0.
+        {
+            scroll_position.y += (bounds.top() - last_bounds.top()) / line_height;
+            if scroll_position.y < 0. {
+                scroll_position.y = 0.;
             }
         }
         if scroll_position.y > max_scroll_top {

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

@@ -67,10 +67,7 @@ impl ScrollAmount {
     }
 
     pub fn is_full_page(&self) -> bool {
-        match self {
-            ScrollAmount::Page(count) if count.abs() == 1.0 => true,
-            _ => false,
-        }
+        matches!(self, ScrollAmount::Page(count) if count.abs() == 1.0)
     }
 
     pub fn direction(&self) -> ScrollDirection {

crates/editor/src/selections_collection.rs 🔗

@@ -119,8 +119,8 @@ impl SelectionsCollection {
         cx: &mut App,
     ) -> Option<Selection<D>> {
         let map = self.display_map(cx);
-        let selection = resolve_selections(self.pending_anchor().as_ref(), &map).next();
-        selection
+
+        resolve_selections(self.pending_anchor().as_ref(), &map).next()
     }
 
     pub(crate) fn pending_mode(&self) -> Option<SelectMode> {
@@ -276,18 +276,18 @@ impl SelectionsCollection {
         cx: &mut App,
     ) -> Selection<D> {
         let map = self.display_map(cx);
-        let selection = resolve_selections([self.newest_anchor()], &map)
+
+        resolve_selections([self.newest_anchor()], &map)
             .next()
-            .unwrap();
-        selection
+            .unwrap()
     }
 
     pub fn newest_display(&self, cx: &mut App) -> Selection<DisplayPoint> {
         let map = self.display_map(cx);
-        let selection = resolve_selections_display([self.newest_anchor()], &map)
+
+        resolve_selections_display([self.newest_anchor()], &map)
             .next()
-            .unwrap();
-        selection
+            .unwrap()
     }
 
     pub fn oldest_anchor(&self) -> &Selection<Anchor> {
@@ -303,10 +303,10 @@ impl SelectionsCollection {
         cx: &mut App,
     ) -> Selection<D> {
         let map = self.display_map(cx);
-        let selection = resolve_selections([self.oldest_anchor()], &map)
+
+        resolve_selections([self.oldest_anchor()], &map)
             .next()
-            .unwrap();
-        selection
+            .unwrap()
     }
 
     pub fn first_anchor(&self) -> Selection<Anchor> {

crates/editor/src/signature_help.rs 🔗

@@ -169,7 +169,7 @@ impl Editor {
         else {
             return;
         };
-        let Some(lsp_store) = self.project.as_ref().map(|p| p.read(cx).lsp_store()) else {
+        let Some(lsp_store) = self.project().map(|p| p.read(cx).lsp_store()) else {
             return;
         };
         let task = lsp_store.update(cx, |lsp_store, cx| {
@@ -182,7 +182,9 @@ impl Editor {
                 let signature_help = task.await;
                 editor
                     .update(cx, |editor, cx| {
-                        let Some(mut signature_help) = signature_help.into_iter().next() else {
+                        let Some(mut signature_help) =
+                            signature_help.unwrap_or_default().into_iter().next()
+                        else {
                             editor
                                 .signature_help_state
                                 .hide(SignatureHelpHiddenBy::AutoClose);
@@ -196,7 +198,7 @@ impl Editor {
                                     .highlight_text(&text, 0..signature.label.len())
                                     .into_iter()
                                     .flat_map(|(range, highlight_id)| {
-                                        Some((range, highlight_id.style(&cx.theme().syntax())?))
+                                        Some((range, highlight_id.style(cx.theme().syntax())?))
                                     });
                                 signature.highlights =
                                     combine_highlights(signature.highlights.clone(), highlights)

crates/editor/src/tasks.rs 🔗

@@ -89,7 +89,7 @@ impl Editor {
                     .lsp_task_source()?;
                 if lsp_settings
                     .get(&lsp_tasks_source)
-                    .map_or(true, |s| s.enable_lsp_tasks)
+                    .is_none_or(|s| s.enable_lsp_tasks)
                 {
                     let buffer_id = buffer.read(cx).remote_id();
                     Some((lsp_tasks_source, buffer_id))

crates/editor/src/test.rs 🔗

@@ -53,7 +53,7 @@ pub fn marked_display_snapshot(
     let (unmarked_text, markers) = marked_text_offsets(text);
 
     let font = Font {
-        family: "Zed Plex Mono".into(),
+        family: ".ZedMono".into(),
         features: FontFeatures::default(),
         fallbacks: None,
         weight: FontWeight::default(),
@@ -184,12 +184,12 @@ pub fn editor_content_with_blocks(editor: &Entity<Editor>, cx: &mut VisualTestCo
     for (row, block) in blocks {
         match block {
             Block::Custom(custom_block) => {
-                if let BlockPlacement::Near(x) = &custom_block.placement {
-                    if snapshot.intersects_fold(x.to_point(&snapshot.buffer_snapshot)) {
-                        continue;
-                    }
+                if let BlockPlacement::Near(x) = &custom_block.placement
+                    && snapshot.intersects_fold(x.to_point(&snapshot.buffer_snapshot))
+                {
+                    continue;
                 };
-                let content = block_content_for_tests(&editor, custom_block.id, cx)
+                let content = block_content_for_tests(editor, custom_block.id, cx)
                     .expect("block content not found");
                 // 2: "related info 1 for diagnostic 0"
                 if let Some(height) = custom_block.height {
@@ -230,26 +230,23 @@ pub fn editor_content_with_blocks(editor: &Entity<Editor>, cx: &mut VisualTestCo
                     lines[row as usize].push_str("§ -----");
                 }
             }
-            Block::ExcerptBoundary {
-                excerpt,
-                height,
-                starts_new_buffer,
-            } => {
-                if starts_new_buffer {
-                    lines[row.0 as usize].push_str(&cx.update(|_, cx| {
-                        format!(
-                            "§ {}",
-                            excerpt
-                                .buffer
-                                .file()
-                                .unwrap()
-                                .file_name(cx)
-                                .to_string_lossy()
-                        )
-                    }));
-                } else {
-                    lines[row.0 as usize].push_str("§ -----")
+            Block::ExcerptBoundary { height, .. } => {
+                for row in row.0..row.0 + height {
+                    lines[row as usize].push_str("§ -----");
                 }
+            }
+            Block::BufferHeader { excerpt, height } => {
+                lines[row.0 as usize].push_str(&cx.update(|_, cx| {
+                    format!(
+                        "§ {}",
+                        excerpt
+                            .buffer
+                            .file()
+                            .unwrap()
+                            .file_name(cx)
+                            .to_string_lossy()
+                    )
+                }));
                 for row in row.0 + 1..row.0 + height {
                     lines[row as usize].push_str("§ -----");
                 }

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

@@ -300,6 +300,7 @@ impl EditorLspTestContext {
         self.to_lsp_range(ranges[0].clone())
     }
 
+    #[expect(clippy::wrong_self_convention, reason = "This is test code")]
     pub fn to_lsp_range(&mut self, range: Range<usize>) -> lsp::Range {
         let snapshot = self.update_editor(|editor, window, cx| editor.snapshot(window, cx));
         let start_point = range.start.to_point(&snapshot.buffer_snapshot);
@@ -326,6 +327,7 @@ impl EditorLspTestContext {
         })
     }
 
+    #[expect(clippy::wrong_self_convention, reason = "This is test code")]
     pub fn to_lsp(&mut self, offset: usize) -> lsp::Position {
         let snapshot = self.update_editor(|editor, window, cx| editor.snapshot(window, cx));
         let point = offset.to_point(&snapshot.buffer_snapshot);

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

@@ -119,13 +119,7 @@ impl EditorTestContext {
             for excerpt in excerpts.into_iter() {
                 let (text, ranges) = marked_text_ranges(excerpt, false);
                 let buffer = cx.new(|cx| Buffer::local(text, cx));
-                multibuffer.push_excerpts(
-                    buffer,
-                    ranges
-                        .into_iter()
-                        .map(|range| ExcerptRange::new(range.clone())),
-                    cx,
-                );
+                multibuffer.push_excerpts(buffer, ranges.into_iter().map(ExcerptRange::new), cx);
             }
             multibuffer
         });
@@ -297,9 +291,8 @@ impl EditorTestContext {
 
     pub fn set_head_text(&mut self, diff_base: &str) {
         self.cx.run_until_parked();
-        let fs = self.update_editor(|editor, _, cx| {
-            editor.project.as_ref().unwrap().read(cx).fs().as_fake()
-        });
+        let fs =
+            self.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).fs().as_fake());
         let path = self.update_buffer(|buffer, _| buffer.file().unwrap().path().clone());
         fs.set_head_for_repo(
             &Self::root_path().join(".git"),
@@ -311,18 +304,16 @@ impl EditorTestContext {
 
     pub fn clear_index_text(&mut self) {
         self.cx.run_until_parked();
-        let fs = self.update_editor(|editor, _, cx| {
-            editor.project.as_ref().unwrap().read(cx).fs().as_fake()
-        });
+        let fs =
+            self.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).fs().as_fake());
         fs.set_index_for_repo(&Self::root_path().join(".git"), &[]);
         self.cx.run_until_parked();
     }
 
     pub fn set_index_text(&mut self, diff_base: &str) {
         self.cx.run_until_parked();
-        let fs = self.update_editor(|editor, _, cx| {
-            editor.project.as_ref().unwrap().read(cx).fs().as_fake()
-        });
+        let fs =
+            self.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).fs().as_fake());
         let path = self.update_buffer(|buffer, _| buffer.file().unwrap().path().clone());
         fs.set_index_for_repo(
             &Self::root_path().join(".git"),
@@ -333,9 +324,8 @@ impl EditorTestContext {
 
     #[track_caller]
     pub fn assert_index_text(&mut self, expected: Option<&str>) {
-        let fs = self.update_editor(|editor, _, cx| {
-            editor.project.as_ref().unwrap().read(cx).fs().as_fake()
-        });
+        let fs =
+            self.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).fs().as_fake());
         let path = self.update_buffer(|buffer, _| buffer.file().unwrap().path().clone());
         let mut found = None;
         fs.with_git_state(&Self::root_path().join(".git"), false, |git_state| {
@@ -430,7 +420,7 @@ impl EditorTestContext {
             if expected_text == "[FOLDED]\n" {
                 assert!(is_folded, "excerpt {} should be folded", ix);
                 let is_selected = selections.iter().any(|s| s.head().excerpt_id == excerpt_id);
-                if expected_selections.len() > 0 {
+                if !expected_selections.is_empty() {
                     assert!(
                         is_selected,
                         "excerpt {ix} should be selected. got {:?}",

crates/eval/build.rs 🔗

@@ -0,0 +1,14 @@
+fn main() {
+    let cargo_toml =
+        std::fs::read_to_string("../zed/Cargo.toml").expect("Failed to read crates/zed/Cargo.toml");
+    let version = cargo_toml
+        .lines()
+        .find(|line| line.starts_with("version = "))
+        .expect("Version not found in crates/zed/Cargo.toml")
+        .split('=')
+        .nth(1)
+        .expect("Invalid version format")
+        .trim()
+        .trim_matches('"');
+    println!("cargo:rustc-env=ZED_PKG_VERSION={}", version);
+}

crates/eval/src/assertions.rs 🔗

@@ -54,7 +54,7 @@ impl AssertionsReport {
     pub fn passed_count(&self) -> usize {
         self.ran
             .iter()
-            .filter(|a| a.result.as_ref().map_or(false, |result| result.passed))
+            .filter(|a| a.result.as_ref().is_ok_and(|result| result.passed))
             .count()
     }
 

crates/eval/src/eval.rs 🔗

@@ -103,7 +103,7 @@ fn main() {
     let languages: HashSet<String> = args.languages.into_iter().collect();
 
     let http_client = Arc::new(ReqwestClient::new());
-    let app = Application::headless().with_http_client(http_client.clone());
+    let app = Application::headless().with_http_client(http_client);
     let all_threads = examples::all(&examples_dir);
 
     app.run(move |cx| {
@@ -112,7 +112,7 @@ fn main() {
         let telemetry = app_state.client.telemetry();
         telemetry.start(system_id, installation_id, session_id, cx);
 
-        let enable_telemetry = env::var("ZED_EVAL_TELEMETRY").map_or(false, |value| value == "1")
+        let enable_telemetry = env::var("ZED_EVAL_TELEMETRY").is_ok_and(|value| value == "1")
             && telemetry.has_checksum_seed();
         if enable_telemetry {
             println!("Telemetry enabled");
@@ -167,15 +167,14 @@ fn main() {
                     continue;
                 }
 
-                if let Some(language) = meta.language_server {
-                    if !languages.contains(&language.file_extension) {
+                if let Some(language) = meta.language_server
+                    && !languages.contains(&language.file_extension) {
                         panic!(
                             "Eval for {:?} could not be run because no language server was found for extension {:?}",
                             meta.name,
                             language.file_extension
                         );
                     }
-                }
 
                 // TODO: This creates a worktree per repetition. Ideally these examples should
                 // either be run sequentially on the same worktree, or reuse worktrees when there
@@ -337,7 +336,7 @@ pub struct AgentAppState {
 }
 
 pub fn init(cx: &mut App) -> Arc<AgentAppState> {
-    let app_version = AppVersion::global(cx);
+    let app_version = AppVersion::load(env!("ZED_PKG_VERSION"));
     release_channel::init(app_version, cx);
     gpui_tokio::init(cx);
 
@@ -350,7 +349,7 @@ pub fn init(cx: &mut App) -> Arc<AgentAppState> {
 
     // Set User-Agent so we can download language servers from GitHub
     let user_agent = format!(
-        "Zed/{} ({}; {})",
+        "Zed Agent Eval/{} ({}; {})",
         app_version,
         std::env::consts::OS,
         std::env::consts::ARCH
@@ -417,11 +416,7 @@ pub fn init(cx: &mut App) -> Arc<AgentAppState> {
 
     language::init(cx);
     debug_adapter_extension::init(extension_host_proxy.clone(), cx);
-    language_extension::init(
-        LspAccess::Noop,
-        extension_host_proxy.clone(),
-        languages.clone(),
-    );
+    language_extension::init(LspAccess::Noop, extension_host_proxy, languages.clone());
     language_model::init(client.clone(), cx);
     language_models::init(user_store.clone(), client.clone(), cx);
     languages::init(languages.clone(), node_runtime.clone(), cx);
@@ -520,7 +515,7 @@ async fn judge_example(
     enable_telemetry: bool,
     cx: &AsyncApp,
 ) -> JudgeOutput {
-    let judge_output = example.judge(model.clone(), &run_output, cx).await;
+    let judge_output = example.judge(model.clone(), run_output, cx).await;
 
     if enable_telemetry {
         telemetry::event!(
@@ -531,7 +526,7 @@ async fn judge_example(
             example_name = example.name.clone(),
             example_repetition = example.repetition,
             diff_evaluation = judge_output.diff.clone(),
-            thread_evaluation = judge_output.thread.clone(),
+            thread_evaluation = judge_output.thread,
             tool_metrics = run_output.tool_metrics,
             response_count = run_output.response_count,
             token_usage = run_output.token_usage,
@@ -711,7 +706,7 @@ fn print_report(
             println!("Average thread score: {average_thread_score}%");
         }
 
-        println!("");
+        println!();
 
         print_h2("CUMULATIVE TOOL METRICS");
         println!("{}", cumulative_tool_metrics);

crates/eval/src/example.rs 🔗

@@ -64,7 +64,7 @@ impl ExampleMetadata {
         self.url
             .split('/')
             .next_back()
-            .unwrap_or(&"")
+            .unwrap_or("")
             .trim_end_matches(".git")
             .into()
     }
@@ -255,7 +255,7 @@ impl ExampleContext {
                     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) {
+                            if let Some(tool_result) = thread.tool_result(tool_use_id) {
                                 let message = if tool_result.is_error {
                                     format!("✖︎ {}", tool_use.name)
                                 } else {
@@ -335,7 +335,7 @@ impl ExampleContext {
             for message in thread.messages().skip(message_count_before) {
                 messages.push(Message {
                     _role: message.role,
-                    text: message.to_string(),
+                    text: message.to_message_content(),
                     tool_use: thread
                         .tool_uses_for_message(message.id, cx)
                         .into_iter()

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

@@ -70,10 +70,10 @@ impl Example for AddArgToTraitMethod {
             let path_str = format!("crates/assistant_tools/src/{}.rs", tool_name);
             let edits = edits.get(Path::new(&path_str));
 
-            let ignored = edits.map_or(false, |edits| {
+            let ignored = edits.is_some_and(|edits| {
                 edits.has_added_line("        _window: Option<gpui::AnyWindowHandle>,\n")
             });
-            let uningored = edits.map_or(false, |edits| {
+            let uningored = edits.is_some_and(|edits| {
                 edits.has_added_line("        window: Option<gpui::AnyWindowHandle>,\n")
             });
 
@@ -89,7 +89,7 @@ impl Example for AddArgToTraitMethod {
         let batch_tool_edits = edits.get(Path::new("crates/assistant_tools/src/batch_tool.rs"));
 
         cx.assert(
-            batch_tool_edits.map_or(false, |edits| {
+            batch_tool_edits.is_some_and(|edits| {
                 edits.has_added_line("        window: Option<gpui::AnyWindowHandle>,\n")
             }),
             "Argument:   batch_tool",

crates/eval/src/explorer.rs 🔗

@@ -46,27 +46,25 @@ fn find_target_files_recursive(
                 max_depth,
                 found_files,
             )?;
-        } else if path.is_file() {
-            if let Some(filename_osstr) = path.file_name() {
-                if let Some(filename_str) = filename_osstr.to_str() {
-                    if filename_str == target_filename {
-                        found_files.push(path);
-                    }
-                }
-            }
+        } else if path.is_file()
+            && let Some(filename_osstr) = path.file_name()
+            && let Some(filename_str) = filename_osstr.to_str()
+            && filename_str == target_filename
+        {
+            found_files.push(path);
         }
     }
     Ok(())
 }
 
 pub fn generate_explorer_html(input_paths: &[PathBuf], output_path: &PathBuf) -> Result<String> {
-    if let Some(parent) = output_path.parent() {
-        if !parent.exists() {
-            fs::create_dir_all(parent).context(format!(
-                "Failed to create output directory: {}",
-                parent.display()
-            ))?;
-        }
+    if let Some(parent) = output_path.parent()
+        && !parent.exists()
+    {
+        fs::create_dir_all(parent).context(format!(
+            "Failed to create output directory: {}",
+            parent.display()
+        ))?;
     }
 
     let template_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src/explorer.html");

crates/eval/src/instance.rs 🔗

@@ -90,11 +90,8 @@ impl ExampleInstance {
         worktrees_dir: &Path,
         repetition: usize,
     ) -> Self {
-        let name = thread.meta().name.to_string();
-        let run_directory = run_dir
-            .join(&name)
-            .join(repetition.to_string())
-            .to_path_buf();
+        let name = thread.meta().name;
+        let run_directory = run_dir.join(&name).join(repetition.to_string());
 
         let repo_path = repo_path_for_url(repos_dir, &thread.meta().url);
 
@@ -376,11 +373,10 @@ impl ExampleInstance {
             );
             let result = this.thread.conversation(&mut example_cx).await;
 
-            if let Err(err) = result {
-                if !err.is::<FailedAssertion>() {
+            if let Err(err) = result
+                && !err.is::<FailedAssertion>() {
                     return Err(err);
                 }
-            }
 
             println!("{}Stopped", this.log_prefix);
 
@@ -459,8 +455,8 @@ impl ExampleInstance {
         let mut output_file =
             File::create(self.run_directory.join("judge.md")).expect("failed to create judge.md");
 
-        let diff_task = self.judge_diff(model.clone(), &run_output, cx);
-        let thread_task = self.judge_thread(model.clone(), &run_output, cx);
+        let diff_task = self.judge_diff(model.clone(), run_output, cx);
+        let thread_task = self.judge_thread(model.clone(), run_output, cx);
 
         let (diff_result, thread_result) = futures::join!(diff_task, thread_task);
 
@@ -661,7 +657,7 @@ pub fn wait_for_lang_server(
         .update(cx, |buffer, cx| {
             lsp_store.update(cx, |lsp_store, cx| {
                 lsp_store
-                    .language_servers_for_local_buffer(&buffer, cx)
+                    .language_servers_for_local_buffer(buffer, cx)
                     .next()
                     .is_some()
             })
@@ -679,8 +675,8 @@ pub fn wait_for_lang_server(
         [
             cx.subscribe(&lsp_store, {
                 let log_prefix = log_prefix.clone();
-                move |_, event, _| match event {
-                    project::LspStoreEvent::LanguageServerUpdate {
+                move |_, event, _| {
+                    if let project::LspStoreEvent::LanguageServerUpdate {
                         message:
                             client::proto::update_language_server::Variant::WorkProgress(
                                 LspWorkProgress {
@@ -689,11 +685,13 @@ pub fn wait_for_lang_server(
                                 },
                             ),
                         ..
-                    } => println!("{}⟲ {message}", log_prefix),
-                    _ => {}
+                    } = event
+                    {
+                        println!("{}⟲ {message}", log_prefix)
+                    }
                 }
             }),
-            cx.subscribe(&project, {
+            cx.subscribe(project, {
                 let buffer = buffer.clone();
                 move |project, event, cx| match event {
                     project::Event::LanguageServerAdded(_, _, _) => {
@@ -771,7 +769,7 @@ pub async fn query_lsp_diagnostics(
 }
 
 fn parse_assertion_result(response: &str) -> Result<RanAssertionResult> {
-    let analysis = get_tag("analysis", response)?.to_string();
+    let analysis = get_tag("analysis", response)?;
     let passed = match get_tag("passed", response)?.to_lowercase().as_str() {
         "true" => true,
         "false" => false,
@@ -838,7 +836,7 @@ fn messages_to_markdown<'a>(message_iter: impl IntoIterator<Item = &'a Message>)
         for segment in &message.segments {
             match segment {
                 MessageSegment::Text(text) => {
-                    messages.push_str(&text);
+                    messages.push_str(text);
                     messages.push_str("\n\n");
                 }
                 MessageSegment::Thinking { text, signature } => {
@@ -846,7 +844,7 @@ fn messages_to_markdown<'a>(message_iter: impl IntoIterator<Item = &'a Message>)
                     if let Some(sig) = signature {
                         messages.push_str(&format!("Signature: {}\n\n", sig));
                     }
-                    messages.push_str(&text);
+                    messages.push_str(text);
                     messages.push_str("\n");
                 }
                 MessageSegment::RedactedThinking(items) => {
@@ -878,7 +876,7 @@ pub async fn send_language_model_request(
     request: LanguageModelRequest,
     cx: &AsyncApp,
 ) -> anyhow::Result<String> {
-    match model.stream_completion_text(request, &cx).await {
+    match model.stream_completion_text(request, cx).await {
         Ok(mut stream) => {
             let mut full_response = String::new();
             while let Some(chunk_result) = stream.stream.next().await {
@@ -915,9 +913,9 @@ impl RequestMarkdown {
             for tool in &request.tools {
                 write!(&mut tools, "# {}\n\n", tool.name).unwrap();
                 write!(&mut tools, "{}\n\n", tool.description).unwrap();
-                write!(
+                writeln!(
                     &mut tools,
-                    "{}\n",
+                    "{}",
                     MarkdownCodeBlock {
                         tag: "json",
                         text: &format!("{:#}", tool.input_schema)
@@ -1191,7 +1189,7 @@ mod test {
             output.analysis,
             Some("The model did a good job but there were still compilations errors.".into())
         );
-        assert_eq!(output.passed, true);
+        assert!(output.passed);
 
         let response = r#"
             Text around ignored
@@ -1211,6 +1209,6 @@ mod test {
             output.analysis,
             Some("Failed to compile:\n- Error 1\n- Error 2".into())
         );
-        assert_eq!(output.passed, false);
+        assert!(!output.passed);
     }
 }

crates/extension/src/extension.rs 🔗

@@ -178,16 +178,15 @@ pub fn parse_wasm_extension_version(
     for part in wasmparser::Parser::new(0).parse_all(wasm_bytes) {
         if let wasmparser::Payload::CustomSection(s) =
             part.context("error parsing wasm extension")?
+            && s.name() == "zed:api-version"
         {
-            if s.name() == "zed:api-version" {
-                version = parse_wasm_extension_version_custom_section(s.data());
-                if version.is_none() {
-                    bail!(
-                        "extension {} has invalid zed:api-version section: {:?}",
-                        extension_id,
-                        s.data()
-                    );
-                }
+            version = parse_wasm_extension_version_custom_section(s.data());
+            if version.is_none() {
+                bail!(
+                    "extension {} has invalid zed:api-version section: {:?}",
+                    extension_id,
+                    s.data()
+                );
             }
         }
     }

crates/extension/src/extension_builder.rs 🔗

@@ -401,7 +401,7 @@ impl ExtensionBuilder {
         let mut clang_path = wasi_sdk_dir.clone();
         clang_path.extend(["bin", &format!("clang{}", env::consts::EXE_SUFFIX)]);
 
-        if fs::metadata(&clang_path).map_or(false, |metadata| metadata.is_file()) {
+        if fs::metadata(&clang_path).is_ok_and(|metadata| metadata.is_file()) {
             return Ok(clang_path);
         }
 
@@ -452,7 +452,7 @@ impl ExtensionBuilder {
         let mut output = Vec::new();
         let mut stack = Vec::new();
 
-        for payload in Parser::new(0).parse_all(&input) {
+        for payload in Parser::new(0).parse_all(input) {
             let payload = payload?;
 
             // Track nesting depth, so that we don't mess with inner producer sections:
@@ -484,14 +484,10 @@ impl ExtensionBuilder {
                 _ => {}
             }
 
-            match &payload {
-                CustomSection(c) => {
-                    if strip_custom_section(c.name()) {
-                        continue;
-                    }
-                }
-
-                _ => {}
+            if let CustomSection(c) = &payload
+                && strip_custom_section(c.name())
+            {
+                continue;
             }
             if let Some((id, range)) = payload.as_section() {
                 RawSection {

crates/extension/src/extension_events.rs 🔗

@@ -19,9 +19,8 @@ pub struct ExtensionEvents;
 impl ExtensionEvents {
     /// Returns the global [`ExtensionEvents`].
     pub fn try_global(cx: &App) -> Option<Entity<Self>> {
-        return cx
-            .try_global::<GlobalExtensionEvents>()
-            .map(|g| g.0.clone());
+        cx.try_global::<GlobalExtensionEvents>()
+            .map(|g| g.0.clone())
     }
 
     fn new(_cx: &mut Context<Self>) -> Self {

crates/extension/src/extension_host_proxy.rs 🔗

@@ -28,7 +28,6 @@ pub struct ExtensionHostProxy {
     snippet_proxy: RwLock<Option<Arc<dyn ExtensionSnippetProxy>>>,
     slash_command_proxy: RwLock<Option<Arc<dyn ExtensionSlashCommandProxy>>>,
     context_server_proxy: RwLock<Option<Arc<dyn ExtensionContextServerProxy>>>,
-    indexed_docs_provider_proxy: RwLock<Option<Arc<dyn ExtensionIndexedDocsProviderProxy>>>,
     debug_adapter_provider_proxy: RwLock<Option<Arc<dyn ExtensionDebugAdapterProviderProxy>>>,
 }
 
@@ -54,7 +53,6 @@ impl ExtensionHostProxy {
             snippet_proxy: RwLock::default(),
             slash_command_proxy: RwLock::default(),
             context_server_proxy: RwLock::default(),
-            indexed_docs_provider_proxy: RwLock::default(),
             debug_adapter_provider_proxy: RwLock::default(),
         }
     }
@@ -87,14 +85,6 @@ impl ExtensionHostProxy {
         self.context_server_proxy.write().replace(Arc::new(proxy));
     }
 
-    pub fn register_indexed_docs_provider_proxy(
-        &self,
-        proxy: impl ExtensionIndexedDocsProviderProxy,
-    ) {
-        self.indexed_docs_provider_proxy
-            .write()
-            .replace(Arc::new(proxy));
-    }
     pub fn register_debug_adapter_proxy(&self, proxy: impl ExtensionDebugAdapterProviderProxy) {
         self.debug_adapter_provider_proxy
             .write()
@@ -408,30 +398,6 @@ impl ExtensionContextServerProxy for ExtensionHostProxy {
     }
 }
 
-pub trait ExtensionIndexedDocsProviderProxy: Send + Sync + 'static {
-    fn register_indexed_docs_provider(&self, extension: Arc<dyn Extension>, provider_id: Arc<str>);
-
-    fn unregister_indexed_docs_provider(&self, provider_id: Arc<str>);
-}
-
-impl ExtensionIndexedDocsProviderProxy for ExtensionHostProxy {
-    fn register_indexed_docs_provider(&self, extension: Arc<dyn Extension>, provider_id: Arc<str>) {
-        let Some(proxy) = self.indexed_docs_provider_proxy.read().clone() else {
-            return;
-        };
-
-        proxy.register_indexed_docs_provider(extension, provider_id)
-    }
-
-    fn unregister_indexed_docs_provider(&self, provider_id: Arc<str>) {
-        let Some(proxy) = self.indexed_docs_provider_proxy.read().clone() else {
-            return;
-        };
-
-        proxy.unregister_indexed_docs_provider(provider_id)
-    }
-}
-
 pub trait ExtensionDebugAdapterProviderProxy: Send + Sync + 'static {
     fn register_debug_adapter(
         &self,

crates/extension/src/extension_manifest.rs 🔗

@@ -84,8 +84,6 @@ pub struct ExtensionManifest {
     #[serde(default)]
     pub slash_commands: BTreeMap<Arc<str>, SlashCommandManifestEntry>,
     #[serde(default)]
-    pub indexed_docs_providers: BTreeMap<Arc<str>, IndexedDocsProviderEntry>,
-    #[serde(default)]
     pub snippets: Option<PathBuf>,
     #[serde(default)]
     pub capabilities: Vec<ExtensionCapability>,
@@ -195,9 +193,6 @@ pub struct SlashCommandManifestEntry {
     pub requires_argument: bool,
 }
 
-#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)]
-pub struct IndexedDocsProviderEntry {}
-
 #[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)]
 pub struct DebugAdapterManifestEntry {
     pub schema_path: Option<PathBuf>,
@@ -271,7 +266,6 @@ fn manifest_from_old_manifest(
         language_servers: Default::default(),
         context_servers: BTreeMap::default(),
         slash_commands: BTreeMap::default(),
-        indexed_docs_providers: BTreeMap::default(),
         snippets: None,
         capabilities: Vec::new(),
         debug_adapters: Default::default(),
@@ -304,7 +298,6 @@ mod tests {
             language_servers: BTreeMap::default(),
             context_servers: BTreeMap::default(),
             slash_commands: BTreeMap::default(),
-            indexed_docs_providers: BTreeMap::default(),
             snippets: None,
             capabilities: vec![],
             debug_adapters: Default::default(),

crates/extension_api/src/extension_api.rs 🔗

@@ -232,10 +232,10 @@ pub trait Extension: Send + Sync {
     ///
     /// To work through a real-world example, take a `cargo run` task and a hypothetical `cargo` locator:
     /// 1. We may need to modify the task; in this case, it is problematic that `cargo run` spawns a binary. We should turn `cargo run` into a debug scenario with
-    /// `cargo build` task. This is the decision we make at `dap_locator_create_scenario` scope.
+    ///    `cargo build` task. This is the decision we make at `dap_locator_create_scenario` scope.
     /// 2. Then, after the build task finishes, we will run `run_dap_locator` of the locator that produced the build task to find the program to be debugged. This function
-    /// should give us a debugger-agnostic configuration for launching a debug target (that we end up resolving with [`Extension::dap_config_to_scenario`]). It's almost as if the user
-    /// found the artifact path by themselves.
+    ///    should give us a debugger-agnostic configuration for launching a debug target (that we end up resolving with [`Extension::dap_config_to_scenario`]). It's almost as if the user
+    ///    found the artifact path by themselves.
     ///
     /// Note that you're not obliged to use build tasks with locators. Specifically, it is sufficient to provide a debug configuration directly in the return value of
     /// `dap_locator_create_scenario` if you're able to do that. Make sure to not fill out `build` field in that case, as that will prevent Zed from running second phase of resolution in such case.

crates/extension_cli/src/main.rs 🔗

@@ -144,10 +144,6 @@ fn extension_provides(manifest: &ExtensionManifest) -> BTreeSet<ExtensionProvide
         provides.insert(ExtensionProvides::ContextServers);
     }
 
-    if !manifest.indexed_docs_providers.is_empty() {
-        provides.insert(ExtensionProvides::IndexedDocsProviders);
-    }
-
     if manifest.snippets.is_some() {
         provides.insert(ExtensionProvides::Snippets);
     }

crates/extension_host/benches/extension_compilation_benchmark.rs 🔗

@@ -132,7 +132,6 @@ fn manifest() -> ExtensionManifest {
             .collect(),
         context_servers: BTreeMap::default(),
         slash_commands: BTreeMap::default(),
-        indexed_docs_providers: BTreeMap::default(),
         snippets: None,
         capabilities: vec![ExtensionCapability::ProcessExec(
             extension::ProcessExecCapability {

crates/extension_host/src/capability_granter.rs 🔗

@@ -108,7 +108,6 @@ mod tests {
             language_servers: BTreeMap::default(),
             context_servers: BTreeMap::default(),
             slash_commands: BTreeMap::default(),
-            indexed_docs_providers: BTreeMap::default(),
             snippets: None,
             capabilities: vec![],
             debug_adapters: Default::default(),
@@ -146,7 +145,7 @@ mod tests {
                 command: "*".to_string(),
                 args: vec!["**".to_string()],
             })],
-            manifest.clone(),
+            manifest,
         );
         assert!(granter.grant_exec("ls", &["-la"]).is_ok());
     }

crates/extension_host/src/extension_host.rs 🔗

@@ -16,9 +16,9 @@ pub use extension::ExtensionManifest;
 use extension::extension_builder::{CompileExtensionOptions, ExtensionBuilder};
 use extension::{
     ExtensionContextServerProxy, ExtensionDebugAdapterProviderProxy, ExtensionEvents,
-    ExtensionGrammarProxy, ExtensionHostProxy, ExtensionIndexedDocsProviderProxy,
-    ExtensionLanguageProxy, ExtensionLanguageServerProxy, ExtensionSlashCommandProxy,
-    ExtensionSnippetProxy, ExtensionThemeProxy,
+    ExtensionGrammarProxy, ExtensionHostProxy, ExtensionLanguageProxy,
+    ExtensionLanguageServerProxy, ExtensionSlashCommandProxy, ExtensionSnippetProxy,
+    ExtensionThemeProxy,
 };
 use fs::{Fs, RemoveOptions};
 use futures::future::join_all;
@@ -93,10 +93,9 @@ pub fn is_version_compatible(
         .wasm_api_version
         .as_ref()
         .and_then(|wasm_api_version| SemanticVersion::from_str(wasm_api_version).ok())
+        && !is_supported_wasm_api_version(release_channel, wasm_api_version)
     {
-        if !is_supported_wasm_api_version(release_channel, wasm_api_version) {
-            return false;
-        }
+        return false;
     }
 
     true
@@ -292,19 +291,17 @@ impl ExtensionStore {
         // it must be asynchronously rebuilt.
         let mut extension_index = ExtensionIndex::default();
         let mut extension_index_needs_rebuild = true;
-        if let Ok(index_content) = index_content {
-            if let Some(index) = serde_json::from_str(&index_content).log_err() {
-                extension_index = index;
-                if let (Ok(Some(index_metadata)), Ok(Some(extensions_metadata))) =
-                    (index_metadata, extensions_metadata)
-                {
-                    if index_metadata
-                        .mtime
-                        .bad_is_greater_than(extensions_metadata.mtime)
-                    {
-                        extension_index_needs_rebuild = false;
-                    }
-                }
+        if let Ok(index_content) = index_content
+            && let Some(index) = serde_json::from_str(&index_content).log_err()
+        {
+            extension_index = index;
+            if let (Ok(Some(index_metadata)), Ok(Some(extensions_metadata))) =
+                (index_metadata, extensions_metadata)
+                && index_metadata
+                    .mtime
+                    .bad_is_greater_than(extensions_metadata.mtime)
+            {
+                extension_index_needs_rebuild = false;
             }
         }
 
@@ -392,10 +389,9 @@ impl ExtensionStore {
 
                         if let Some(path::Component::Normal(extension_dir_name)) =
                             event_path.components().next()
+                            && let Some(extension_id) = extension_dir_name.to_str()
                         {
-                            if let Some(extension_id) = extension_dir_name.to_str() {
-                                reload_tx.unbounded_send(Some(extension_id.into())).ok();
-                            }
+                            reload_tx.unbounded_send(Some(extension_id.into())).ok();
                         }
                     }
                 }
@@ -566,12 +562,12 @@ impl ExtensionStore {
                 extensions
                     .into_iter()
                     .filter(|extension| {
-                        this.extension_index.extensions.get(&extension.id).map_or(
-                            true,
-                            |installed_extension| {
+                        this.extension_index
+                            .extensions
+                            .get(&extension.id)
+                            .is_none_or(|installed_extension| {
                                 installed_extension.manifest.version != extension.manifest.version
-                            },
-                        )
+                            })
                     })
                     .collect()
             })
@@ -763,8 +759,8 @@ impl ExtensionStore {
             if let ExtensionOperation::Install = operation {
                 this.update( cx, |this, cx| {
                     cx.emit(Event::ExtensionInstalled(extension_id.clone()));
-                    if let Some(events) = ExtensionEvents::try_global(cx) {
-                        if let Some(manifest) = this.extension_manifest_for_id(&extension_id) {
+                    if let Some(events) = ExtensionEvents::try_global(cx)
+                        && let Some(manifest) = this.extension_manifest_for_id(&extension_id) {
                             events.update(cx, |this, cx| {
                                 this.emit(
                                     extension::Event::ExtensionInstalled(manifest.clone()),
@@ -772,7 +768,6 @@ impl ExtensionStore {
                                 )
                             });
                         }
-                    }
                 })
                 .ok();
             }
@@ -912,12 +907,12 @@ impl ExtensionStore {
 
             extension_store.update(cx, |_, cx| {
                 cx.emit(Event::ExtensionUninstalled(extension_id.clone()));
-                if let Some(events) = ExtensionEvents::try_global(cx) {
-                    if let Some(manifest) = extension_manifest {
-                        events.update(cx, |this, cx| {
-                            this.emit(extension::Event::ExtensionUninstalled(manifest.clone()), cx)
-                        });
-                    }
+                if let Some(events) = ExtensionEvents::try_global(cx)
+                    && let Some(manifest) = extension_manifest
+                {
+                    events.update(cx, |this, cx| {
+                        this.emit(extension::Event::ExtensionUninstalled(manifest.clone()), cx)
+                    });
                 }
             })?;
 
@@ -997,12 +992,12 @@ impl ExtensionStore {
             this.update(cx, |this, cx| this.reload(None, cx))?.await;
             this.update(cx, |this, cx| {
                 cx.emit(Event::ExtensionInstalled(extension_id.clone()));
-                if let Some(events) = ExtensionEvents::try_global(cx) {
-                    if let Some(manifest) = this.extension_manifest_for_id(&extension_id) {
-                        events.update(cx, |this, cx| {
-                            this.emit(extension::Event::ExtensionInstalled(manifest.clone()), cx)
-                        });
-                    }
+                if let Some(events) = ExtensionEvents::try_global(cx)
+                    && let Some(manifest) = this.extension_manifest_for_id(&extension_id)
+                {
+                    events.update(cx, |this, cx| {
+                        this.emit(extension::Event::ExtensionInstalled(manifest.clone()), cx)
+                    });
                 }
             })?;
 
@@ -1118,15 +1113,17 @@ impl ExtensionStore {
             extensions_to_unload.len() - reload_count
         );
 
-        for extension_id in &extensions_to_load {
-            if let Some(extension) = new_index.extensions.get(extension_id) {
-                telemetry::event!(
-                    "Extension Loaded",
-                    extension_id,
-                    version = extension.manifest.version
-                );
-            }
-        }
+        let extension_ids = extensions_to_load
+            .iter()
+            .filter_map(|id| {
+                Some((
+                    id.clone(),
+                    new_index.extensions.get(id)?.manifest.version.clone(),
+                ))
+            })
+            .collect::<Vec<_>>();
+
+        telemetry::event!("Extensions Loaded", id_and_versions = extension_ids);
 
         let themes_to_remove = old_index
             .themes
@@ -1178,22 +1175,18 @@ impl ExtensionStore {
                 }
             }
 
-            for (server_id, _) in &extension.manifest.context_servers {
+            for server_id in extension.manifest.context_servers.keys() {
                 self.proxy.unregister_context_server(server_id.clone(), cx);
             }
-            for (adapter, _) in &extension.manifest.debug_adapters {
+            for adapter in extension.manifest.debug_adapters.keys() {
                 self.proxy.unregister_debug_adapter(adapter.clone());
             }
-            for (locator, _) in &extension.manifest.debug_locators {
+            for locator in extension.manifest.debug_locators.keys() {
                 self.proxy.unregister_debug_locator(locator.clone());
             }
-            for (command_name, _) in &extension.manifest.slash_commands {
+            for command_name in extension.manifest.slash_commands.keys() {
                 self.proxy.unregister_slash_command(command_name.clone());
             }
-            for (provider_id, _) in &extension.manifest.indexed_docs_providers {
-                self.proxy
-                    .unregister_indexed_docs_provider(provider_id.clone());
-            }
         }
 
         self.wasm_extensions
@@ -1277,6 +1270,7 @@ impl ExtensionStore {
                         queries,
                         context_provider,
                         toolchain_provider: None,
+                        manifest_name: None,
                     })
                 }),
             );
@@ -1342,7 +1336,7 @@ impl ExtensionStore {
                     &extension_path,
                     &extension.manifest,
                     wasm_host.clone(),
-                    &cx,
+                    cx,
                 )
                 .await
                 .with_context(|| format!("Loading extension from {extension_path:?}"));
@@ -1392,16 +1386,11 @@ impl ExtensionStore {
                         );
                     }
 
-                    for (id, _context_server_entry) in &manifest.context_servers {
+                    for id in manifest.context_servers.keys() {
                         this.proxy
                             .register_context_server(extension.clone(), id.clone(), cx);
                     }
 
-                    for (provider_id, _provider) in &manifest.indexed_docs_providers {
-                        this.proxy
-                            .register_indexed_docs_provider(extension.clone(), provider_id.clone());
-                    }
-
                     for (debug_adapter, meta) in &manifest.debug_adapters {
                         let mut path = root_dir.clone();
                         path.push(Path::new(manifest.id.as_ref()));
@@ -1462,7 +1451,7 @@ impl ExtensionStore {
 
                     if extension_dir
                         .file_name()
-                        .map_or(false, |file_name| file_name == ".DS_Store")
+                        .is_some_and(|file_name| file_name == ".DS_Store")
                     {
                         continue;
                     }
@@ -1686,9 +1675,8 @@ impl ExtensionStore {
                 let schema_path = &extension::build_debug_adapter_schema_path(adapter_name, meta);
 
                 if fs.is_file(&src_dir.join(schema_path)).await {
-                    match schema_path.parent() {
-                        Some(parent) => fs.create_dir(&tmp_dir.join(parent)).await?,
-                        None => {}
+                    if let Some(parent) = schema_path.parent() {
+                        fs.create_dir(&tmp_dir.join(parent)).await?
                     }
                     fs.copy_file(
                         &src_dir.join(schema_path),
@@ -1782,7 +1770,7 @@ impl ExtensionStore {
         })?;
 
         for client in clients {
-            Self::sync_extensions_over_ssh(&this, client, cx)
+            Self::sync_extensions_over_ssh(this, client, cx)
                 .await
                 .log_err();
         }
@@ -1794,10 +1782,10 @@ impl ExtensionStore {
         let connection_options = client.read(cx).connection_options();
         let ssh_url = connection_options.ssh_url();
 
-        if let Some(existing_client) = self.ssh_clients.get(&ssh_url) {
-            if existing_client.upgrade().is_some() {
-                return;
-            }
+        if let Some(existing_client) = self.ssh_clients.get(&ssh_url)
+            && existing_client.upgrade().is_some()
+        {
+            return;
         }
 
         self.ssh_clients.insert(ssh_url, client.downgrade());

crates/extension_host/src/extension_store_test.rs 🔗

@@ -160,7 +160,6 @@ async fn test_extension_store(cx: &mut TestAppContext) {
                         language_servers: BTreeMap::default(),
                         context_servers: BTreeMap::default(),
                         slash_commands: BTreeMap::default(),
-                        indexed_docs_providers: BTreeMap::default(),
                         snippets: None,
                         capabilities: Vec::new(),
                         debug_adapters: Default::default(),
@@ -191,7 +190,6 @@ async fn test_extension_store(cx: &mut TestAppContext) {
                         language_servers: BTreeMap::default(),
                         context_servers: BTreeMap::default(),
                         slash_commands: BTreeMap::default(),
-                        indexed_docs_providers: BTreeMap::default(),
                         snippets: None,
                         capabilities: Vec::new(),
                         debug_adapters: Default::default(),
@@ -371,7 +369,6 @@ async fn test_extension_store(cx: &mut TestAppContext) {
                 language_servers: BTreeMap::default(),
                 context_servers: BTreeMap::default(),
                 slash_commands: BTreeMap::default(),
-                indexed_docs_providers: BTreeMap::default(),
                 snippets: None,
                 capabilities: Vec::new(),
                 debug_adapters: Default::default(),

crates/extension_host/src/headless_host.rs 🔗

@@ -163,6 +163,7 @@ impl HeadlessExtensionStore {
                             queries: LanguageQueries::default(),
                             context_provider: None,
                             toolchain_provider: None,
+                            manifest_name: None,
                         })
                     }),
                 );
@@ -174,7 +175,7 @@ impl HeadlessExtensionStore {
         }
 
         let wasm_extension: Arc<dyn Extension> =
-            Arc::new(WasmExtension::load(&extension_dir, &manifest, wasm_host.clone(), &cx).await?);
+            Arc::new(WasmExtension::load(&extension_dir, &manifest, wasm_host.clone(), cx).await?);
 
         for (language_server_id, language_server_config) in &manifest.language_servers {
             for language in language_server_config.languages() {

crates/extension_host/src/wasm_host.rs 🔗

@@ -532,7 +532,7 @@ fn wasm_engine(executor: &BackgroundExecutor) -> wasmtime::Engine {
                     // `Future::poll`.
                     const EPOCH_INTERVAL: Duration = Duration::from_millis(100);
                     let mut timer = Timer::interval(EPOCH_INTERVAL);
-                    while let Some(_) = timer.next().await {
+                    while (timer.next().await).is_some() {
                         // Exit the loop and thread once the engine is dropped.
                         let Some(engine) = engine_ref.upgrade() else {
                             break;
@@ -701,16 +701,15 @@ pub fn parse_wasm_extension_version(
     for part in wasmparser::Parser::new(0).parse_all(wasm_bytes) {
         if let wasmparser::Payload::CustomSection(s) =
             part.context("error parsing wasm extension")?
+            && s.name() == "zed:api-version"
         {
-            if s.name() == "zed:api-version" {
-                version = parse_wasm_extension_version_custom_section(s.data());
-                if version.is_none() {
-                    bail!(
-                        "extension {} has invalid zed:api-version section: {:?}",
-                        extension_id,
-                        s.data()
-                    );
-                }
+            version = parse_wasm_extension_version_custom_section(s.data());
+            if version.is_none() {
+                bail!(
+                    "extension {} has invalid zed:api-version section: {:?}",
+                    extension_id,
+                    s.data()
+                );
             }
         }
     }

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

@@ -938,7 +938,7 @@ impl ExtensionImports for WasmState {
                             binary: settings.binary.map(|binary| settings::CommandSettings {
                                 path: binary.path,
                                 arguments: binary.arguments,
-                                env: binary.env,
+                                env: binary.env.map(|env| env.into_iter().collect()),
                             }),
                             settings: settings.settings,
                             initialization_options: settings.initialization_options,

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

@@ -58,10 +58,9 @@ impl RenderOnce for FeatureUpsell {
                     el.child(
                         Button::new("open_docs", "View Documentation")
                             .icon(IconName::ArrowUpRight)
-                            .icon_size(IconSize::XSmall)
+                            .icon_size(IconSize::Small)
                             .icon_position(IconPosition::End)
                             .on_click({
-                                let docs_url = docs_url.clone();
                                 move |_event, _window, cx| {
                                     telemetry::event!(
                                         "Documentation Viewed",

crates/extensions_ui/src/extensions_ui.rs 🔗

@@ -116,6 +116,7 @@ pub fn init(cx: &mut App) {
                         files: false,
                         directories: true,
                         multiple: false,
+                        prompt: None,
                     },
                     DirectoryLister::Local(
                         workspace.project().clone(),
@@ -693,7 +694,7 @@ impl ExtensionsPage {
                                 cx.open_url(&repository_url);
                             }
                         }))
-                        .tooltip(Tooltip::text(repository_url.clone()))
+                        .tooltip(Tooltip::text(repository_url))
                     })),
             )
     }
@@ -703,7 +704,7 @@ impl ExtensionsPage {
         extension: &ExtensionMetadata,
         cx: &mut Context<Self>,
     ) -> ExtensionCard {
-        let this = cx.entity().clone();
+        let this = cx.entity();
         let status = Self::extension_status(&extension.id, cx);
         let has_dev_extension = Self::dev_extension_exists(&extension.id, cx);
 
@@ -826,7 +827,7 @@ impl ExtensionsPage {
                                         cx.open_url(&repository_url);
                                     }
                                 }))
-                                .tooltip(Tooltip::text(repository_url.clone())),
+                                .tooltip(Tooltip::text(repository_url)),
                             )
                             .child(
                                 PopoverMenu::new(SharedString::from(format!(
@@ -862,7 +863,7 @@ impl ExtensionsPage {
         window: &mut Window,
         cx: &mut App,
     ) -> Entity<ContextMenu> {
-        let context_menu = ContextMenu::build(window, cx, |context_menu, window, _| {
+        ContextMenu::build(window, cx, |context_menu, window, _| {
             context_menu
                 .entry(
                     "Install Another Version...",
@@ -886,9 +887,7 @@ impl ExtensionsPage {
                         cx.write_to_clipboard(ClipboardItem::new_string(authors.join(", ")));
                     }
                 })
-        });
-
-        context_menu
+        })
     }
 
     fn show_extension_version_list(
@@ -1030,15 +1029,14 @@ impl ExtensionsPage {
                                 .read(cx)
                                 .extension_manifest_for_id(&extension_id)
                                 .cloned()
+                                && let Some(events) = extension::ExtensionEvents::try_global(cx)
                             {
-                                if let Some(events) = extension::ExtensionEvents::try_global(cx) {
-                                    events.update(cx, |this, cx| {
-                                        this.emit(
-                                            extension::Event::ConfigureExtensionRequested(manifest),
-                                            cx,
-                                        )
-                                    });
-                                }
+                                events.update(cx, |this, cx| {
+                                    this.emit(
+                                        extension::Event::ConfigureExtensionRequested(manifest),
+                                        cx,
+                                    )
+                                });
                             }
                         }
                     })

crates/feature_flags/src/feature_flags.rs 🔗

@@ -14,7 +14,7 @@ struct FeatureFlags {
 }
 
 pub static ZED_DISABLE_STAFF: LazyLock<bool> = LazyLock::new(|| {
-    std::env::var("ZED_DISABLE_STAFF").map_or(false, |value| !value.is_empty() && value != "0")
+    std::env::var("ZED_DISABLE_STAFF").is_ok_and(|value| !value.is_empty() && value != "0")
 });
 
 impl FeatureFlags {
@@ -77,14 +77,6 @@ impl FeatureFlag for NotebookFeatureFlag {
     const NAME: &'static str = "notebooks";
 }
 
-pub struct ThreadAutoCaptureFeatureFlag {}
-impl FeatureFlag for ThreadAutoCaptureFeatureFlag {
-    const NAME: &'static str = "thread-auto-capture";
-
-    fn enabled_for_staff() -> bool {
-        false
-    }
-}
 pub struct PanicFeatureFlag;
 
 impl FeatureFlag for PanicFeatureFlag {
@@ -97,10 +89,21 @@ impl FeatureFlag for JjUiFeatureFlag {
     const NAME: &'static str = "jj-ui";
 }
 
-pub struct AcpFeatureFlag;
+pub struct GeminiAndNativeFeatureFlag;
+
+impl FeatureFlag for GeminiAndNativeFeatureFlag {
+    // This was previously called "acp".
+    //
+    // We renamed it because existing builds used it to enable the Claude Code
+    // integration too, and we'd like to turn Gemini/Native on in new builds
+    // without enabling Claude Code in old builds.
+    const NAME: &'static str = "gemini-and-native";
+}
+
+pub struct ClaudeCodeFeatureFlag;
 
-impl FeatureFlag for AcpFeatureFlag {
-    const NAME: &'static str = "acp";
+impl FeatureFlag for ClaudeCodeFeatureFlag {
+    const NAME: &'static str = "claude-code";
 }
 
 pub trait FeatureFlagViewExt<V: 'static> {

crates/feedback/src/system_specs.rs 🔗

@@ -31,7 +31,7 @@ impl SystemSpecs {
         let architecture = env::consts::ARCH;
         let commit_sha = match release_channel {
             ReleaseChannel::Dev | ReleaseChannel::Nightly => {
-                AppCommitSha::try_global(cx).map(|sha| sha.full().clone())
+                AppCommitSha::try_global(cx).map(|sha| sha.full())
             }
             _ => None,
         };
@@ -135,7 +135,7 @@ impl Display for SystemSpecs {
 fn try_determine_available_gpus() -> Option<String> {
     #[cfg(any(target_os = "linux", target_os = "freebsd"))]
     {
-        return std::process::Command::new("vulkaninfo")
+        std::process::Command::new("vulkaninfo")
             .args(&["--summary"])
             .output()
             .ok()
@@ -150,11 +150,11 @@ fn try_determine_available_gpus() -> Option<String> {
                 ]
                 .join("\n")
             })
-            .or(Some("Failed to run `vulkaninfo --summary`".to_string()));
+            .or(Some("Failed to run `vulkaninfo --summary`".to_string()))
     }
     #[cfg(not(any(target_os = "linux", target_os = "freebsd")))]
     {
-        return None;
+        None
     }
 }
 

crates/file_finder/src/file_finder.rs 🔗

@@ -17,7 +17,7 @@ use fuzzy::{CharBag, PathMatch, PathMatchCandidate};
 use gpui::{
     Action, AnyElement, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
     KeyContext, Modifiers, ModifiersChangedEvent, ParentElement, Render, Styled, Task, WeakEntity,
-    Window, actions,
+    Window, actions, rems,
 };
 use open_path_prompt::OpenPathPrompt;
 use picker::{Picker, PickerDelegate};
@@ -209,11 +209,11 @@ impl FileFinder {
         let Some(init_modifiers) = self.init_modifiers.take() else {
             return;
         };
-        if self.picker.read(cx).delegate.has_changed_selected_index {
-            if !event.modified() || !init_modifiers.is_subset_of(&event) {
-                self.init_modifiers = None;
-                window.dispatch_action(menu::Confirm.boxed_clone(), cx);
-            }
+        if self.picker.read(cx).delegate.has_changed_selected_index
+            && (!event.modified() || !init_modifiers.is_subset_of(event))
+        {
+            self.init_modifiers = None;
+            window.dispatch_action(menu::Confirm.boxed_clone(), cx);
         }
     }
 
@@ -267,10 +267,9 @@ impl FileFinder {
     ) {
         self.picker.update(cx, |picker, cx| {
             picker.delegate.include_ignored = match picker.delegate.include_ignored {
-                Some(true) => match FileFinderSettings::get_global(cx).include_ignored {
-                    Some(_) => Some(false),
-                    None => None,
-                },
+                Some(true) => FileFinderSettings::get_global(cx)
+                    .include_ignored
+                    .map(|_| false),
                 Some(false) => Some(true),
                 None => Some(true),
             };
@@ -323,34 +322,34 @@ impl FileFinder {
     ) {
         self.picker.update(cx, |picker, cx| {
             let delegate = &mut picker.delegate;
-            if let Some(workspace) = delegate.workspace.upgrade() {
-                if let Some(m) = delegate.matches.get(delegate.selected_index()) {
-                    let path = match &m {
-                        Match::History { path, .. } => {
-                            let worktree_id = path.project.worktree_id;
-                            ProjectPath {
-                                worktree_id,
-                                path: Arc::clone(&path.project.path),
-                            }
+            if let Some(workspace) = delegate.workspace.upgrade()
+                && let Some(m) = delegate.matches.get(delegate.selected_index())
+            {
+                let path = match &m {
+                    Match::History { path, .. } => {
+                        let worktree_id = path.project.worktree_id;
+                        ProjectPath {
+                            worktree_id,
+                            path: Arc::clone(&path.project.path),
                         }
-                        Match::Search(m) => ProjectPath {
-                            worktree_id: WorktreeId::from_usize(m.0.worktree_id),
-                            path: m.0.path.clone(),
-                        },
-                        Match::CreateNew(p) => p.clone(),
-                    };
-                    let open_task = workspace.update(cx, move |workspace, cx| {
-                        workspace.split_path_preview(path, false, Some(split_direction), window, cx)
-                    });
-                    open_task.detach_and_log_err(cx);
-                }
+                    }
+                    Match::Search(m) => ProjectPath {
+                        worktree_id: WorktreeId::from_usize(m.0.worktree_id),
+                        path: m.0.path.clone(),
+                    },
+                    Match::CreateNew(p) => p.clone(),
+                };
+                let open_task = workspace.update(cx, move |workspace, cx| {
+                    workspace.split_path_preview(path, false, Some(split_direction), window, cx)
+                });
+                open_task.detach_and_log_err(cx);
             }
         })
     }
 
     pub fn modal_max_width(width_setting: Option<FileFinderWidth>, window: &mut Window) -> Pixels {
         let window_width = window.viewport_size().width;
-        let small_width = Pixels(545.);
+        let small_width = rems(34.).to_pixels(window.rem_size());
 
         match width_setting {
             None | Some(FileFinderWidth::Small) => small_width,
@@ -497,7 +496,7 @@ impl Match {
     fn panel_match(&self) -> Option<&ProjectPanelOrdMatch> {
         match self {
             Match::History { panel_match, .. } => panel_match.as_ref(),
-            Match::Search(panel_match) => Some(&panel_match),
+            Match::Search(panel_match) => Some(panel_match),
             Match::CreateNew(_) => None,
         }
     }
@@ -537,7 +536,7 @@ impl Matches {
             self.matches.binary_search_by(|m| {
                 // `reverse()` since if cmp_matches(a, b) == Ordering::Greater, then a is better than b.
                 // And we want the better entries go first.
-                Self::cmp_matches(self.separate_history, currently_opened, &m, &entry).reverse()
+                Self::cmp_matches(self.separate_history, currently_opened, m, entry).reverse()
             })
         }
     }
@@ -675,17 +674,17 @@ impl Matches {
             let path_str = panel_match.0.path.to_string_lossy();
             let filename_str = filename.to_string_lossy();
 
-            if let Some(filename_pos) = path_str.rfind(&*filename_str) {
-                if panel_match.0.positions[0] >= filename_pos {
-                    let mut prev_position = panel_match.0.positions[0];
-                    for p in &panel_match.0.positions[1..] {
-                        if *p != prev_position + 1 {
-                            return false;
-                        }
-                        prev_position = *p;
+            if let Some(filename_pos) = path_str.rfind(&*filename_str)
+                && panel_match.0.positions[0] >= filename_pos
+            {
+                let mut prev_position = panel_match.0.positions[0];
+                for p in &panel_match.0.positions[1..] {
+                    if *p != prev_position + 1 {
+                        return false;
                     }
-                    return true;
+                    prev_position = *p;
                 }
+                return true;
             }
         }
 
@@ -878,9 +877,7 @@ impl FileFinderDelegate {
                 PathMatchCandidateSet {
                     snapshot: worktree.snapshot(),
                     include_ignored: self.include_ignored.unwrap_or_else(|| {
-                        worktree
-                            .root_entry()
-                            .map_or(false, |entry| entry.is_ignored)
+                        worktree.root_entry().is_some_and(|entry| entry.is_ignored)
                     }),
                     include_root_name,
                     candidates: project::Candidates::Files,
@@ -1045,10 +1042,10 @@ impl FileFinderDelegate {
                         )
                     } else {
                         let mut path = Arc::clone(project_relative_path);
-                        if project_relative_path.as_ref() == Path::new("") {
-                            if let Some(absolute_path) = &entry_path.absolute {
-                                path = Arc::from(absolute_path.as_path());
-                            }
+                        if project_relative_path.as_ref() == Path::new("")
+                            && let Some(absolute_path) = &entry_path.absolute
+                        {
+                            path = Arc::from(absolute_path.as_path());
                         }
 
                         let mut path_match = PathMatch {
@@ -1078,23 +1075,21 @@ impl FileFinderDelegate {
                 ),
             };
 
-        if file_name_positions.is_empty() {
-            if let Some(user_home_path) = std::env::var("HOME").ok() {
-                let user_home_path = user_home_path.trim();
-                if !user_home_path.is_empty() {
-                    if (&full_path).starts_with(user_home_path) {
-                        full_path.replace_range(0..user_home_path.len(), "~");
-                        full_path_positions.retain_mut(|pos| {
-                            if *pos >= user_home_path.len() {
-                                *pos -= user_home_path.len();
-                                *pos += 1;
-                                true
-                            } else {
-                                false
-                            }
-                        })
+        if file_name_positions.is_empty()
+            && let Some(user_home_path) = std::env::var("HOME").ok()
+        {
+            let user_home_path = user_home_path.trim();
+            if !user_home_path.is_empty() && full_path.starts_with(user_home_path) {
+                full_path.replace_range(0..user_home_path.len(), "~");
+                full_path_positions.retain_mut(|pos| {
+                    if *pos >= user_home_path.len() {
+                        *pos -= user_home_path.len();
+                        *pos += 1;
+                        true
+                    } else {
+                        false
                     }
-                }
+                })
             }
         }
 
@@ -1242,14 +1237,13 @@ impl FileFinderDelegate {
 
     /// Skips first history match (that is displayed topmost) if it's currently opened.
     fn calculate_selected_index(&self, cx: &mut Context<Picker<Self>>) -> usize {
-        if FileFinderSettings::get_global(cx).skip_focus_for_active_in_search {
-            if let Some(Match::History { path, .. }) = self.matches.get(0) {
-                if Some(path) == self.currently_opened_path.as_ref() {
-                    let elements_after_first = self.matches.len() - 1;
-                    if elements_after_first > 0 {
-                        return 1;
-                    }
-                }
+        if FileFinderSettings::get_global(cx).skip_focus_for_active_in_search
+            && let Some(Match::History { path, .. }) = self.matches.get(0)
+            && Some(path) == self.currently_opened_path.as_ref()
+        {
+            let elements_after_first = self.matches.len() - 1;
+            if elements_after_first > 0 {
+                return 1;
             }
         }
 
@@ -1310,10 +1304,10 @@ impl PickerDelegate for FileFinderDelegate {
                 .enumerate()
                 .find(|(_, m)| !matches!(m, Match::History { .. }))
                 .map(|(i, _)| i);
-            if let Some(first_non_history_index) = first_non_history_index {
-                if first_non_history_index > 0 {
-                    return vec![first_non_history_index - 1];
-                }
+            if let Some(first_non_history_index) = first_non_history_index
+                && first_non_history_index > 0
+            {
+                return vec![first_non_history_index - 1];
             }
         }
         Vec::new()
@@ -1402,7 +1396,7 @@ impl PickerDelegate for FileFinderDelegate {
             cx.notify();
             Task::ready(())
         } else {
-            let path_position = PathWithPosition::parse_str(&raw_query);
+            let path_position = PathWithPosition::parse_str(raw_query);
 
             #[cfg(windows)]
             let raw_query = raw_query.trim().to_owned().replace("/", "\\");
@@ -1436,69 +1430,101 @@ impl PickerDelegate for FileFinderDelegate {
         window: &mut Window,
         cx: &mut Context<Picker<FileFinderDelegate>>,
     ) {
-        if let Some(m) = self.matches.get(self.selected_index()) {
-            if let Some(workspace) = self.workspace.upgrade() {
-                let open_task = workspace.update(cx, |workspace, cx| {
-                    let split_or_open =
-                        |workspace: &mut Workspace,
-                         project_path,
-                         window: &mut Window,
-                         cx: &mut Context<Workspace>| {
-                            let allow_preview =
-                                PreviewTabsSettings::get_global(cx).enable_preview_from_file_finder;
-                            if secondary {
-                                workspace.split_path_preview(
-                                    project_path,
-                                    allow_preview,
-                                    None,
-                                    window,
-                                    cx,
-                                )
-                            } else {
-                                workspace.open_path_preview(
-                                    project_path,
-                                    None,
-                                    true,
-                                    allow_preview,
-                                    true,
-                                    window,
-                                    cx,
-                                )
-                            }
-                        };
-                    match &m {
-                        Match::CreateNew(project_path) => {
-                            // Create a new file with the given filename
-                            if secondary {
-                                workspace.split_path_preview(
-                                    project_path.clone(),
-                                    false,
-                                    None,
-                                    window,
-                                    cx,
-                                )
-                            } else {
-                                workspace.open_path_preview(
-                                    project_path.clone(),
-                                    None,
-                                    true,
-                                    false,
-                                    true,
-                                    window,
-                                    cx,
-                                )
-                            }
+        if let Some(m) = self.matches.get(self.selected_index())
+            && let Some(workspace) = self.workspace.upgrade()
+        {
+            let open_task = workspace.update(cx, |workspace, cx| {
+                let split_or_open =
+                    |workspace: &mut Workspace,
+                     project_path,
+                     window: &mut Window,
+                     cx: &mut Context<Workspace>| {
+                        let allow_preview =
+                            PreviewTabsSettings::get_global(cx).enable_preview_from_file_finder;
+                        if secondary {
+                            workspace.split_path_preview(
+                                project_path,
+                                allow_preview,
+                                None,
+                                window,
+                                cx,
+                            )
+                        } else {
+                            workspace.open_path_preview(
+                                project_path,
+                                None,
+                                true,
+                                allow_preview,
+                                true,
+                                window,
+                                cx,
+                            )
                         }
+                    };
+                match &m {
+                    Match::CreateNew(project_path) => {
+                        // Create a new file with the given filename
+                        if secondary {
+                            workspace.split_path_preview(
+                                project_path.clone(),
+                                false,
+                                None,
+                                window,
+                                cx,
+                            )
+                        } else {
+                            workspace.open_path_preview(
+                                project_path.clone(),
+                                None,
+                                true,
+                                false,
+                                true,
+                                window,
+                                cx,
+                            )
+                        }
+                    }
 
-                        Match::History { path, .. } => {
-                            let worktree_id = path.project.worktree_id;
-                            if workspace
-                                .project()
-                                .read(cx)
-                                .worktree_for_id(worktree_id, cx)
-                                .is_some()
-                            {
-                                split_or_open(
+                    Match::History { path, .. } => {
+                        let worktree_id = path.project.worktree_id;
+                        if workspace
+                            .project()
+                            .read(cx)
+                            .worktree_for_id(worktree_id, cx)
+                            .is_some()
+                        {
+                            split_or_open(
+                                workspace,
+                                ProjectPath {
+                                    worktree_id,
+                                    path: Arc::clone(&path.project.path),
+                                },
+                                window,
+                                cx,
+                            )
+                        } else {
+                            match path.absolute.as_ref() {
+                                Some(abs_path) => {
+                                    if secondary {
+                                        workspace.split_abs_path(
+                                            abs_path.to_path_buf(),
+                                            false,
+                                            window,
+                                            cx,
+                                        )
+                                    } else {
+                                        workspace.open_abs_path(
+                                            abs_path.to_path_buf(),
+                                            OpenOptions {
+                                                visible: Some(OpenVisible::None),
+                                                ..Default::default()
+                                            },
+                                            window,
+                                            cx,
+                                        )
+                                    }
+                                }
+                                None => split_or_open(
                                     workspace,
                                     ProjectPath {
                                         worktree_id,
@@ -1506,88 +1532,52 @@ impl PickerDelegate for FileFinderDelegate {
                                     },
                                     window,
                                     cx,
-                                )
-                            } else {
-                                match path.absolute.as_ref() {
-                                    Some(abs_path) => {
-                                        if secondary {
-                                            workspace.split_abs_path(
-                                                abs_path.to_path_buf(),
-                                                false,
-                                                window,
-                                                cx,
-                                            )
-                                        } else {
-                                            workspace.open_abs_path(
-                                                abs_path.to_path_buf(),
-                                                OpenOptions {
-                                                    visible: Some(OpenVisible::None),
-                                                    ..Default::default()
-                                                },
-                                                window,
-                                                cx,
-                                            )
-                                        }
-                                    }
-                                    None => split_or_open(
-                                        workspace,
-                                        ProjectPath {
-                                            worktree_id,
-                                            path: Arc::clone(&path.project.path),
-                                        },
-                                        window,
-                                        cx,
-                                    ),
-                                }
+                                ),
                             }
                         }
-                        Match::Search(m) => split_or_open(
-                            workspace,
-                            ProjectPath {
-                                worktree_id: WorktreeId::from_usize(m.0.worktree_id),
-                                path: m.0.path.clone(),
-                            },
-                            window,
-                            cx,
-                        ),
                     }
-                });
+                    Match::Search(m) => split_or_open(
+                        workspace,
+                        ProjectPath {
+                            worktree_id: WorktreeId::from_usize(m.0.worktree_id),
+                            path: m.0.path.clone(),
+                        },
+                        window,
+                        cx,
+                    ),
+                }
+            });
 
-                let row = self
-                    .latest_search_query
-                    .as_ref()
-                    .and_then(|query| query.path_position.row)
-                    .map(|row| row.saturating_sub(1));
-                let col = self
-                    .latest_search_query
-                    .as_ref()
-                    .and_then(|query| query.path_position.column)
-                    .unwrap_or(0)
-                    .saturating_sub(1);
-                let finder = self.file_finder.clone();
-
-                cx.spawn_in(window, async move |_, cx| {
-                    let item = open_task.await.notify_async_err(cx)?;
-                    if let Some(row) = row {
-                        if let Some(active_editor) = item.downcast::<Editor>() {
-                            active_editor
-                                .downgrade()
-                                .update_in(cx, |editor, window, cx| {
-                                    editor.go_to_singleton_buffer_point(
-                                        Point::new(row, col),
-                                        window,
-                                        cx,
-                                    );
-                                })
-                                .log_err();
-                        }
-                    }
-                    finder.update(cx, |_, cx| cx.emit(DismissEvent)).ok()?;
+            let row = self
+                .latest_search_query
+                .as_ref()
+                .and_then(|query| query.path_position.row)
+                .map(|row| row.saturating_sub(1));
+            let col = self
+                .latest_search_query
+                .as_ref()
+                .and_then(|query| query.path_position.column)
+                .unwrap_or(0)
+                .saturating_sub(1);
+            let finder = self.file_finder.clone();
+
+            cx.spawn_in(window, async move |_, cx| {
+                let item = open_task.await.notify_async_err(cx)?;
+                if let Some(row) = row
+                    && let Some(active_editor) = item.downcast::<Editor>()
+                {
+                    active_editor
+                        .downgrade()
+                        .update_in(cx, |editor, window, cx| {
+                            editor.go_to_singleton_buffer_point(Point::new(row, col), window, cx);
+                        })
+                        .log_err();
+                }
+                finder.update(cx, |_, cx| cx.emit(DismissEvent)).ok()?;
 
-                    Some(())
-                })
-                .detach();
-            }
+                Some(())
+            })
+            .detach();
         }
     }
 
@@ -1759,7 +1749,7 @@ impl PickerDelegate for FileFinderDelegate {
                                         Some(ContextMenu::build(window, cx, {
                                             let focus_handle = focus_handle.clone();
                                             move |menu, _, _| {
-                                                menu.context(focus_handle.clone())
+                                                menu.context(focus_handle)
                                                     .action(
                                                         "Split Left",
                                                         pane::SplitLeft.boxed_clone(),

crates/file_finder/src/file_finder_tests.rs 🔗

@@ -1614,7 +1614,7 @@ async fn test_select_current_open_file_when_no_history(cx: &mut gpui::TestAppCon
 
     let picker = open_file_picker(&workspace, cx);
     picker.update(cx, |finder, _| {
-        assert_match_selection(&finder, 0, "1_qw");
+        assert_match_selection(finder, 0, "1_qw");
     });
 }
 
@@ -2623,7 +2623,7 @@ async fn open_queried_buffer(
     workspace: &Entity<Workspace>,
     cx: &mut gpui::VisualTestContext,
 ) -> Vec<FoundPath> {
-    let picker = open_file_picker(&workspace, cx);
+    let picker = open_file_picker(workspace, cx);
     cx.simulate_input(input);
 
     let history_items = picker.update(cx, |finder, _| {

crates/file_finder/src/open_path_prompt.rs 🔗

@@ -75,16 +75,16 @@ impl OpenPathDelegate {
                 ..
             } => {
                 let mut i = selected_match_index;
-                if let Some(user_input) = user_input {
-                    if !user_input.exists || !user_input.is_dir {
-                        if i == 0 {
-                            return Some(CandidateInfo {
-                                path: user_input.file.clone(),
-                                is_dir: false,
-                            });
-                        } else {
-                            i -= 1;
-                        }
+                if let Some(user_input) = user_input
+                    && (!user_input.exists || !user_input.is_dir)
+                {
+                    if i == 0 {
+                        return Some(CandidateInfo {
+                            path: user_input.file.clone(),
+                            is_dir: false,
+                        });
+                    } else {
+                        i -= 1;
                     }
                 }
                 let id = self.string_matches.get(i)?.candidate_id;
@@ -112,7 +112,7 @@ impl OpenPathDelegate {
                 entries,
                 ..
             } => user_input
-                .into_iter()
+                .iter()
                 .filter(|user_input| !user_input.exists || !user_input.is_dir)
                 .map(|user_input| user_input.file.string.clone())
                 .chain(self.string_matches.iter().filter_map(|string_match| {
@@ -637,7 +637,7 @@ impl PickerDelegate for OpenPathDelegate {
                 FileIcons::get_folder_icon(false, cx)?
             } else {
                 let path = path::Path::new(&candidate.path.string);
-                FileIcons::get_icon(&path, cx)?
+                FileIcons::get_icon(path, cx)?
             };
             Some(Icon::from_path(icon).color(Color::Muted))
         });
@@ -653,7 +653,7 @@ impl PickerDelegate for OpenPathDelegate {
                         if parent_path == &self.prompt_root {
                             format!("{}{}", self.prompt_root, candidate.path.string)
                         } else {
-                            candidate.path.string.clone()
+                            candidate.path.string
                         },
                         match_positions,
                     )),
@@ -684,7 +684,7 @@ impl PickerDelegate for OpenPathDelegate {
                                 };
                                 StyledText::new(label)
                                     .with_default_highlights(
-                                        &window.text_style().clone(),
+                                        &window.text_style(),
                                         vec![(
                                             delta..delta + label_len,
                                             HighlightStyle::color(Color::Conflict.color(cx)),
@@ -694,7 +694,7 @@ impl PickerDelegate for OpenPathDelegate {
                             } else {
                                 StyledText::new(format!("{label} (create)"))
                                     .with_default_highlights(
-                                        &window.text_style().clone(),
+                                        &window.text_style(),
                                         vec![(
                                             delta..delta + label_len,
                                             HighlightStyle::color(Color::Created.color(cx)),
@@ -728,7 +728,7 @@ impl PickerDelegate for OpenPathDelegate {
                         .child(LabelLike::new().child(label_with_highlights)),
                 )
             }
-            DirectoryState::None { .. } => return None,
+            DirectoryState::None { .. } => None,
         }
     }
 

crates/file_icons/src/file_icons.rs 🔗

@@ -33,13 +33,23 @@ impl FileIcons {
         // TODO: Associate a type with the languages and have the file's language
         //       override these associations
 
-        // check if file name is in suffixes
-        // e.g. catch file named `eslint.config.js` instead of `.eslint.config.js`
-        if let Some(typ) = path.file_name().and_then(|typ| typ.to_str()) {
+        if let Some(mut typ) = path.file_name().and_then(|typ| typ.to_str()) {
+            // check if file name is in suffixes
+            // e.g. catch file named `eslint.config.js` instead of `.eslint.config.js`
             let maybe_path = get_icon_from_suffix(typ);
             if maybe_path.is_some() {
                 return maybe_path;
             }
+
+            // check if suffix based on first dot is in suffixes
+            // e.g. consider `module.js` as suffix to angular's module file named `auth.module.js`
+            while let Some((_, suffix)) = typ.split_once('.') {
+                let maybe_path = get_icon_from_suffix(suffix);
+                if maybe_path.is_some() {
+                    return maybe_path;
+                }
+                typ = suffix;
+            }
         }
 
         // primary case: check if the files extension or the hidden file name
@@ -62,7 +72,7 @@ impl FileIcons {
                 return maybe_path;
             }
         }
-        return this.get_icon_for_type("default", cx);
+        this.get_icon_for_type("default", cx)
     }
 
     fn default_icon_theme(cx: &App) -> Option<Arc<IconTheme>> {

crates/fs/Cargo.toml 🔗

@@ -51,6 +51,7 @@ ashpd.workspace = true
 
 [dev-dependencies]
 gpui = { workspace = true, features = ["test-support"] }
+git = { workspace = true, features = ["test-support"] }
 
 [features]
 test-support = ["gpui/test-support", "git/test-support"]

crates/fs/src/fake_git_repo.rs 🔗

@@ -1,8 +1,9 @@
-use crate::{FakeFs, Fs};
+use crate::{FakeFs, FakeFsEntry, Fs};
 use anyhow::{Context as _, Result};
 use collections::{HashMap, HashSet};
 use futures::future::{self, BoxFuture, join_all};
 use git::{
+    Oid,
     blame::Blame,
     repository::{
         AskPassDelegate, Branch, CommitDetails, CommitOptions, FetchOptions, GitRepository,
@@ -10,8 +11,9 @@ use git::{
     },
     status::{FileStatus, GitStatus, StatusCode, TrackedStatus, UnmergedStatus},
 };
-use gpui::{AsyncApp, BackgroundExecutor, SharedString};
+use gpui::{AsyncApp, BackgroundExecutor, SharedString, Task};
 use ignore::gitignore::GitignoreBuilder;
+use parking_lot::Mutex;
 use rope::Rope;
 use smol::future::FutureExt as _;
 use std::{path::PathBuf, sync::Arc};
@@ -19,6 +21,7 @@ use std::{path::PathBuf, sync::Arc};
 #[derive(Clone)]
 pub struct FakeGitRepository {
     pub(crate) fs: Arc<FakeFs>,
+    pub(crate) checkpoints: Arc<Mutex<HashMap<Oid, FakeFsEntry>>>,
     pub(crate) executor: BackgroundExecutor,
     pub(crate) dot_git_path: PathBuf,
     pub(crate) repository_dir_path: PathBuf,
@@ -183,7 +186,7 @@ impl GitRepository for FakeGitRepository {
         async move { None }.boxed()
     }
 
-    fn status(&self, path_prefixes: &[RepoPath]) -> BoxFuture<'_, Result<GitStatus>> {
+    fn status(&self, path_prefixes: &[RepoPath]) -> Task<Result<GitStatus>> {
         let workdir_path = self.dot_git_path.parent().unwrap();
 
         // Load gitignores
@@ -311,7 +314,10 @@ impl GitRepository for FakeGitRepository {
                 entries: entries.into(),
             })
         });
-        async move { result? }.boxed()
+        Task::ready(match result {
+            Ok(result) => result,
+            Err(e) => Err(e),
+        })
     }
 
     fn branches(&self) -> BoxFuture<'_, Result<Vec<Branch>>> {
@@ -339,7 +345,7 @@ impl GitRepository for FakeGitRepository {
 
     fn create_branch(&self, name: String) -> BoxFuture<'_, Result<()>> {
         self.with_state_async(true, move |state| {
-            state.branches.insert(name.to_owned());
+            state.branches.insert(name);
             Ok(())
         })
     }
@@ -402,11 +408,11 @@ impl GitRepository for FakeGitRepository {
         &self,
         _paths: Vec<RepoPath>,
         _env: Arc<HashMap<String, String>>,
-    ) -> BoxFuture<Result<()>> {
+    ) -> BoxFuture<'_, Result<()>> {
         unimplemented!()
     }
 
-    fn stash_pop(&self, _env: Arc<HashMap<String, String>>) -> BoxFuture<Result<()>> {
+    fn stash_pop(&self, _env: Arc<HashMap<String, String>>) -> BoxFuture<'_, Result<()>> {
         unimplemented!()
     }
 
@@ -466,22 +472,57 @@ impl GitRepository for FakeGitRepository {
     }
 
     fn checkpoint(&self) -> BoxFuture<'static, Result<GitRepositoryCheckpoint>> {
-        unimplemented!()
+        let executor = self.executor.clone();
+        let fs = self.fs.clone();
+        let checkpoints = self.checkpoints.clone();
+        let repository_dir_path = self.repository_dir_path.parent().unwrap().to_path_buf();
+        async move {
+            executor.simulate_random_delay().await;
+            let oid = Oid::random(&mut executor.rng());
+            let entry = fs.entry(&repository_dir_path)?;
+            checkpoints.lock().insert(oid, entry);
+            Ok(GitRepositoryCheckpoint { commit_sha: oid })
+        }
+        .boxed()
     }
 
-    fn restore_checkpoint(
-        &self,
-        _checkpoint: GitRepositoryCheckpoint,
-    ) -> BoxFuture<'_, Result<()>> {
-        unimplemented!()
+    fn restore_checkpoint(&self, checkpoint: GitRepositoryCheckpoint) -> BoxFuture<'_, Result<()>> {
+        let executor = self.executor.clone();
+        let fs = self.fs.clone();
+        let checkpoints = self.checkpoints.clone();
+        let repository_dir_path = self.repository_dir_path.parent().unwrap().to_path_buf();
+        async move {
+            executor.simulate_random_delay().await;
+            let checkpoints = checkpoints.lock();
+            let entry = checkpoints
+                .get(&checkpoint.commit_sha)
+                .context(format!("invalid checkpoint: {}", checkpoint.commit_sha))?;
+            fs.insert_entry(&repository_dir_path, entry.clone())?;
+            Ok(())
+        }
+        .boxed()
     }
 
     fn compare_checkpoints(
         &self,
-        _left: GitRepositoryCheckpoint,
-        _right: GitRepositoryCheckpoint,
+        left: GitRepositoryCheckpoint,
+        right: GitRepositoryCheckpoint,
     ) -> BoxFuture<'_, Result<bool>> {
-        unimplemented!()
+        let executor = self.executor.clone();
+        let checkpoints = self.checkpoints.clone();
+        async move {
+            executor.simulate_random_delay().await;
+            let checkpoints = checkpoints.lock();
+            let left = checkpoints
+                .get(&left.commit_sha)
+                .context(format!("invalid left checkpoint: {}", left.commit_sha))?;
+            let right = checkpoints
+                .get(&right.commit_sha)
+                .context(format!("invalid right checkpoint: {}", right.commit_sha))?;
+
+            Ok(left == right)
+        }
+        .boxed()
     }
 
     fn diff_checkpoints(
@@ -496,3 +537,63 @@ impl GitRepository for FakeGitRepository {
         unimplemented!()
     }
 }
+
+#[cfg(test)]
+mod tests {
+    use crate::{FakeFs, Fs};
+    use gpui::BackgroundExecutor;
+    use serde_json::json;
+    use std::path::Path;
+    use util::path;
+
+    #[gpui::test]
+    async fn test_checkpoints(executor: BackgroundExecutor) {
+        let fs = FakeFs::new(executor);
+        fs.insert_tree(
+            path!("/"),
+            json!({
+                "bar": {
+                    "baz": "qux"
+                },
+                "foo": {
+                    ".git": {},
+                    "a": "lorem",
+                    "b": "ipsum",
+                },
+            }),
+        )
+        .await;
+        fs.with_git_state(Path::new("/foo/.git"), true, |_git| {})
+            .unwrap();
+        let repository = fs.open_repo(Path::new("/foo/.git")).unwrap();
+
+        let checkpoint_1 = repository.checkpoint().await.unwrap();
+        fs.write(Path::new("/foo/b"), b"IPSUM").await.unwrap();
+        fs.write(Path::new("/foo/c"), b"dolor").await.unwrap();
+        let checkpoint_2 = repository.checkpoint().await.unwrap();
+        let checkpoint_3 = repository.checkpoint().await.unwrap();
+
+        assert!(
+            repository
+                .compare_checkpoints(checkpoint_2.clone(), checkpoint_3.clone())
+                .await
+                .unwrap()
+        );
+        assert!(
+            !repository
+                .compare_checkpoints(checkpoint_1.clone(), checkpoint_2.clone())
+                .await
+                .unwrap()
+        );
+
+        repository.restore_checkpoint(checkpoint_1).await.unwrap();
+        assert_eq!(
+            fs.files_with_contents(Path::new("")),
+            [
+                (Path::new(path!("/bar/baz")).into(), b"qux".into()),
+                (Path::new(path!("/foo/a")).into(), b"lorem".into()),
+                (Path::new(path!("/foo/b")).into(), b"ipsum".into())
+            ]
+        );
+    }
+}

crates/fs/src/fs.rs 🔗

@@ -12,7 +12,7 @@ use gpui::BackgroundExecutor;
 use gpui::Global;
 use gpui::ReadGlobal as _;
 use std::borrow::Cow;
-use util::command::new_std_command;
+use util::command::{new_smol_command, new_std_command};
 
 #[cfg(unix)]
 use std::os::fd::{AsFd, AsRawFd};
@@ -134,6 +134,7 @@ pub trait Fs: Send + Sync {
     fn home_dir(&self) -> Option<PathBuf>;
     fn open_repo(&self, abs_dot_git: &Path) -> Option<Arc<dyn GitRepository>>;
     fn git_init(&self, abs_work_directory: &Path, fallback_branch_name: String) -> Result<()>;
+    async fn git_clone(&self, repo_url: &str, abs_work_directory: &Path) -> Result<()>;
     fn is_fake(&self) -> bool;
     async fn is_case_sensitive(&self) -> Result<bool>;
 
@@ -419,18 +420,19 @@ impl Fs for RealFs {
 
     async fn remove_file(&self, path: &Path, options: RemoveOptions) -> Result<()> {
         #[cfg(windows)]
-        if let Ok(Some(metadata)) = self.metadata(path).await {
-            if metadata.is_symlink && metadata.is_dir {
-                self.remove_dir(
-                    path,
-                    RemoveOptions {
-                        recursive: false,
-                        ignore_if_not_exists: true,
-                    },
-                )
-                .await?;
-                return Ok(());
-            }
+        if let Ok(Some(metadata)) = self.metadata(path).await
+            && metadata.is_symlink
+            && metadata.is_dir
+        {
+            self.remove_dir(
+                path,
+                RemoveOptions {
+                    recursive: false,
+                    ignore_if_not_exists: true,
+                },
+            )
+            .await?;
+            return Ok(());
         }
 
         match smol::fs::remove_file(path).await {
@@ -466,11 +468,11 @@ impl Fs for RealFs {
 
     #[cfg(any(target_os = "linux", target_os = "freebsd"))]
     async fn trash_file(&self, path: &Path, _options: RemoveOptions) -> Result<()> {
-        if let Ok(Some(metadata)) = self.metadata(path).await {
-            if metadata.is_symlink {
-                // TODO: trash_file does not support trashing symlinks yet - https://github.com/bilelmoussaoui/ashpd/issues/255
-                return self.remove_file(path, RemoveOptions::default()).await;
-            }
+        if let Ok(Some(metadata)) = self.metadata(path).await
+            && metadata.is_symlink
+        {
+            // TODO: trash_file does not support trashing symlinks yet - https://github.com/bilelmoussaoui/ashpd/issues/255
+            return self.remove_file(path, RemoveOptions::default()).await;
         }
         let file = smol::fs::File::open(path).await?;
         match trash::trash_file(&file.as_fd()).await {
@@ -623,13 +625,13 @@ impl Fs for RealFs {
     async fn is_file(&self, path: &Path) -> bool {
         smol::fs::metadata(path)
             .await
-            .map_or(false, |metadata| metadata.is_file())
+            .is_ok_and(|metadata| metadata.is_file())
     }
 
     async fn is_dir(&self, path: &Path) -> bool {
         smol::fs::metadata(path)
             .await
-            .map_or(false, |metadata| metadata.is_dir())
+            .is_ok_and(|metadata| metadata.is_dir())
     }
 
     async fn metadata(&self, path: &Path) -> Result<Option<Metadata>> {
@@ -765,24 +767,23 @@ impl Fs for RealFs {
         let pending_paths: Arc<Mutex<Vec<PathEvent>>> = Default::default();
         let watcher = Arc::new(fs_watcher::FsWatcher::new(tx, pending_paths.clone()));
 
-        if watcher.add(path).is_err() {
-            // If the path doesn't exist yet (e.g. settings.json), watch the parent dir to learn when it's created.
-            if let Some(parent) = path.parent() {
-                if let Err(e) = watcher.add(parent) {
-                    log::warn!("Failed to watch: {e}");
-                }
-            }
+        // If the path doesn't exist yet (e.g. settings.json), watch the parent dir to learn when it's created.
+        if watcher.add(path).is_err()
+            && let Some(parent) = path.parent()
+            && let Err(e) = watcher.add(parent)
+        {
+            log::warn!("Failed to watch: {e}");
         }
 
         // Check if path is a symlink and follow the target parent
-        if let Some(mut target) = self.read_link(&path).await.ok() {
+        if let Some(mut target) = self.read_link(path).await.ok() {
             // Check if symlink target is relative path, if so make it absolute
-            if target.is_relative() {
-                if let Some(parent) = path.parent() {
-                    target = parent.join(target);
-                    if let Ok(canonical) = self.canonicalize(&target).await {
-                        target = SanitizedPath::from(canonical).as_path().to_path_buf();
-                    }
+            if target.is_relative()
+                && let Some(parent) = path.parent()
+            {
+                target = parent.join(target);
+                if let Ok(canonical) = self.canonicalize(&target).await {
+                    target = SanitizedPath::from(canonical).as_path().to_path_buf();
                 }
             }
             watcher.add(&target).ok();
@@ -839,6 +840,23 @@ impl Fs for RealFs {
         Ok(())
     }
 
+    async fn git_clone(&self, repo_url: &str, abs_work_directory: &Path) -> Result<()> {
+        let output = new_smol_command("git")
+            .current_dir(abs_work_directory)
+            .args(&["clone", repo_url])
+            .output()
+            .await?;
+
+        if !output.status.success() {
+            anyhow::bail!(
+                "git clone failed: {}",
+                String::from_utf8_lossy(&output.stderr)
+            );
+        }
+
+        Ok(())
+    }
+
     fn is_fake(&self) -> bool {
         false
     }
@@ -906,7 +924,7 @@ pub struct FakeFs {
 
 #[cfg(any(test, feature = "test-support"))]
 struct FakeFsState {
-    root: Arc<Mutex<FakeFsEntry>>,
+    root: FakeFsEntry,
     next_inode: u64,
     next_mtime: SystemTime,
     git_event_tx: smol::channel::Sender<PathBuf>,
@@ -921,7 +939,7 @@ struct FakeFsState {
 }
 
 #[cfg(any(test, feature = "test-support"))]
-#[derive(Debug)]
+#[derive(Clone, Debug)]
 enum FakeFsEntry {
     File {
         inode: u64,
@@ -935,7 +953,7 @@ enum FakeFsEntry {
         inode: u64,
         mtime: MTime,
         len: u64,
-        entries: BTreeMap<String, Arc<Mutex<FakeFsEntry>>>,
+        entries: BTreeMap<String, FakeFsEntry>,
         git_repo_state: Option<Arc<Mutex<FakeGitRepositoryState>>>,
     },
     Symlink {
@@ -943,6 +961,67 @@ enum FakeFsEntry {
     },
 }
 
+#[cfg(any(test, feature = "test-support"))]
+impl PartialEq for FakeFsEntry {
+    fn eq(&self, other: &Self) -> bool {
+        match (self, other) {
+            (
+                Self::File {
+                    inode: l_inode,
+                    mtime: l_mtime,
+                    len: l_len,
+                    content: l_content,
+                    git_dir_path: l_git_dir_path,
+                },
+                Self::File {
+                    inode: r_inode,
+                    mtime: r_mtime,
+                    len: r_len,
+                    content: r_content,
+                    git_dir_path: r_git_dir_path,
+                },
+            ) => {
+                l_inode == r_inode
+                    && l_mtime == r_mtime
+                    && l_len == r_len
+                    && l_content == r_content
+                    && l_git_dir_path == r_git_dir_path
+            }
+            (
+                Self::Dir {
+                    inode: l_inode,
+                    mtime: l_mtime,
+                    len: l_len,
+                    entries: l_entries,
+                    git_repo_state: l_git_repo_state,
+                },
+                Self::Dir {
+                    inode: r_inode,
+                    mtime: r_mtime,
+                    len: r_len,
+                    entries: r_entries,
+                    git_repo_state: r_git_repo_state,
+                },
+            ) => {
+                let same_repo_state = match (l_git_repo_state.as_ref(), r_git_repo_state.as_ref()) {
+                    (Some(l), Some(r)) => Arc::ptr_eq(l, r),
+                    (None, None) => true,
+                    _ => false,
+                };
+                l_inode == r_inode
+                    && l_mtime == r_mtime
+                    && l_len == r_len
+                    && l_entries == r_entries
+                    && same_repo_state
+            }
+            (Self::Symlink { target: l_target }, Self::Symlink { target: r_target }) => {
+                l_target == r_target
+            }
+            _ => false,
+        }
+    }
+}
+
 #[cfg(any(test, feature = "test-support"))]
 impl FakeFsState {
     fn get_and_increment_mtime(&mut self) -> MTime {
@@ -957,25 +1036,9 @@ impl FakeFsState {
         inode
     }
 
-    fn read_path(&self, target: &Path) -> Result<Arc<Mutex<FakeFsEntry>>> {
-        Ok(self
-            .try_read_path(target, true)
-            .ok_or_else(|| {
-                anyhow!(io::Error::new(
-                    io::ErrorKind::NotFound,
-                    format!("not found: {target:?}")
-                ))
-            })?
-            .0)
-    }
-
-    fn try_read_path(
-        &self,
-        target: &Path,
-        follow_symlink: bool,
-    ) -> Option<(Arc<Mutex<FakeFsEntry>>, PathBuf)> {
-        let mut path = target.to_path_buf();
+    fn canonicalize(&self, target: &Path, follow_symlink: bool) -> Option<PathBuf> {
         let mut canonical_path = PathBuf::new();
+        let mut path = target.to_path_buf();
         let mut entry_stack = Vec::new();
         'outer: loop {
             let mut path_components = path.components().peekable();
@@ -985,7 +1048,7 @@ impl FakeFsState {
                     Component::Prefix(prefix_component) => prefix = Some(prefix_component),
                     Component::RootDir => {
                         entry_stack.clear();
-                        entry_stack.push(self.root.clone());
+                        entry_stack.push(&self.root);
                         canonical_path.clear();
                         match prefix {
                             Some(prefix_component) => {
@@ -1002,20 +1065,18 @@ impl FakeFsState {
                         canonical_path.pop();
                     }
                     Component::Normal(name) => {
-                        let current_entry = entry_stack.last().cloned()?;
-                        let current_entry = current_entry.lock();
-                        if let FakeFsEntry::Dir { entries, .. } = &*current_entry {
-                            let entry = entries.get(name.to_str().unwrap()).cloned()?;
-                            if path_components.peek().is_some() || follow_symlink {
-                                let entry = entry.lock();
-                                if let FakeFsEntry::Symlink { target, .. } = &*entry {
-                                    let mut target = target.clone();
-                                    target.extend(path_components);
-                                    path = target;
-                                    continue 'outer;
-                                }
+                        let current_entry = *entry_stack.last()?;
+                        if let FakeFsEntry::Dir { entries, .. } = current_entry {
+                            let entry = entries.get(name.to_str().unwrap())?;
+                            if (path_components.peek().is_some() || follow_symlink)
+                                && let FakeFsEntry::Symlink { target, .. } = entry
+                            {
+                                let mut target = target.clone();
+                                target.extend(path_components);
+                                path = target;
+                                continue 'outer;
                             }
-                            entry_stack.push(entry.clone());
+                            entry_stack.push(entry);
                             canonical_path = canonical_path.join(name);
                         } else {
                             return None;
@@ -1025,19 +1086,74 @@ impl FakeFsState {
             }
             break;
         }
-        Some((entry_stack.pop()?, canonical_path))
+
+        if entry_stack.is_empty() {
+            None
+        } else {
+            Some(canonical_path)
+        }
+    }
+
+    fn try_entry(
+        &mut self,
+        target: &Path,
+        follow_symlink: bool,
+    ) -> Option<(&mut FakeFsEntry, PathBuf)> {
+        let canonical_path = self.canonicalize(target, follow_symlink)?;
+
+        let mut components = canonical_path
+            .components()
+            .skip_while(|component| matches!(component, Component::Prefix(_)));
+        let Some(Component::RootDir) = components.next() else {
+            panic!(
+                "the path {:?} was not canonicalized properly {:?}",
+                target, canonical_path
+            )
+        };
+
+        let mut entry = &mut self.root;
+        for component in components {
+            match component {
+                Component::Normal(name) => {
+                    if let FakeFsEntry::Dir { entries, .. } = entry {
+                        entry = entries.get_mut(name.to_str().unwrap())?;
+                    } else {
+                        return None;
+                    }
+                }
+                _ => {
+                    panic!(
+                        "the path {:?} was not canonicalized properly {:?}",
+                        target, canonical_path
+                    )
+                }
+            }
+        }
+
+        Some((entry, canonical_path))
+    }
+
+    fn entry(&mut self, target: &Path) -> Result<&mut FakeFsEntry> {
+        Ok(self
+            .try_entry(target, true)
+            .ok_or_else(|| {
+                anyhow!(io::Error::new(
+                    io::ErrorKind::NotFound,
+                    format!("not found: {target:?}")
+                ))
+            })?
+            .0)
     }
 
-    fn write_path<Fn, T>(&self, path: &Path, callback: Fn) -> Result<T>
+    fn write_path<Fn, T>(&mut self, path: &Path, callback: Fn) -> Result<T>
     where
-        Fn: FnOnce(btree_map::Entry<String, Arc<Mutex<FakeFsEntry>>>) -> Result<T>,
+        Fn: FnOnce(btree_map::Entry<String, FakeFsEntry>) -> Result<T>,
     {
         let path = normalize_path(path);
         let filename = path.file_name().context("cannot overwrite the root")?;
         let parent_path = path.parent().unwrap();
 
-        let parent = self.read_path(parent_path)?;
-        let mut parent = parent.lock();
+        let parent = self.entry(parent_path)?;
         let new_entry = parent
             .dir_entries(parent_path)?
             .entry(filename.to_str().unwrap().into());
@@ -1087,13 +1203,13 @@ impl FakeFs {
             this: this.clone(),
             executor: executor.clone(),
             state: Arc::new(Mutex::new(FakeFsState {
-                root: Arc::new(Mutex::new(FakeFsEntry::Dir {
+                root: FakeFsEntry::Dir {
                     inode: 0,
                     mtime: MTime(UNIX_EPOCH),
                     len: 0,
                     entries: Default::default(),
                     git_repo_state: None,
-                })),
+                },
                 git_event_tx: tx,
                 next_mtime: UNIX_EPOCH + Self::SYSTEMTIME_INTERVAL,
                 next_inode: 1,
@@ -1143,15 +1259,15 @@ impl FakeFs {
             .write_path(path, move |entry| {
                 match entry {
                     btree_map::Entry::Vacant(e) => {
-                        e.insert(Arc::new(Mutex::new(FakeFsEntry::File {
+                        e.insert(FakeFsEntry::File {
                             inode: new_inode,
                             mtime: new_mtime,
                             content: Vec::new(),
                             len: 0,
                             git_dir_path: None,
-                        })));
+                        });
                     }
-                    btree_map::Entry::Occupied(mut e) => match &mut *e.get_mut().lock() {
+                    btree_map::Entry::Occupied(mut e) => match &mut *e.get_mut() {
                         FakeFsEntry::File { mtime, .. } => *mtime = new_mtime,
                         FakeFsEntry::Dir { mtime, .. } => *mtime = new_mtime,
                         FakeFsEntry::Symlink { .. } => {}
@@ -1170,7 +1286,7 @@ impl FakeFs {
     pub async fn insert_symlink(&self, path: impl AsRef<Path>, target: PathBuf) {
         let mut state = self.state.lock();
         let path = path.as_ref();
-        let file = Arc::new(Mutex::new(FakeFsEntry::Symlink { target }));
+        let file = FakeFsEntry::Symlink { target };
         state
             .write_path(path.as_ref(), move |e| match e {
                 btree_map::Entry::Vacant(e) => {
@@ -1203,13 +1319,13 @@ impl FakeFs {
             match entry {
                 btree_map::Entry::Vacant(e) => {
                     kind = Some(PathEventKind::Created);
-                    e.insert(Arc::new(Mutex::new(FakeFsEntry::File {
+                    e.insert(FakeFsEntry::File {
                         inode: new_inode,
                         mtime: new_mtime,
                         len: new_len,
                         content: new_content,
                         git_dir_path: None,
-                    })));
+                    });
                 }
                 btree_map::Entry::Occupied(mut e) => {
                     kind = Some(PathEventKind::Changed);
@@ -1219,7 +1335,7 @@ impl FakeFs {
                         len,
                         content,
                         ..
-                    } = &mut *e.get_mut().lock()
+                    } = e.get_mut()
                     {
                         *mtime = new_mtime;
                         *content = new_content;
@@ -1241,9 +1357,8 @@ impl FakeFs {
     pub fn read_file_sync(&self, path: impl AsRef<Path>) -> Result<Vec<u8>> {
         let path = path.as_ref();
         let path = normalize_path(path);
-        let state = self.state.lock();
-        let entry = state.read_path(&path)?;
-        let entry = entry.lock();
+        let mut state = self.state.lock();
+        let entry = state.entry(&path)?;
         entry.file_content(&path).cloned()
     }
 
@@ -1251,9 +1366,8 @@ impl FakeFs {
         let path = path.as_ref();
         let path = normalize_path(path);
         self.simulate_random_delay().await;
-        let state = self.state.lock();
-        let entry = state.read_path(&path)?;
-        let entry = entry.lock();
+        let mut state = self.state.lock();
+        let entry = state.entry(&path)?;
         entry.file_content(&path).cloned()
     }
 
@@ -1274,6 +1388,25 @@ impl FakeFs {
         self.state.lock().flush_events(count);
     }
 
+    pub(crate) fn entry(&self, target: &Path) -> Result<FakeFsEntry> {
+        self.state.lock().entry(target).cloned()
+    }
+
+    pub(crate) fn insert_entry(&self, target: &Path, new_entry: FakeFsEntry) -> Result<()> {
+        let mut state = self.state.lock();
+        state.write_path(target, |entry| {
+            match entry {
+                btree_map::Entry::Vacant(vacant_entry) => {
+                    vacant_entry.insert(new_entry);
+                }
+                btree_map::Entry::Occupied(mut occupied_entry) => {
+                    occupied_entry.insert(new_entry);
+                }
+            }
+            Ok(())
+        })
+    }
+
     #[must_use]
     pub fn insert_tree<'a>(
         &'a self,
@@ -1343,20 +1476,19 @@ impl FakeFs {
         F: FnOnce(&mut FakeGitRepositoryState, &Path, &Path) -> T,
     {
         let mut state = self.state.lock();
-        let entry = state.read_path(dot_git).context("open .git")?;
-        let mut entry = entry.lock();
+        let git_event_tx = state.git_event_tx.clone();
+        let entry = state.entry(dot_git).context("open .git")?;
 
-        if let FakeFsEntry::Dir { git_repo_state, .. } = &mut *entry {
+        if let FakeFsEntry::Dir { git_repo_state, .. } = entry {
             let repo_state = git_repo_state.get_or_insert_with(|| {
                 log::debug!("insert git state for {dot_git:?}");
-                Arc::new(Mutex::new(FakeGitRepositoryState::new(
-                    state.git_event_tx.clone(),
-                )))
+                Arc::new(Mutex::new(FakeGitRepositoryState::new(git_event_tx)))
             });
             let mut repo_state = repo_state.lock();
 
             let result = f(&mut repo_state, dot_git, dot_git);
 
+            drop(repo_state);
             if emit_git_event {
                 state.emit_event([(dot_git, None)]);
             }
@@ -1380,21 +1512,20 @@ impl FakeFs {
                 }
             }
             .clone();
-            drop(entry);
-            let Some((git_dir_entry, canonical_path)) = state.try_read_path(&path, true) else {
+            let Some((git_dir_entry, canonical_path)) = state.try_entry(&path, true) else {
                 anyhow::bail!("pointed-to git dir {path:?} not found")
             };
             let FakeFsEntry::Dir {
                 git_repo_state,
                 entries,
                 ..
-            } = &mut *git_dir_entry.lock()
+            } = git_dir_entry
             else {
                 anyhow::bail!("gitfile points to a non-directory")
             };
             let common_dir = if let Some(child) = entries.get("commondir") {
                 Path::new(
-                    std::str::from_utf8(child.lock().file_content("commondir".as_ref())?)
+                    std::str::from_utf8(child.file_content("commondir".as_ref())?)
                         .context("commondir content")?,
                 )
                 .to_owned()
@@ -1402,15 +1533,14 @@ impl FakeFs {
                 canonical_path.clone()
             };
             let repo_state = git_repo_state.get_or_insert_with(|| {
-                Arc::new(Mutex::new(FakeGitRepositoryState::new(
-                    state.git_event_tx.clone(),
-                )))
+                Arc::new(Mutex::new(FakeGitRepositoryState::new(git_event_tx)))
             });
             let mut repo_state = repo_state.lock();
 
             let result = f(&mut repo_state, &canonical_path, &common_dir);
 
             if emit_git_event {
+                drop(repo_state);
                 state.emit_event([(canonical_path, None)]);
             }
 
@@ -1438,10 +1568,10 @@ impl FakeFs {
 
     pub fn insert_branches(&self, dot_git: &Path, branches: &[&str]) {
         self.with_git_state(dot_git, true, |state| {
-            if let Some(first) = branches.first() {
-                if state.current_branch_name.is_none() {
-                    state.current_branch_name = Some(first.to_string())
-                }
+            if let Some(first) = branches.first()
+                && state.current_branch_name.is_none()
+            {
+                state.current_branch_name = Some(first.to_string())
             }
             state
                 .branches
@@ -1549,7 +1679,7 @@ impl FakeFs {
     /// by mutating the head, index, and unmerged state.
     pub fn set_status_for_repo(&self, dot_git: &Path, statuses: &[(&Path, FileStatus)]) {
         let workdir_path = dot_git.parent().unwrap();
-        let workdir_contents = self.files_with_contents(&workdir_path);
+        let workdir_contents = self.files_with_contents(workdir_path);
         self.with_git_state(dot_git, true, |state| {
             state.index_contents.clear();
             state.head_contents.clear();
@@ -1637,14 +1767,12 @@ impl FakeFs {
     pub fn paths(&self, include_dot_git: bool) -> Vec<PathBuf> {
         let mut result = Vec::new();
         let mut queue = collections::VecDeque::new();
-        queue.push_back((
-            PathBuf::from(util::path!("/")),
-            self.state.lock().root.clone(),
-        ));
+        let state = &*self.state.lock();
+        queue.push_back((PathBuf::from(util::path!("/")), &state.root));
         while let Some((path, entry)) = queue.pop_front() {
-            if let FakeFsEntry::Dir { entries, .. } = &*entry.lock() {
+            if let FakeFsEntry::Dir { entries, .. } = entry {
                 for (name, entry) in entries {
-                    queue.push_back((path.join(name), entry.clone()));
+                    queue.push_back((path.join(name), entry));
                 }
             }
             if include_dot_git
@@ -1661,14 +1789,12 @@ impl FakeFs {
     pub fn directories(&self, include_dot_git: bool) -> Vec<PathBuf> {
         let mut result = Vec::new();
         let mut queue = collections::VecDeque::new();
-        queue.push_back((
-            PathBuf::from(util::path!("/")),
-            self.state.lock().root.clone(),
-        ));
+        let state = &*self.state.lock();
+        queue.push_back((PathBuf::from(util::path!("/")), &state.root));
         while let Some((path, entry)) = queue.pop_front() {
-            if let FakeFsEntry::Dir { entries, .. } = &*entry.lock() {
+            if let FakeFsEntry::Dir { entries, .. } = entry {
                 for (name, entry) in entries {
-                    queue.push_back((path.join(name), entry.clone()));
+                    queue.push_back((path.join(name), entry));
                 }
                 if include_dot_git
                     || !path
@@ -1685,17 +1811,14 @@ impl FakeFs {
     pub fn files(&self) -> Vec<PathBuf> {
         let mut result = Vec::new();
         let mut queue = collections::VecDeque::new();
-        queue.push_back((
-            PathBuf::from(util::path!("/")),
-            self.state.lock().root.clone(),
-        ));
+        let state = &*self.state.lock();
+        queue.push_back((PathBuf::from(util::path!("/")), &state.root));
         while let Some((path, entry)) = queue.pop_front() {
-            let e = entry.lock();
-            match &*e {
+            match entry {
                 FakeFsEntry::File { .. } => result.push(path),
                 FakeFsEntry::Dir { entries, .. } => {
                     for (name, entry) in entries {
-                        queue.push_back((path.join(name), entry.clone()));
+                        queue.push_back((path.join(name), entry));
                     }
                 }
                 FakeFsEntry::Symlink { .. } => {}
@@ -1707,13 +1830,10 @@ impl FakeFs {
     pub fn files_with_contents(&self, prefix: &Path) -> Vec<(PathBuf, Vec<u8>)> {
         let mut result = Vec::new();
         let mut queue = collections::VecDeque::new();
-        queue.push_back((
-            PathBuf::from(util::path!("/")),
-            self.state.lock().root.clone(),
-        ));
+        let state = &*self.state.lock();
+        queue.push_back((PathBuf::from(util::path!("/")), &state.root));
         while let Some((path, entry)) = queue.pop_front() {
-            let e = entry.lock();
-            match &*e {
+            match entry {
                 FakeFsEntry::File { content, .. } => {
                     if path.starts_with(prefix) {
                         result.push((path, content.clone()));
@@ -1721,7 +1841,7 @@ impl FakeFs {
                 }
                 FakeFsEntry::Dir { entries, .. } => {
                     for (name, entry) in entries {
-                        queue.push_back((path.join(name), entry.clone()));
+                        queue.push_back((path.join(name), entry));
                     }
                 }
                 FakeFsEntry::Symlink { .. } => {}
@@ -1787,10 +1907,7 @@ impl FakeFsEntry {
         }
     }
 
-    fn dir_entries(
-        &mut self,
-        path: &Path,
-    ) -> Result<&mut BTreeMap<String, Arc<Mutex<FakeFsEntry>>>> {
+    fn dir_entries(&mut self, path: &Path) -> Result<&mut BTreeMap<String, FakeFsEntry>> {
         if let Self::Dir { entries, .. } = self {
             Ok(entries)
         } else {
@@ -1837,13 +1954,13 @@ struct FakeHandle {
 impl FileHandle for FakeHandle {
     fn current_path(&self, fs: &Arc<dyn Fs>) -> Result<PathBuf> {
         let fs = fs.as_fake();
-        let state = fs.state.lock();
-        let Some(target) = state.moves.get(&self.inode) else {
+        let mut state = fs.state.lock();
+        let Some(target) = state.moves.get(&self.inode).cloned() else {
             anyhow::bail!("fake fd not moved")
         };
 
-        if state.try_read_path(&target, false).is_some() {
-            return Ok(target.clone());
+        if state.try_entry(&target, false).is_some() {
+            return Ok(target);
         }
         anyhow::bail!("fake fd target not found")
     }
@@ -1870,13 +1987,13 @@ impl Fs for FakeFs {
             state.write_path(&cur_path, |entry| {
                 entry.or_insert_with(|| {
                     created_dirs.push((cur_path.clone(), Some(PathEventKind::Created)));
-                    Arc::new(Mutex::new(FakeFsEntry::Dir {
+                    FakeFsEntry::Dir {
                         inode,
                         mtime,
                         len: 0,
                         entries: Default::default(),
                         git_repo_state: None,
-                    }))
+                    }
                 });
                 Ok(())
             })?
@@ -1891,13 +2008,13 @@ impl Fs for FakeFs {
         let mut state = self.state.lock();
         let inode = state.get_and_increment_inode();
         let mtime = state.get_and_increment_mtime();
-        let file = Arc::new(Mutex::new(FakeFsEntry::File {
+        let file = FakeFsEntry::File {
             inode,
             mtime,
             len: 0,
             content: Vec::new(),
             git_dir_path: None,
-        }));
+        };
         let mut kind = Some(PathEventKind::Created);
         state.write_path(path, |entry| {
             match entry {
@@ -1921,7 +2038,7 @@ impl Fs for FakeFs {
 
     async fn create_symlink(&self, path: &Path, target: PathBuf) -> Result<()> {
         let mut state = self.state.lock();
-        let file = Arc::new(Mutex::new(FakeFsEntry::Symlink { target }));
+        let file = FakeFsEntry::Symlink { target };
         state
             .write_path(path.as_ref(), move |e| match e {
                 btree_map::Entry::Vacant(e) => {
@@ -1984,7 +2101,7 @@ impl Fs for FakeFs {
             }
         })?;
 
-        let inode = match *moved_entry.lock() {
+        let inode = match moved_entry {
             FakeFsEntry::File { inode, .. } => inode,
             FakeFsEntry::Dir { inode, .. } => inode,
             _ => 0,
@@ -2033,8 +2150,8 @@ impl Fs for FakeFs {
         let mut state = self.state.lock();
         let mtime = state.get_and_increment_mtime();
         let inode = state.get_and_increment_inode();
-        let source_entry = state.read_path(&source)?;
-        let content = source_entry.lock().file_content(&source)?.clone();
+        let source_entry = state.entry(&source)?;
+        let content = source_entry.file_content(&source)?.clone();
         let mut kind = Some(PathEventKind::Created);
         state.write_path(&target, |e| match e {
             btree_map::Entry::Occupied(e) => {
@@ -2048,13 +2165,13 @@ impl Fs for FakeFs {
                 }
             }
             btree_map::Entry::Vacant(e) => Ok(Some(
-                e.insert(Arc::new(Mutex::new(FakeFsEntry::File {
+                e.insert(FakeFsEntry::File {
                     inode,
                     mtime,
                     len: content.len() as u64,
                     content,
                     git_dir_path: None,
-                })))
+                })
                 .clone(),
             )),
         })?;
@@ -2070,8 +2187,7 @@ impl Fs for FakeFs {
         let base_name = path.file_name().context("cannot remove the root")?;
 
         let mut state = self.state.lock();
-        let parent_entry = state.read_path(parent_path)?;
-        let mut parent_entry = parent_entry.lock();
+        let parent_entry = state.entry(parent_path)?;
         let entry = parent_entry
             .dir_entries(parent_path)?
             .entry(base_name.to_str().unwrap().into());
@@ -2082,15 +2198,14 @@ impl Fs for FakeFs {
                     anyhow::bail!("{path:?} does not exist");
                 }
             }
-            btree_map::Entry::Occupied(e) => {
+            btree_map::Entry::Occupied(mut entry) => {
                 {
-                    let mut entry = e.get().lock();
-                    let children = entry.dir_entries(&path)?;
+                    let children = entry.get_mut().dir_entries(&path)?;
                     if !options.recursive && !children.is_empty() {
                         anyhow::bail!("{path:?} is not empty");
                     }
                 }
-                e.remove();
+                entry.remove();
             }
         }
         state.emit_event([(path, Some(PathEventKind::Removed))]);
@@ -2104,8 +2219,7 @@ impl Fs for FakeFs {
         let parent_path = path.parent().context("cannot remove the root")?;
         let base_name = path.file_name().unwrap();
         let mut state = self.state.lock();
-        let parent_entry = state.read_path(parent_path)?;
-        let mut parent_entry = parent_entry.lock();
+        let parent_entry = state.entry(parent_path)?;
         let entry = parent_entry
             .dir_entries(parent_path)?
             .entry(base_name.to_str().unwrap().into());
@@ -2115,9 +2229,9 @@ impl Fs for FakeFs {
                     anyhow::bail!("{path:?} does not exist");
                 }
             }
-            btree_map::Entry::Occupied(e) => {
-                e.get().lock().file_content(&path)?;
-                e.remove();
+            btree_map::Entry::Occupied(mut entry) => {
+                entry.get_mut().file_content(&path)?;
+                entry.remove();
             }
         }
         state.emit_event([(path, Some(PathEventKind::Removed))]);
@@ -2131,12 +2245,10 @@ impl Fs for FakeFs {
 
     async fn open_handle(&self, path: &Path) -> Result<Arc<dyn FileHandle>> {
         self.simulate_random_delay().await;
-        let state = self.state.lock();
-        let entry = state.read_path(&path)?;
-        let entry = entry.lock();
-        let inode = match *entry {
-            FakeFsEntry::File { inode, .. } => inode,
-            FakeFsEntry::Dir { inode, .. } => inode,
+        let mut state = self.state.lock();
+        let inode = match state.entry(path)? {
+            FakeFsEntry::File { inode, .. } => *inode,
+            FakeFsEntry::Dir { inode, .. } => *inode,
             _ => unreachable!(),
         };
         Ok(Arc::new(FakeHandle { inode }))
@@ -2144,7 +2256,7 @@ impl Fs for FakeFs {
 
     async fn load(&self, path: &Path) -> Result<String> {
         let content = self.load_internal(path).await?;
-        Ok(String::from_utf8(content.clone())?)
+        Ok(String::from_utf8(content)?)
     }
 
     async fn load_bytes(&self, path: &Path) -> Result<Vec<u8>> {
@@ -2154,6 +2266,9 @@ impl Fs for FakeFs {
     async fn atomic_write(&self, path: PathBuf, data: String) -> Result<()> {
         self.simulate_random_delay().await;
         let path = normalize_path(path.as_path());
+        if let Some(path) = path.parent() {
+            self.create_dir(path).await?;
+        }
         self.write_file_internal(path, data.into_bytes(), true)?;
         Ok(())
     }
@@ -2183,8 +2298,8 @@ impl Fs for FakeFs {
         let path = normalize_path(path);
         self.simulate_random_delay().await;
         let state = self.state.lock();
-        let (_, canonical_path) = state
-            .try_read_path(&path, true)
+        let canonical_path = state
+            .canonicalize(&path, true)
             .with_context(|| format!("path does not exist: {path:?}"))?;
         Ok(canonical_path)
     }
@@ -2192,9 +2307,9 @@ impl Fs for FakeFs {
     async fn is_file(&self, path: &Path) -> bool {
         let path = normalize_path(path);
         self.simulate_random_delay().await;
-        let state = self.state.lock();
-        if let Some((entry, _)) = state.try_read_path(&path, true) {
-            entry.lock().is_file()
+        let mut state = self.state.lock();
+        if let Some((entry, _)) = state.try_entry(&path, true) {
+            entry.is_file()
         } else {
             false
         }
@@ -2211,17 +2326,16 @@ impl Fs for FakeFs {
         let path = normalize_path(path);
         let mut state = self.state.lock();
         state.metadata_call_count += 1;
-        if let Some((mut entry, _)) = state.try_read_path(&path, false) {
-            let is_symlink = entry.lock().is_symlink();
+        if let Some((mut entry, _)) = state.try_entry(&path, false) {
+            let is_symlink = entry.is_symlink();
             if is_symlink {
-                if let Some(e) = state.try_read_path(&path, true).map(|e| e.0) {
+                if let Some(e) = state.try_entry(&path, true).map(|e| e.0) {
                     entry = e;
                 } else {
                     return Ok(None);
                 }
             }
 
-            let entry = entry.lock();
             Ok(Some(match &*entry {
                 FakeFsEntry::File {
                     inode, mtime, len, ..
@@ -2253,12 +2367,11 @@ impl Fs for FakeFs {
     async fn read_link(&self, path: &Path) -> Result<PathBuf> {
         self.simulate_random_delay().await;
         let path = normalize_path(path);
-        let state = self.state.lock();
+        let mut state = self.state.lock();
         let (entry, _) = state
-            .try_read_path(&path, false)
+            .try_entry(&path, false)
             .with_context(|| format!("path does not exist: {path:?}"))?;
-        let entry = entry.lock();
-        if let FakeFsEntry::Symlink { target } = &*entry {
+        if let FakeFsEntry::Symlink { target } = entry {
             Ok(target.clone())
         } else {
             anyhow::bail!("not a symlink: {path:?}")
@@ -2273,8 +2386,7 @@ impl Fs for FakeFs {
         let path = normalize_path(path);
         let mut state = self.state.lock();
         state.read_dir_call_count += 1;
-        let entry = state.read_path(&path)?;
-        let mut entry = entry.lock();
+        let entry = state.entry(&path)?;
         let children = entry.dir_entries(&path)?;
         let paths = children
             .keys()
@@ -2300,19 +2412,18 @@ impl Fs for FakeFs {
             tx,
             original_path: path.to_owned(),
             fs_state: self.state.clone(),
-            prefixes: Mutex::new(vec![path.to_owned()]),
+            prefixes: Mutex::new(vec![path]),
         });
         (
             Box::pin(futures::StreamExt::filter(rx, {
                 let watcher = watcher.clone();
                 move |events| {
                     let result = events.iter().any(|evt_path| {
-                        let result = watcher
+                        watcher
                             .prefixes
                             .lock()
                             .iter()
-                            .any(|prefix| evt_path.path.starts_with(prefix));
-                        result
+                            .any(|prefix| evt_path.path.starts_with(prefix))
                     });
                     let executor = executor.clone();
                     async move {
@@ -2338,6 +2449,7 @@ impl Fs for FakeFs {
                     dot_git_path: abs_dot_git.to_path_buf(),
                     repository_dir_path: repository_dir_path.to_owned(),
                     common_dir_path: common_dir_path.to_owned(),
+                    checkpoints: Arc::default(),
                 }) as _
             },
         )
@@ -2352,6 +2464,10 @@ impl Fs for FakeFs {
         smol::block_on(self.create_dir(&abs_work_directory_path.join(".git")))
     }
 
+    async fn git_clone(&self, _repo_url: &str, _abs_work_directory: &Path) -> Result<()> {
+        anyhow::bail!("Git clone is not supported in fake Fs")
+    }
+
     fn is_fake(&self) -> bool {
         true
     }

crates/fs/src/fs_watcher.rs 🔗

@@ -1,6 +1,9 @@
 use notify::EventKind;
 use parking_lot::Mutex;
-use std::sync::{Arc, OnceLock};
+use std::{
+    collections::HashMap,
+    sync::{Arc, OnceLock},
+};
 use util::{ResultExt, paths::SanitizedPath};
 
 use crate::{PathEvent, PathEventKind, Watcher};
@@ -8,6 +11,7 @@ use crate::{PathEvent, PathEventKind, Watcher};
 pub struct FsWatcher {
     tx: smol::channel::Sender<()>,
     pending_path_events: Arc<Mutex<Vec<PathEvent>>>,
+    registrations: Mutex<HashMap<Arc<std::path::Path>, WatcherRegistrationId>>,
 }
 
 impl FsWatcher {
@@ -18,10 +22,24 @@ impl FsWatcher {
         Self {
             tx,
             pending_path_events,
+            registrations: Default::default(),
         }
     }
 }
 
+impl Drop for FsWatcher {
+    fn drop(&mut self) {
+        let mut registrations = self.registrations.lock();
+        let registrations = registrations.drain();
+
+        let _ = global(|g| {
+            for (_, registration) in registrations {
+                g.remove(registration);
+            }
+        });
+    }
+}
+
 impl Watcher for FsWatcher {
     fn add(&self, path: &std::path::Path) -> anyhow::Result<()> {
         let root_path = SanitizedPath::from(path);
@@ -29,75 +47,143 @@ impl Watcher for FsWatcher {
         let tx = self.tx.clone();
         let pending_paths = self.pending_path_events.clone();
 
-        use notify::Watcher;
+        let path: Arc<std::path::Path> = path.into();
+
+        if self.registrations.lock().contains_key(&path) {
+            return Ok(());
+        }
 
-        global({
+        let registration_id = global({
+            let path = path.clone();
             |g| {
-                g.add(move |event: &notify::Event| {
-                    let kind = match event.kind {
-                        EventKind::Create(_) => Some(PathEventKind::Created),
-                        EventKind::Modify(_) => Some(PathEventKind::Changed),
-                        EventKind::Remove(_) => Some(PathEventKind::Removed),
-                        _ => None,
-                    };
-                    let mut path_events = event
-                        .paths
-                        .iter()
-                        .filter_map(|event_path| {
-                            let event_path = SanitizedPath::from(event_path);
-                            event_path.starts_with(&root_path).then(|| PathEvent {
-                                path: event_path.as_path().to_path_buf(),
-                                kind,
+                g.add(
+                    path,
+                    notify::RecursiveMode::NonRecursive,
+                    move |event: &notify::Event| {
+                        let kind = match event.kind {
+                            EventKind::Create(_) => Some(PathEventKind::Created),
+                            EventKind::Modify(_) => Some(PathEventKind::Changed),
+                            EventKind::Remove(_) => Some(PathEventKind::Removed),
+                            _ => None,
+                        };
+                        let mut path_events = event
+                            .paths
+                            .iter()
+                            .filter_map(|event_path| {
+                                let event_path = SanitizedPath::from(event_path);
+                                event_path.starts_with(&root_path).then(|| PathEvent {
+                                    path: event_path.as_path().to_path_buf(),
+                                    kind,
+                                })
                             })
-                        })
-                        .collect::<Vec<_>>();
-
-                    if !path_events.is_empty() {
-                        path_events.sort();
-                        let mut pending_paths = pending_paths.lock();
-                        if pending_paths.is_empty() {
-                            tx.try_send(()).ok();
+                            .collect::<Vec<_>>();
+
+                        if !path_events.is_empty() {
+                            path_events.sort();
+                            let mut pending_paths = pending_paths.lock();
+                            if pending_paths.is_empty() {
+                                tx.try_send(()).ok();
+                            }
+                            util::extend_sorted(
+                                &mut *pending_paths,
+                                path_events,
+                                usize::MAX,
+                                |a, b| a.path.cmp(&b.path),
+                            );
                         }
-                        util::extend_sorted(
-                            &mut *pending_paths,
-                            path_events,
-                            usize::MAX,
-                            |a, b| a.path.cmp(&b.path),
-                        );
-                    }
-                })
+                    },
+                )
             }
-        })?;
-
-        global(|g| {
-            g.watcher
-                .lock()
-                .watch(path, notify::RecursiveMode::NonRecursive)
         })??;
 
+        self.registrations.lock().insert(path, registration_id);
+
         Ok(())
     }
 
     fn remove(&self, path: &std::path::Path) -> anyhow::Result<()> {
-        use notify::Watcher;
-        Ok(global(|w| w.watcher.lock().unwatch(path))??)
+        let Some(registration) = self.registrations.lock().remove(path) else {
+            return Ok(());
+        };
+
+        global(|w| w.remove(registration))
     }
 }
 
+#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash)]
+pub struct WatcherRegistrationId(u32);
+
+struct WatcherRegistrationState {
+    callback: Arc<dyn Fn(&notify::Event) + Send + Sync>,
+    path: Arc<std::path::Path>,
+}
+
+struct WatcherState {
+    watchers: HashMap<WatcherRegistrationId, WatcherRegistrationState>,
+    path_registrations: HashMap<Arc<std::path::Path>, u32>,
+    last_registration: WatcherRegistrationId,
+}
+
 pub struct GlobalWatcher {
+    state: Mutex<WatcherState>,
+
+    // DANGER: never keep the state lock while holding the watcher lock
     // two mutexes because calling watcher.add triggers an watcher.event, which needs watchers.
     #[cfg(target_os = "linux")]
-    pub(super) watcher: Mutex<notify::INotifyWatcher>,
+    watcher: Mutex<notify::INotifyWatcher>,
     #[cfg(target_os = "freebsd")]
-    pub(super) watcher: Mutex<notify::KqueueWatcher>,
+    watcher: Mutex<notify::KqueueWatcher>,
     #[cfg(target_os = "windows")]
-    pub(super) watcher: Mutex<notify::ReadDirectoryChangesWatcher>,
-    pub(super) watchers: Mutex<Vec<Box<dyn Fn(&notify::Event) + Send + Sync>>>,
+    watcher: Mutex<notify::ReadDirectoryChangesWatcher>,
 }
 
 impl GlobalWatcher {
-    pub(super) fn add(&self, cb: impl Fn(&notify::Event) + Send + Sync + 'static) {
-        self.watchers.lock().push(Box::new(cb))
+    #[must_use]
+    fn add(
+        &self,
+        path: Arc<std::path::Path>,
+        mode: notify::RecursiveMode,
+        cb: impl Fn(&notify::Event) + Send + Sync + 'static,
+    ) -> anyhow::Result<WatcherRegistrationId> {
+        use notify::Watcher;
+
+        self.watcher.lock().watch(&path, mode)?;
+
+        let mut state = self.state.lock();
+
+        let id = state.last_registration;
+        state.last_registration = WatcherRegistrationId(id.0 + 1);
+
+        let registration_state = WatcherRegistrationState {
+            callback: Arc::new(cb),
+            path: path.clone(),
+        };
+        state.watchers.insert(id, registration_state);
+        *state.path_registrations.entry(path).or_insert(0) += 1;
+
+        Ok(id)
+    }
+
+    pub fn remove(&self, id: WatcherRegistrationId) {
+        use notify::Watcher;
+        let mut state = self.state.lock();
+        let Some(registration_state) = state.watchers.remove(&id) else {
+            return;
+        };
+
+        let Some(count) = state.path_registrations.get_mut(&registration_state.path) else {
+            return;
+        };
+        *count -= 1;
+        if *count == 0 {
+            state.path_registrations.remove(&registration_state.path);
+
+            drop(state);
+            self.watcher
+                .lock()
+                .unwatch(&registration_state.path)
+                .log_err();
+        }
     }
 }
 
@@ -114,8 +200,16 @@ fn handle_event(event: Result<notify::Event, notify::Error>) {
         return;
     };
     global::<()>(move |watcher| {
-        for f in watcher.watchers.lock().iter() {
-            f(&event)
+        let callbacks = {
+            let state = watcher.state.lock();
+            state
+                .watchers
+                .values()
+                .map(|r| r.callback.clone())
+                .collect::<Vec<_>>()
+        };
+        for callback in callbacks {
+            callback(&event);
         }
     })
     .log_err();
@@ -124,8 +218,12 @@ fn handle_event(event: Result<notify::Event, notify::Error>) {
 pub fn global<T>(f: impl FnOnce(&GlobalWatcher) -> T) -> anyhow::Result<T> {
     let result = FS_WATCHER_INSTANCE.get_or_init(|| {
         notify::recommended_watcher(handle_event).map(|file_watcher| GlobalWatcher {
+            state: Mutex::new(WatcherState {
+                watchers: Default::default(),
+                path_registrations: Default::default(),
+                last_registration: Default::default(),
+            }),
             watcher: Mutex::new(file_watcher),
-            watchers: Default::default(),
         })
     });
     match result {

crates/fs/src/mac_watcher.rs 🔗

@@ -41,10 +41,9 @@ impl Watcher for MacWatcher {
         if let Some((watched_path, _)) = handles
             .range::<Path, _>((Bound::Unbounded, Bound::Included(path)))
             .next_back()
+            && path.starts_with(watched_path)
         {
-            if path.starts_with(watched_path) {
-                return Ok(());
-            }
+            return Ok(());
         }
 
         let (stream, handle) = EventStream::new(&[path], self.latency);

crates/fsevent/src/fsevent.rs 🔗

@@ -178,40 +178,39 @@ impl EventStream {
                     flags.contains(StreamFlags::USER_DROPPED)
                         || flags.contains(StreamFlags::KERNEL_DROPPED)
                 })
+                && let Some(last_valid_event_id) = state.last_valid_event_id.take()
             {
-                if let Some(last_valid_event_id) = state.last_valid_event_id.take() {
-                    fs::FSEventStreamStop(state.stream);
-                    fs::FSEventStreamInvalidate(state.stream);
-                    fs::FSEventStreamRelease(state.stream);
-
-                    let stream_context = fs::FSEventStreamContext {
-                        version: 0,
-                        info,
-                        retain: None,
-                        release: None,
-                        copy_description: None,
-                    };
-                    let stream = fs::FSEventStreamCreate(
-                        cf::kCFAllocatorDefault,
-                        Self::trampoline,
-                        &stream_context,
-                        state.paths,
-                        last_valid_event_id,
-                        state.latency.as_secs_f64(),
-                        fs::kFSEventStreamCreateFlagFileEvents
-                            | fs::kFSEventStreamCreateFlagNoDefer
-                            | fs::kFSEventStreamCreateFlagWatchRoot,
-                    );
-
-                    state.stream = stream;
-                    fs::FSEventStreamScheduleWithRunLoop(
-                        state.stream,
-                        cf::CFRunLoopGetCurrent(),
-                        cf::kCFRunLoopDefaultMode,
-                    );
-                    fs::FSEventStreamStart(state.stream);
-                    stream_restarted = true;
-                }
+                fs::FSEventStreamStop(state.stream);
+                fs::FSEventStreamInvalidate(state.stream);
+                fs::FSEventStreamRelease(state.stream);
+
+                let stream_context = fs::FSEventStreamContext {
+                    version: 0,
+                    info,
+                    retain: None,
+                    release: None,
+                    copy_description: None,
+                };
+                let stream = fs::FSEventStreamCreate(
+                    cf::kCFAllocatorDefault,
+                    Self::trampoline,
+                    &stream_context,
+                    state.paths,
+                    last_valid_event_id,
+                    state.latency.as_secs_f64(),
+                    fs::kFSEventStreamCreateFlagFileEvents
+                        | fs::kFSEventStreamCreateFlagNoDefer
+                        | fs::kFSEventStreamCreateFlagWatchRoot,
+                );
+
+                state.stream = stream;
+                fs::FSEventStreamScheduleWithRunLoop(
+                    state.stream,
+                    cf::CFRunLoopGetCurrent(),
+                    cf::kCFRunLoopDefaultMode,
+                );
+                fs::FSEventStreamStart(state.stream);
+                stream_restarted = true;
             }
 
             if !stream_restarted {

crates/git/Cargo.toml 🔗

@@ -12,7 +12,7 @@ workspace = true
 path = "src/git.rs"
 
 [features]
-test-support = []
+test-support = ["rand"]
 
 [dependencies]
 anyhow.workspace = true
@@ -26,6 +26,7 @@ http_client.workspace = true
 log.workspace = true
 parking_lot.workspace = true
 regex.workspace = true
+rand = { workspace = true, optional = true }
 rope.workspace = true
 schemars.workspace = true
 serde.workspace = true
@@ -47,3 +48,4 @@ text = { workspace = true, features = ["test-support"] }
 unindent.workspace = true
 gpui = { workspace = true, features = ["test-support"] }
 tempfile.workspace = true
+rand.workspace = true

crates/git/src/blame.rs 🔗

@@ -73,6 +73,7 @@ async fn run_git_blame(
         .current_dir(working_directory)
         .arg("blame")
         .arg("--incremental")
+        .arg("-w")
         .arg("--contents")
         .arg("-")
         .arg(path.as_os_str())
@@ -288,14 +289,12 @@ fn parse_git_blame(output: &str) -> Result<Vec<BlameEntry>> {
             }
         };
 
-        if done {
-            if let Some(entry) = current_entry.take() {
-                index.insert(entry.sha, entries.len());
+        if done && let Some(entry) = current_entry.take() {
+            index.insert(entry.sha, entries.len());
 
-                // We only want annotations that have a commit.
-                if !entry.sha.is_zero() {
-                    entries.push(entry);
-                }
+            // We only want annotations that have a commit.
+            if !entry.sha.is_zero() {
+                entries.push(entry);
             }
         }
     }

crates/git/src/git.rs 🔗

@@ -93,6 +93,8 @@ actions!(
         Init,
         /// Opens all modified files in the editor.
         OpenModifiedFiles,
+        /// Clones a repository.
+        Clone,
     ]
 );
 
@@ -117,6 +119,13 @@ impl Oid {
         Ok(Self(oid))
     }
 
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn random(rng: &mut impl rand::Rng) -> Self {
+        let mut bytes = [0; 20];
+        rng.fill(&mut bytes);
+        Self::from_bytes(&bytes).unwrap()
+    }
+
     pub fn as_bytes(&self) -> &[u8] {
         self.0.as_bytes()
     }

crates/git/src/repository.rs 🔗

@@ -6,7 +6,7 @@ use collections::HashMap;
 use futures::future::BoxFuture;
 use futures::{AsyncWriteExt, FutureExt as _, select_biased};
 use git2::BranchType;
-use gpui::{AppContext as _, AsyncApp, BackgroundExecutor, SharedString};
+use gpui::{AppContext as _, AsyncApp, BackgroundExecutor, SharedString, Task};
 use parking_lot::Mutex;
 use rope::Rope;
 use schemars::JsonSchema;
@@ -269,10 +269,8 @@ impl GitExcludeOverride {
     pub async fn restore_original(&mut self) -> Result<()> {
         if let Some(ref original) = self.original_excludes {
             smol::fs::write(&self.git_exclude_path, original).await?;
-        } else {
-            if self.git_exclude_path.exists() {
-                smol::fs::remove_file(&self.git_exclude_path).await?;
-            }
+        } else if self.git_exclude_path.exists() {
+            smol::fs::remove_file(&self.git_exclude_path).await?;
         }
 
         self.added_excludes = None;
@@ -338,7 +336,7 @@ pub trait GitRepository: Send + Sync {
 
     fn merge_message(&self) -> BoxFuture<'_, Option<String>>;
 
-    fn status(&self, path_prefixes: &[RepoPath]) -> BoxFuture<'_, Result<GitStatus>>;
+    fn status(&self, path_prefixes: &[RepoPath]) -> Task<Result<GitStatus>>;
 
     fn branches(&self) -> BoxFuture<'_, Result<Vec<Branch>>>;
 
@@ -399,9 +397,9 @@ pub trait GitRepository: Send + Sync {
         &self,
         paths: Vec<RepoPath>,
         env: Arc<HashMap<String, String>>,
-    ) -> BoxFuture<Result<()>>;
+    ) -> BoxFuture<'_, Result<()>>;
 
-    fn stash_pop(&self, env: Arc<HashMap<String, String>>) -> BoxFuture<Result<()>>;
+    fn stash_pop(&self, env: Arc<HashMap<String, String>>) -> BoxFuture<'_, Result<()>>;
 
     fn push(
         &self,
@@ -858,7 +856,7 @@ impl GitRepository for RealGitRepository {
                     let output = new_smol_command(&git_binary_path)
                         .current_dir(&working_directory)
                         .envs(env.iter())
-                        .args(["update-index", "--add", "--cacheinfo", "100644", &sha])
+                        .args(["update-index", "--add", "--cacheinfo", "100644", sha])
                         .arg(path.to_unix_style())
                         .output()
                         .await?;
@@ -918,7 +916,7 @@ impl GitRepository for RealGitRepository {
                     .context("no stdin for git cat-file subprocess")?;
                 let mut stdin = BufWriter::new(stdin);
                 for rev in &revs {
-                    write!(&mut stdin, "{rev}\n")?;
+                    writeln!(&mut stdin, "{rev}")?;
                 }
                 stdin.flush()?;
                 drop(stdin);
@@ -953,25 +951,27 @@ impl GitRepository for RealGitRepository {
             .boxed()
     }
 
-    fn status(&self, path_prefixes: &[RepoPath]) -> BoxFuture<'_, Result<GitStatus>> {
+    fn status(&self, path_prefixes: &[RepoPath]) -> Task<Result<GitStatus>> {
         let git_binary_path = self.git_binary_path.clone();
-        let working_directory = self.working_directory();
-        let path_prefixes = path_prefixes.to_owned();
-        self.executor
-            .spawn(async move {
-                let output = new_std_command(&git_binary_path)
-                    .current_dir(working_directory?)
-                    .args(git_status_args(&path_prefixes))
-                    .output()?;
-                if output.status.success() {
-                    let stdout = String::from_utf8_lossy(&output.stdout);
-                    stdout.parse()
-                } else {
-                    let stderr = String::from_utf8_lossy(&output.stderr);
-                    anyhow::bail!("git status failed: {stderr}");
-                }
-            })
-            .boxed()
+        let working_directory = match self.working_directory() {
+            Ok(working_directory) => working_directory,
+            Err(e) => return Task::ready(Err(e)),
+        };
+        let args = git_status_args(path_prefixes);
+        log::debug!("Checking for git status in {path_prefixes:?}");
+        self.executor.spawn(async move {
+            let output = new_std_command(&git_binary_path)
+                .current_dir(working_directory)
+                .args(args)
+                .output()?;
+            if output.status.success() {
+                let stdout = String::from_utf8_lossy(&output.stdout);
+                stdout.parse()
+            } else {
+                let stderr = String::from_utf8_lossy(&output.stderr);
+                anyhow::bail!("git status failed: {stderr}");
+            }
+        })
     }
 
     fn branches(&self) -> BoxFuture<'_, Result<Vec<Branch>>> {
@@ -1054,7 +1054,7 @@ impl GitRepository for RealGitRepository {
                 let (_, branch_name) = name.split_once("/").context("Unexpected branch format")?;
                 let revision = revision.get();
                 let branch_commit = revision.peel_to_commit()?;
-                let mut branch = repo.branch(&branch_name, &branch_commit, false)?;
+                let mut branch = repo.branch(branch_name, &branch_commit, false)?;
                 branch.set_upstream(Some(&name))?;
                 branch
             } else {
@@ -1203,7 +1203,7 @@ impl GitRepository for RealGitRepository {
         &self,
         paths: Vec<RepoPath>,
         env: Arc<HashMap<String, String>>,
-    ) -> BoxFuture<Result<()>> {
+    ) -> BoxFuture<'_, Result<()>> {
         let working_directory = self.working_directory();
         self.executor
             .spawn(async move {
@@ -1227,7 +1227,7 @@ impl GitRepository for RealGitRepository {
             .boxed()
     }
 
-    fn stash_pop(&self, env: Arc<HashMap<String, String>>) -> BoxFuture<Result<()>> {
+    fn stash_pop(&self, env: Arc<HashMap<String, String>>) -> BoxFuture<'_, Result<()>> {
         let working_directory = self.working_directory();
         self.executor
             .spawn(async move {
@@ -1445,12 +1445,11 @@ impl GitRepository for RealGitRepository {
 
                 let mut remote_branches = vec![];
                 let mut add_if_matching = async |remote_head: &str| {
-                    if let Ok(merge_base) = git_cmd(&["merge-base", &head, remote_head]).await {
-                        if merge_base.trim() == head {
-                            if let Some(s) = remote_head.strip_prefix("refs/remotes/") {
-                                remote_branches.push(s.to_owned().into());
-                            }
-                        }
+                    if let Ok(merge_base) = git_cmd(&["merge-base", &head, remote_head]).await
+                        && merge_base.trim() == head
+                        && let Some(s) = remote_head.strip_prefix("refs/remotes/")
+                    {
+                        remote_branches.push(s.to_owned().into());
                     }
                 };
 
@@ -1572,10 +1571,9 @@ impl GitRepository for RealGitRepository {
                     Err(error) => {
                         if let Some(GitBinaryCommandError { status, .. }) =
                             error.downcast_ref::<GitBinaryCommandError>()
+                            && status.code() == Some(1)
                         {
-                            if status.code() == Some(1) {
-                                return Ok(false);
-                            }
+                            return Ok(false);
                         }
 
                         Err(error)
@@ -2030,7 +2028,7 @@ fn parse_branch_input(input: &str) -> Result<Vec<Branch>> {
 
         branches.push(Branch {
             is_head: is_current_branch,
-            ref_name: ref_name,
+            ref_name,
             most_recent_commit: Some(CommitSummary {
                 sha: head_sha,
                 subject,
@@ -2052,7 +2050,7 @@ fn parse_branch_input(input: &str) -> Result<Vec<Branch>> {
 }
 
 fn parse_upstream_track(upstream_track: &str) -> Result<UpstreamTracking> {
-    if upstream_track == "" {
+    if upstream_track.is_empty() {
         return Ok(UpstreamTracking::Tracked(UpstreamTrackingStatus {
             ahead: 0,
             behind: 0,
@@ -2347,7 +2345,7 @@ mod tests {
         #[allow(clippy::octal_escapes)]
         let input = "*\0060964da10574cd9bf06463a53bf6e0769c5c45e\0\0refs/heads/zed-patches\0refs/remotes/origin/zed-patches\0\01733187470\0generated protobuf\n";
         assert_eq!(
-            parse_branch_input(&input).unwrap(),
+            parse_branch_input(input).unwrap(),
             vec![Branch {
                 is_head: true,
                 ref_name: "refs/heads/zed-patches".into(),

crates/git/src/status.rs 🔗

@@ -153,17 +153,11 @@ impl FileStatus {
     }
 
     pub fn is_conflicted(self) -> bool {
-        match self {
-            FileStatus::Unmerged { .. } => true,
-            _ => false,
-        }
+        matches!(self, FileStatus::Unmerged { .. })
     }
 
     pub fn is_ignored(self) -> bool {
-        match self {
-            FileStatus::Ignored => true,
-            _ => false,
-        }
+        matches!(self, FileStatus::Ignored)
     }
 
     pub fn has_changes(&self) -> bool {
@@ -176,40 +170,31 @@ impl FileStatus {
 
     pub fn is_modified(self) -> bool {
         match self {
-            FileStatus::Tracked(tracked) => match (tracked.index_status, tracked.worktree_status) {
-                (StatusCode::Modified, _) | (_, StatusCode::Modified) => true,
-                _ => false,
-            },
+            FileStatus::Tracked(tracked) => matches!(
+                (tracked.index_status, tracked.worktree_status),
+                (StatusCode::Modified, _) | (_, StatusCode::Modified)
+            ),
             _ => false,
         }
     }
 
     pub fn is_created(self) -> bool {
         match self {
-            FileStatus::Tracked(tracked) => match (tracked.index_status, tracked.worktree_status) {
-                (StatusCode::Added, _) | (_, StatusCode::Added) => true,
-                _ => false,
-            },
+            FileStatus::Tracked(tracked) => matches!(
+                (tracked.index_status, tracked.worktree_status),
+                (StatusCode::Added, _) | (_, StatusCode::Added)
+            ),
             FileStatus::Untracked => true,
             _ => false,
         }
     }
 
     pub fn is_deleted(self) -> bool {
-        match self {
-            FileStatus::Tracked(tracked) => match (tracked.index_status, tracked.worktree_status) {
-                (StatusCode::Deleted, _) | (_, StatusCode::Deleted) => true,
-                _ => false,
-            },
-            _ => false,
-        }
+        matches!(self, FileStatus::Tracked(tracked) if matches!((tracked.index_status, tracked.worktree_status), (StatusCode::Deleted, _) | (_, StatusCode::Deleted)))
     }
 
     pub fn is_untracked(self) -> bool {
-        match self {
-            FileStatus::Untracked => true,
-            _ => false,
-        }
+        matches!(self, FileStatus::Untracked)
     }
 
     pub fn summary(self) -> GitSummary {
@@ -468,7 +453,7 @@ impl FromStr for GitStatus {
                 Some((path, status))
             })
             .collect::<Vec<_>>();
-        entries.sort_unstable_by(|(a, _), (b, _)| a.cmp(&b));
+        entries.sort_unstable_by(|(a, _), (b, _)| a.cmp(b));
         // When a file exists in HEAD, is deleted in the index, and exists again in the working copy,
         // git produces two lines for it, one reading `D ` (deleted in index, unmodified in working copy)
         // and the other reading `??` (untracked). Merge these two into the equivalent of `DA`.

crates/git_hosting_providers/src/git_hosting_providers.rs 🔗

@@ -49,13 +49,13 @@ pub fn register_additional_providers(
 
 pub fn get_host_from_git_remote_url(remote_url: &str) -> Result<String> {
     maybe!({
-        if let Some(remote_url) = remote_url.strip_prefix("git@") {
-            if let Some((host, _)) = remote_url.trim_start_matches("git@").split_once(':') {
-                return Some(host.to_string());
-            }
+        if let Some(remote_url) = remote_url.strip_prefix("git@")
+            && let Some((host, _)) = remote_url.trim_start_matches("git@").split_once(':')
+        {
+            return Some(host.to_string());
         }
 
-        Url::parse(&remote_url)
+        Url::parse(remote_url)
             .ok()
             .and_then(|remote_url| remote_url.host_str().map(|host| host.to_string()))
     })

crates/git_hosting_providers/src/providers/github.rs 🔗

@@ -474,7 +474,7 @@ mod tests {
 
         assert_eq!(
             github
-                .extract_pull_request(&remote, &message)
+                .extract_pull_request(&remote, message)
                 .unwrap()
                 .url
                 .as_str(),
@@ -488,6 +488,6 @@ mod tests {
             See the original PR, this is a fix.
             "#
         };
-        assert_eq!(github.extract_pull_request(&remote, &message), None);
+        assert_eq!(github.extract_pull_request(&remote, message), None);
     }
 }

crates/git_ui/src/blame_ui.rs 🔗

@@ -172,7 +172,7 @@ impl BlameRenderer for GitBlameRenderer {
                 .clone()
                 .unwrap_or("<no name>".to_string())
                 .into(),
-            author_email: blame.author_mail.clone().unwrap_or("".to_string()).into(),
+            author_email: blame.author_mail.unwrap_or("".to_string()).into(),
             message: details,
         };
 
@@ -186,7 +186,7 @@ impl BlameRenderer for GitBlameRenderer {
             .get(0..8)
             .map(|sha| sha.to_string().into())
             .unwrap_or_else(|| commit_details.sha.clone());
-        let full_sha = commit_details.sha.to_string().clone();
+        let full_sha = commit_details.sha.to_string();
         let absolute_timestamp = format_local_timestamp(
             commit_details.commit_time,
             OffsetDateTime::now_utc(),
@@ -377,7 +377,7 @@ impl BlameRenderer for GitBlameRenderer {
                 has_parent: true,
             },
             repository.downgrade(),
-            workspace.clone(),
+            workspace,
             window,
             cx,
         )

crates/git_ui/src/branch_picker.rs 🔗

@@ -48,7 +48,7 @@ pub fn open(
     window: &mut Window,
     cx: &mut Context<Workspace>,
 ) {
-    let repository = workspace.project().read(cx).active_repository(cx).clone();
+    let repository = workspace.project().read(cx).active_repository(cx);
     let style = BranchListStyle::Modal;
     workspace.toggle_modal(window, cx, |window, cx| {
         BranchList::new(repository, style, rems(34.), window, cx)
@@ -144,7 +144,7 @@ impl BranchList {
         })
         .detach_and_log_err(cx);
 
-        let delegate = BranchListDelegate::new(repository.clone(), style);
+        let delegate = BranchListDelegate::new(repository, style);
         let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
 
         let _subscription = cx.subscribe(&picker, |_, _, _, cx| {
@@ -473,7 +473,7 @@ impl PickerDelegate for BranchListDelegate {
             && entry.is_new
         {
             Some(
-                IconButton::new("branch-from-default", IconName::GitBranchSmall)
+                IconButton::new("branch-from-default", IconName::GitBranchAlt)
                     .on_click(cx.listener(move |this, _, window, cx| {
                         this.delegate.set_selected_index(ix, window, cx);
                         this.delegate.confirm(true, window, cx);

crates/git_ui/src/commit_modal.rs 🔗

@@ -35,7 +35,7 @@ impl ModalContainerProperties {
 
         // Calculate width based on character width
         let mut modal_width = 460.0;
-        let style = window.text_style().clone();
+        let style = window.text_style();
         let font_id = window.text_system().resolve_font(&style.font());
         let font_size = style.font_size.to_pixels(window.rem_size());
 
@@ -135,11 +135,10 @@ impl CommitModal {
                             .as_ref()
                             .and_then(|repo| repo.read(cx).head_commit.as_ref())
                             .is_some()
+                            && !git_panel.amend_pending()
                         {
-                            if !git_panel.amend_pending() {
-                                git_panel.set_amend_pending(true, cx);
-                                git_panel.load_last_commit_message_if_empty(cx);
-                            }
+                            git_panel.set_amend_pending(true, cx);
+                            git_panel.load_last_commit_message_if_empty(cx);
                         }
                     }
                     ForceMode::Commit => {
@@ -180,7 +179,7 @@ impl CommitModal {
 
         let commit_editor = git_panel.update(cx, |git_panel, cx| {
             git_panel.set_modal_open(true, cx);
-            let buffer = git_panel.commit_message_buffer(cx).clone();
+            let buffer = git_panel.commit_message_buffer(cx);
             let panel_editor = git_panel.commit_editor.clone();
             let project = git_panel.project.clone();
 
@@ -195,12 +194,12 @@ impl CommitModal {
 
         let commit_message = commit_editor.read(cx).text(cx);
 
-        if let Some(suggested_commit_message) = suggested_commit_message {
-            if commit_message.is_empty() {
-                commit_editor.update(cx, |editor, cx| {
-                    editor.set_placeholder_text(suggested_commit_message, cx);
-                });
-            }
+        if let Some(suggested_commit_message) = suggested_commit_message
+            && commit_message.is_empty()
+        {
+            commit_editor.update(cx, |editor, cx| {
+                editor.set_placeholder_text(suggested_commit_message, cx);
+            });
         }
 
         let focus_handle = commit_editor.focus_handle(cx);
@@ -272,7 +271,7 @@ impl CommitModal {
                     .child(
                         div()
                             .px_1()
-                            .child(Icon::new(IconName::ChevronDownSmall).size(IconSize::XSmall)),
+                            .child(Icon::new(IconName::ChevronDown).size(IconSize::XSmall)),
                     ),
             )
             .menu({
@@ -286,7 +285,7 @@ impl CommitModal {
                     Some(ContextMenu::build(window, cx, |context_menu, _, _| {
                         context_menu
                             .when_some(keybinding_target.clone(), |el, keybinding_target| {
-                                el.context(keybinding_target.clone())
+                                el.context(keybinding_target)
                             })
                             .when(has_previous_commit, |this| {
                                 this.toggleable_entry(
@@ -392,15 +391,9 @@ impl CommitModal {
             });
         let focus_handle = self.focus_handle(cx);
 
-        let close_kb_hint =
-            if let Some(close_kb) = ui::KeyBinding::for_action(&menu::Cancel, window, cx) {
-                Some(
-                    KeybindingHint::new(close_kb, cx.theme().colors().editor_background)
-                        .suffix("Cancel"),
-                )
-            } else {
-                None
-            };
+        let close_kb_hint = ui::KeyBinding::for_action(&menu::Cancel, window, cx).map(|close_kb| {
+            KeybindingHint::new(close_kb, cx.theme().colors().editor_background).suffix("Cancel")
+        });
 
         h_flex()
             .group("commit_editor_footer")
@@ -483,7 +476,7 @@ impl CommitModal {
                         }),
                         self.render_git_commit_menu(
                             ElementId::Name(format!("split-button-right-{}", commit_label).into()),
-                            Some(focus_handle.clone()),
+                            Some(focus_handle),
                         )
                         .into_any_element(),
                     )),

crates/git_ui/src/commit_tooltip.rs 🔗

@@ -181,7 +181,7 @@ impl Render for CommitTooltip {
             .get(0..8)
             .map(|sha| sha.to_string().into())
             .unwrap_or_else(|| self.commit.sha.clone());
-        let full_sha = self.commit.sha.to_string().clone();
+        let full_sha = self.commit.sha.to_string();
         let absolute_timestamp = format_local_timestamp(
             self.commit.commit_time,
             OffsetDateTime::now_utc(),

crates/git_ui/src/commit_view.rs 🔗

@@ -88,11 +88,10 @@ impl CommitView {
                             let ix = pane.items().position(|item| {
                                 let commit_view = item.downcast::<CommitView>();
                                 commit_view
-                                    .map_or(false, |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);
-                                return;
                             } else {
                                 pane.add_item(Box::new(commit_view), true, true, None, window, cx);
                             }
@@ -160,7 +159,7 @@ impl CommitView {
             });
         }
 
-        cx.spawn(async move |this, mut cx| {
+        cx.spawn(async move |this, cx| {
             for file in commit_diff.files {
                 let is_deleted = file.new_text.is_none();
                 let new_text = file.new_text.unwrap_or_default();
@@ -179,9 +178,9 @@ impl CommitView {
                     worktree_id,
                 }) as Arc<dyn language::File>;
 
-                let buffer = build_buffer(new_text, file, &language_registry, &mut cx).await?;
+                let buffer = build_buffer(new_text, file, &language_registry, cx).await?;
                 let buffer_diff =
-                    build_buffer_diff(old_text, &buffer, &language_registry, &mut cx).await?;
+                    build_buffer_diff(old_text, &buffer, &language_registry, cx).await?;
 
                 this.update(cx, |this, cx| {
                     this.multibuffer.update(cx, |multibuffer, cx| {

crates/git_ui/src/conflict_view.rs 🔗

@@ -55,7 +55,7 @@ pub fn register_editor(editor: &mut Editor, buffer: Entity<MultiBuffer>, cx: &mu
         buffers: Default::default(),
     });
 
-    let buffers = buffer.read(cx).all_buffers().clone();
+    let buffers = buffer.read(cx).all_buffers();
     for buffer in buffers {
         buffer_added(editor, buffer, cx);
     }
@@ -112,7 +112,7 @@ fn excerpt_for_buffer_updated(
 }
 
 fn buffer_added(editor: &mut Editor, buffer: Entity<Buffer>, cx: &mut Context<Editor>) {
-    let Some(project) = &editor.project else {
+    let Some(project) = editor.project() else {
         return;
     };
     let git_store = project.read(cx).git_store().clone();
@@ -129,7 +129,7 @@ fn buffer_added(editor: &mut Editor, buffer: Entity<Buffer>, cx: &mut Context<Ed
             let subscription = cx.subscribe(&conflict_set, conflicts_updated);
             BufferConflicts {
                 block_ids: Vec::new(),
-                conflict_set: conflict_set.clone(),
+                conflict_set,
                 _subscription: subscription,
             }
         });
@@ -156,7 +156,7 @@ fn buffers_removed(editor: &mut Editor, removed_buffer_ids: &[BufferId], cx: &mu
         .unwrap()
         .buffers
         .retain(|buffer_id, buffer| {
-            if removed_buffer_ids.contains(&buffer_id) {
+            if removed_buffer_ids.contains(buffer_id) {
                 removed_block_ids.extend(buffer.block_ids.iter().map(|(_, block_id)| *block_id));
                 false
             } else {
@@ -222,12 +222,12 @@ fn conflicts_updated(
                 let precedes_start = range
                     .context
                     .start
-                    .cmp(&conflict_range.start, &buffer_snapshot)
+                    .cmp(&conflict_range.start, buffer_snapshot)
                     .is_le();
                 let follows_end = range
                     .context
                     .end
-                    .cmp(&conflict_range.start, &buffer_snapshot)
+                    .cmp(&conflict_range.start, buffer_snapshot)
                     .is_ge();
                 precedes_start && follows_end
             }) else {
@@ -268,12 +268,12 @@ fn conflicts_updated(
             let precedes_start = range
                 .context
                 .start
-                .cmp(&conflict.range.start, &buffer_snapshot)
+                .cmp(&conflict.range.start, buffer_snapshot)
                 .is_le();
             let follows_end = range
                 .context
                 .end
-                .cmp(&conflict.range.start, &buffer_snapshot)
+                .cmp(&conflict.range.start, buffer_snapshot)
                 .is_ge();
             precedes_start && follows_end
         }) else {
@@ -437,7 +437,6 @@ fn render_conflict_buttons(
             Button::new("both", "Use Both")
                 .label_size(LabelSize::Small)
                 .on_click({
-                    let editor = editor.clone();
                     let conflict = conflict.clone();
                     let ours = conflict.ours.clone();
                     let theirs = conflict.theirs.clone();
@@ -469,7 +468,7 @@ pub(crate) fn resolve_conflict(
         let Some((workspace, project, multibuffer, buffer)) = editor
             .update(cx, |editor, cx| {
                 let workspace = editor.workspace()?;
-                let project = editor.project.clone()?;
+                let project = editor.project()?.clone();
                 let multibuffer = editor.buffer().clone();
                 let buffer_id = resolved_conflict.ours.end.buffer_id?;
                 let buffer = multibuffer.read(cx).buffer(buffer_id)?;

crates/git_ui/src/file_diff_view.rs 🔗

@@ -123,7 +123,7 @@ impl FileDiffView {
             old_buffer,
             new_buffer,
             _recalculate_diff_task: cx.spawn(async move |this, cx| {
-                while let Ok(_) = buffer_changes_rx.recv().await {
+                while buffer_changes_rx.recv().await.is_ok() {
                     loop {
                         let mut timer = cx
                             .background_executor()
@@ -398,7 +398,7 @@ mod tests {
 
         let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
 
-        let (workspace, mut cx) =
+        let (workspace, cx) =
             cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
 
         let diff_view = workspace
@@ -417,7 +417,7 @@ mod tests {
         // Verify initial diff
         assert_state_with_diff(
             &diff_view.read_with(cx, |diff_view, _| diff_view.editor.clone()),
-            &mut cx,
+            cx,
             &unindent(
                 "
                 - old line 1
@@ -452,7 +452,7 @@ mod tests {
         cx.executor().advance_clock(RECALCULATE_DIFF_DEBOUNCE);
         assert_state_with_diff(
             &diff_view.read_with(cx, |diff_view, _| diff_view.editor.clone()),
-            &mut cx,
+            cx,
             &unindent(
                 "
                 - old line 1
@@ -487,7 +487,7 @@ mod tests {
         cx.executor().advance_clock(RECALCULATE_DIFF_DEBOUNCE);
         assert_state_with_diff(
             &diff_view.read_with(cx, |diff_view, _| diff_view.editor.clone()),
-            &mut cx,
+            cx,
             &unindent(
                 "
                   ˇnew line 1

crates/git_ui/src/git_panel.rs 🔗

@@ -103,7 +103,7 @@ fn prompt<T>(
 where
     T: IntoEnumIterator + VariantNames + 'static,
 {
-    let rx = window.prompt(PromptLevel::Info, msg, detail, &T::VARIANTS, cx);
+    let rx = window.prompt(PromptLevel::Info, msg, detail, T::VARIANTS, cx);
     cx.spawn(async move |_| Ok(T::iter().nth(rx.await?).unwrap()))
 }
 
@@ -426,7 +426,7 @@ impl GitPanel {
         let git_store = project.read(cx).git_store().clone();
         let active_repository = project.read(cx).active_repository(cx);
 
-        let git_panel = cx.new(|cx| {
+        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| {
@@ -563,9 +563,7 @@ impl GitPanel {
 
             this.schedule_update(false, window, cx);
             this
-        });
-
-        git_panel
+        })
     }
 
     fn hide_scrollbars(&mut self, window: &mut Window, cx: &mut Context<Self>) {
@@ -652,14 +650,14 @@ impl GitPanel {
         if GitPanelSettings::get_global(cx).sort_by_path {
             return self
                 .entries
-                .binary_search_by(|entry| entry.status_entry().unwrap().repo_path.cmp(&path))
+                .binary_search_by(|entry| entry.status_entry().unwrap().repo_path.cmp(path))
                 .ok();
         }
 
         if self.conflicted_count > 0 {
             let conflicted_start = 1;
             if let Ok(ix) = self.entries[conflicted_start..conflicted_start + self.conflicted_count]
-                .binary_search_by(|entry| entry.status_entry().unwrap().repo_path.cmp(&path))
+                .binary_search_by(|entry| entry.status_entry().unwrap().repo_path.cmp(path))
             {
                 return Some(conflicted_start + ix);
             }
@@ -671,7 +669,7 @@ impl GitPanel {
                 0
             } + 1;
             if let Ok(ix) = self.entries[tracked_start..tracked_start + self.tracked_count]
-                .binary_search_by(|entry| entry.status_entry().unwrap().repo_path.cmp(&path))
+                .binary_search_by(|entry| entry.status_entry().unwrap().repo_path.cmp(path))
             {
                 return Some(tracked_start + ix);
             }
@@ -687,7 +685,7 @@ impl GitPanel {
                 0
             } + 1;
             if let Ok(ix) = self.entries[untracked_start..untracked_start + self.new_count]
-                .binary_search_by(|entry| entry.status_entry().unwrap().repo_path.cmp(&path))
+                .binary_search_by(|entry| entry.status_entry().unwrap().repo_path.cmp(path))
             {
                 return Some(untracked_start + ix);
             }
@@ -775,7 +773,7 @@ impl GitPanel {
 
         if window
             .focused(cx)
-            .map_or(false, |focused| self.focus_handle == focused)
+            .is_some_and(|focused| self.focus_handle == focused)
         {
             dispatch_context.add("menu");
             dispatch_context.add("ChangesList");
@@ -894,9 +892,7 @@ impl GitPanel {
         let have_entries = self
             .active_repository
             .as_ref()
-            .map_or(false, |active_repository| {
-                active_repository.read(cx).status_summary().count > 0
-            });
+            .is_some_and(|active_repository| active_repository.read(cx).status_summary().count > 0);
         if have_entries && self.selected_entry.is_none() {
             self.selected_entry = Some(1);
             self.scroll_to_selected_entry(cx);
@@ -926,19 +922,17 @@ impl GitPanel {
             let workspace = self.workspace.upgrade()?;
             let git_repo = self.active_repository.as_ref()?;
 
-            if let Some(project_diff) = workspace.read(cx).active_item_as::<ProjectDiff>(cx) {
-                if let Some(project_path) = project_diff.read(cx).active_path(cx) {
-                    if Some(&entry.repo_path)
-                        == git_repo
-                            .read(cx)
-                            .project_path_to_repo_path(&project_path, cx)
-                            .as_ref()
-                    {
-                        project_diff.focus_handle(cx).focus(window);
-                        project_diff.update(cx, |project_diff, cx| project_diff.autoscroll(cx));
-                        return None;
-                    }
-                }
+            if let Some(project_diff) = workspace.read(cx).active_item_as::<ProjectDiff>(cx)
+                && let Some(project_path) = project_diff.read(cx).active_path(cx)
+                && Some(&entry.repo_path)
+                    == git_repo
+                        .read(cx)
+                        .project_path_to_repo_path(&project_path, cx)
+                        .as_ref()
+            {
+                project_diff.focus_handle(cx).focus(window);
+                project_diff.update(cx, |project_diff, cx| project_diff.autoscroll(cx));
+                return None;
             };
 
             self.workspace
@@ -1202,16 +1196,13 @@ impl GitPanel {
             window,
             cx,
         );
-        cx.spawn(async move |this, cx| match prompt.await {
-            Ok(RestoreCancel::RestoreTrackedFiles) => {
+        cx.spawn(async move |this, cx| {
+            if let Ok(RestoreCancel::RestoreTrackedFiles) = prompt.await {
                 this.update(cx, |this, cx| {
                     this.perform_checkout(entries, cx);
                 })
                 .ok();
             }
-            _ => {
-                return;
-            }
         })
         .detach();
     }
@@ -1341,10 +1332,10 @@ impl GitPanel {
                     .iter()
                     .filter_map(|entry| entry.status_entry())
                     .filter(|status_entry| {
-                        section.contains(&status_entry, repository)
+                        section.contains(status_entry, repository)
                             && status_entry.staging.as_bool() != Some(goal_staged_state)
                     })
-                    .map(|status_entry| status_entry.clone())
+                    .cloned()
                     .collect::<Vec<_>>();
 
                 (goal_staged_state, entries)
@@ -1476,7 +1467,6 @@ impl GitPanel {
             .read(cx)
             .as_singleton()
             .unwrap()
-            .clone()
     }
 
     fn toggle_staged_for_selected(
@@ -1642,13 +1632,12 @@ impl GitPanel {
     fn has_commit_message(&self, cx: &mut Context<Self>) -> bool {
         let text = self.commit_editor.read(cx).text(cx);
         if !text.trim().is_empty() {
-            return true;
+            true
         } else if text.is_empty() {
-            return self
-                .suggest_commit_message(cx)
-                .is_some_and(|text| !text.trim().is_empty());
+            self.suggest_commit_message(cx)
+                .is_some_and(|text| !text.trim().is_empty())
         } else {
-            return false;
+            false
         }
     }
 
@@ -1833,7 +1822,9 @@ impl GitPanel {
 
         let git_status_entry = if let Some(staged_entry) = &self.single_staged_entry {
             Some(staged_entry)
-        } else if let Some(single_tracked_entry) = &self.single_tracked_entry {
+        } else if self.total_staged_count() == 0
+            && let Some(single_tracked_entry) = &self.single_tracked_entry
+        {
             Some(single_tracked_entry)
         } else {
             None
@@ -1950,7 +1941,7 @@ impl GitPanel {
                     thinking_allowed: false,
                 };
 
-                let stream = model.stream_completion_text(request, &cx);
+                let stream = model.stream_completion_text(request, cx);
                 match stream.await {
                     Ok(mut messages) => {
                         if !text_empty {
@@ -2081,6 +2072,100 @@ impl GitPanel {
             .detach_and_log_err(cx);
     }
 
+    pub(crate) fn git_clone(&mut self, repo: String, window: &mut Window, cx: &mut Context<Self>) {
+        let path = cx.prompt_for_paths(gpui::PathPromptOptions {
+            files: false,
+            directories: true,
+            multiple: false,
+            prompt: Some("Select as Repository Destination".into()),
+        });
+
+        let workspace = self.workspace.clone();
+
+        cx.spawn_in(window, async move |this, cx| {
+            let mut paths = path.await.ok()?.ok()??;
+            let mut path = paths.pop()?;
+            let repo_name = repo
+                .split(std::path::MAIN_SEPARATOR_STR)
+                .last()?
+                .strip_suffix(".git")?
+                .to_owned();
+
+            let fs = this.read_with(cx, |this, _| this.fs.clone()).ok()?;
+
+            let prompt_answer = match fs.git_clone(&repo, path.as_path()).await {
+                Ok(_) => cx.update(|window, cx| {
+                    window.prompt(
+                        PromptLevel::Info,
+                        &format!("Git Clone: {}", repo_name),
+                        None,
+                        &["Add repo to project", "Open repo in new project"],
+                        cx,
+                    )
+                }),
+                Err(e) => {
+                    this.update(cx, |this: &mut GitPanel, cx| {
+                        let toast = StatusToast::new(e.to_string(), cx, |this, _| {
+                            this.icon(ToastIcon::new(IconName::XCircle).color(Color::Error))
+                                .dismiss_button(true)
+                        });
+
+                        this.workspace
+                            .update(cx, |workspace, cx| {
+                                workspace.toggle_status_toast(toast, cx);
+                            })
+                            .ok();
+                    })
+                    .ok()?;
+
+                    return None;
+                }
+            }
+            .ok()?;
+
+            path.push(repo_name);
+            match prompt_answer.await.ok()? {
+                0 => {
+                    workspace
+                        .update(cx, |workspace, cx| {
+                            workspace
+                                .project()
+                                .update(cx, |project, cx| {
+                                    project.create_worktree(path.as_path(), true, cx)
+                                })
+                                .detach();
+                        })
+                        .ok();
+                }
+                1 => {
+                    workspace
+                        .update(cx, move |workspace, cx| {
+                            workspace::open_new(
+                                Default::default(),
+                                workspace.app_state().clone(),
+                                cx,
+                                move |workspace, _, cx| {
+                                    cx.activate(true);
+                                    workspace
+                                        .project()
+                                        .update(cx, |project, cx| {
+                                            project.create_worktree(&path, true, cx)
+                                        })
+                                        .detach();
+                                },
+                            )
+                            .detach();
+                        })
+                        .ok();
+                }
+                _ => {}
+            }
+
+            Some(())
+        })
+        .detach();
+    }
+
     pub(crate) fn git_init(&mut self, window: &mut Window, cx: &mut Context<Self>) {
         let worktrees = self
             .project
@@ -2090,7 +2175,7 @@ impl GitPanel {
 
         let worktree = if worktrees.len() == 1 {
             Task::ready(Some(worktrees.first().unwrap().clone()))
-        } else if worktrees.len() == 0 {
+        } else if worktrees.is_empty() {
             let result = window.prompt(
                 PromptLevel::Warning,
                 "Unable to initialize a git repository",
@@ -2418,10 +2503,11 @@ impl GitPanel {
                 new_co_authors.push((name.clone(), email.clone()))
             }
         }
-        if !project.is_local() && !project.is_read_only(cx) {
-            if let Some(local_committer) = self.local_committer(room, cx) {
-                new_co_authors.push(local_committer);
-            }
+        if !project.is_local()
+            && !project.is_read_only(cx)
+            && let Some(local_committer) = self.local_committer(room, cx)
+        {
+            new_co_authors.push(local_committer);
         }
         new_co_authors
     }
@@ -2660,35 +2746,34 @@ impl GitPanel {
         for pending in self.pending.iter() {
             if pending.target_status == TargetStatus::Staged {
                 pending_staged_count += pending.entries.len();
-                last_pending_staged = pending.entries.iter().next().cloned();
+                last_pending_staged = pending.entries.first().cloned();
             }
-            if let Some(single_staged) = &single_staged_entry {
-                if pending
+            if let Some(single_staged) = &single_staged_entry
+                && pending
                     .entries
                     .iter()
                     .any(|entry| entry.repo_path == single_staged.repo_path)
-                {
-                    pending_status_for_single_staged = Some(pending.target_status);
-                }
+            {
+                pending_status_for_single_staged = Some(pending.target_status);
             }
         }
 
-        if conflict_entries.len() == 0 && staged_count == 1 && pending_staged_count == 0 {
+        if conflict_entries.is_empty() && staged_count == 1 && pending_staged_count == 0 {
             match pending_status_for_single_staged {
                 Some(TargetStatus::Staged) | None => {
                     self.single_staged_entry = single_staged_entry;
                 }
                 _ => {}
             }
-        } else if conflict_entries.len() == 0 && pending_staged_count == 1 {
+        } else if conflict_entries.is_empty() && pending_staged_count == 1 {
             self.single_staged_entry = last_pending_staged;
         }
 
-        if conflict_entries.len() == 0 && changed_entries.len() == 1 {
+        if conflict_entries.is_empty() && changed_entries.len() == 1 {
             self.single_tracked_entry = changed_entries.first().cloned();
         }
 
-        if conflict_entries.len() > 0 {
+        if !conflict_entries.is_empty() {
             self.entries.push(GitListEntry::Header(GitHeaderEntry {
                 header: Section::Conflict,
             }));
@@ -2696,7 +2781,7 @@ impl GitPanel {
                 .extend(conflict_entries.into_iter().map(GitListEntry::Status));
         }
 
-        if changed_entries.len() > 0 {
+        if !changed_entries.is_empty() {
             if !sort_by_path {
                 self.entries.push(GitListEntry::Header(GitHeaderEntry {
                     header: Section::Tracked,
@@ -2705,7 +2790,7 @@ impl GitPanel {
             self.entries
                 .extend(changed_entries.into_iter().map(GitListEntry::Status));
         }
-        if new_entries.len() > 0 {
+        if !new_entries.is_empty() {
             self.entries.push(GitListEntry::Header(GitHeaderEntry {
                 header: Section::New,
             }));
@@ -2844,8 +2929,7 @@ impl GitPanel {
             .matches(git::repository::REMOTE_CANCELLED_BY_USER)
             .next()
             .is_some()
-        {
-            return; // Hide the cancelled by user message
+        { // Hide the cancelled by user message
         } else {
             workspace.update(cx, |workspace, cx| {
                 let workspace_weak = cx.weak_entity();
@@ -2899,11 +2983,9 @@ impl GitPanel {
             let status_toast = StatusToast::new(message, cx, move |this, _cx| {
                 use remote_output::SuccessStyle::*;
                 match style {
-                    Toast { .. } => {
-                        this.icon(ToastIcon::new(IconName::GitBranchSmall).color(Color::Muted))
-                    }
+                    Toast => this.icon(ToastIcon::new(IconName::GitBranchAlt).color(Color::Muted)),
                     ToastWithLog { output } => this
-                        .icon(ToastIcon::new(IconName::GitBranchSmall).color(Color::Muted))
+                        .icon(ToastIcon::new(IconName::GitBranchAlt).color(Color::Muted))
                         .action("View Log", move |window, cx| {
                             let output = output.clone();
                             let output =
@@ -2915,7 +2997,7 @@ impl GitPanel {
                                 .ok();
                         }),
                     PushPrLink { text, link } => this
-                        .icon(ToastIcon::new(IconName::GitBranchSmall).color(Color::Muted))
+                        .icon(ToastIcon::new(IconName::GitBranchAlt).color(Color::Muted))
                         .action(text, move |_, cx| cx.open_url(&link)),
                 }
             });
@@ -3109,7 +3191,7 @@ impl GitPanel {
                             .justify_center()
                             .border_l_1()
                             .border_color(cx.theme().colors().border)
-                            .child(Icon::new(IconName::ChevronDownSmall).size(IconSize::XSmall)),
+                            .child(Icon::new(IconName::ChevronDown).size(IconSize::XSmall)),
                     ),
             )
             .menu({
@@ -3122,7 +3204,7 @@ impl GitPanel {
                     Some(ContextMenu::build(window, cx, |context_menu, _, _| {
                         context_menu
                             .when_some(keybinding_target.clone(), |el, keybinding_target| {
-                                el.context(keybinding_target.clone())
+                                el.context(keybinding_target)
                             })
                             .when(has_previous_commit, |this| {
                                 this.toggleable_entry(
@@ -3178,12 +3260,10 @@ impl GitPanel {
             } else {
                 "Amend Tracked"
             }
+        } else if self.has_staged_changes() {
+            "Commit"
         } else {
-            if self.has_staged_changes() {
-                "Commit"
-            } else {
-                "Commit Tracked"
-            }
+            "Commit Tracked"
         }
     }
 
@@ -3304,7 +3384,7 @@ impl GitPanel {
         let enable_coauthors = self.render_co_authors(cx);
 
         let editor_focus_handle = self.commit_editor.focus_handle(cx);
-        let expand_tooltip_focus_handle = editor_focus_handle.clone();
+        let expand_tooltip_focus_handle = editor_focus_handle;
 
         let branch = active_repository.read(cx).branch.clone();
         let head_commit = active_repository.read(cx).head_commit.clone();
@@ -3317,7 +3397,7 @@ impl GitPanel {
             * MAX_PANEL_EDITOR_LINES
             + gap;
 
-        let git_panel = cx.entity().clone();
+        let git_panel = cx.entity();
         let display_name = SharedString::from(Arc::from(
             active_repository
                 .read(cx)
@@ -3333,7 +3413,7 @@ impl GitPanel {
                 display_name,
                 branch,
                 head_commit,
-                Some(git_panel.clone()),
+                Some(git_panel),
             ))
             .child(
                 panel_editor_container(window, cx)
@@ -3484,7 +3564,7 @@ impl GitPanel {
                 }),
                 self.render_git_commit_menu(
                     ElementId::Name(format!("split-button-right-{}", title).into()),
-                    Some(commit_tooltip_focus_handle.clone()),
+                    Some(commit_tooltip_focus_handle),
                     cx,
                 )
                 .into_any_element(),
@@ -3550,7 +3630,7 @@ impl GitPanel {
                                 CommitView::open(
                                     commit.clone(),
                                     repo.clone(),
-                                    workspace.clone().clone(),
+                                    workspace.clone(),
                                     window,
                                     cx,
                                 );
@@ -4258,7 +4338,7 @@ impl GitPanel {
                         }
                     })
                     .child(
-                        self.entry_label(display_name.clone(), label_color)
+                        self.entry_label(display_name, label_color)
                             .when(status.is_deleted(), |this| this.strikethrough()),
                     ),
             )
@@ -4396,7 +4476,7 @@ fn current_language_model(cx: &Context<'_, GitPanel>) -> Option<Arc<dyn Language
 impl Render for GitPanel {
     fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
         let project = self.project.read(cx);
-        let has_entries = self.entries.len() > 0;
+        let has_entries = !self.entries.is_empty();
         let room = self
             .workspace
             .upgrade()
@@ -4404,7 +4484,7 @@ impl Render for GitPanel {
 
         let has_write_access = self.has_write_access(cx);
 
-        let has_co_authors = room.map_or(false, |room| {
+        let has_co_authors = room.is_some_and(|room| {
             self.load_local_committer(cx);
             let room = room.read(cx);
             room.remote_participants()
@@ -4524,7 +4604,7 @@ impl editor::Addon for GitPanelAddon {
 
         git_panel
             .read(cx)
-            .render_buffer_header_controls(&git_panel, &file, window, cx)
+            .render_buffer_header_controls(&git_panel, file, window, cx)
     }
 }
 
@@ -4561,7 +4641,7 @@ impl Panel for GitPanel {
     }
 
     fn icon(&self, _: &Window, cx: &App) -> Option<ui::IconName> {
-        Some(ui::IconName::GitBranchSmall).filter(|_| GitPanelSettings::get_global(cx).button)
+        Some(ui::IconName::GitBranchAlt).filter(|_| GitPanelSettings::get_global(cx).button)
     }
 
     fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
@@ -4607,7 +4687,7 @@ impl GitPanelMessageTooltip {
                     author_email: details.author_email.clone(),
                     commit_time: OffsetDateTime::from_unix_timestamp(details.commit_timestamp)?,
                     message: Some(ParsedCommitMessage {
-                        message: details.message.clone(),
+                        message: details.message,
                         ..Default::default()
                     }),
                 };
@@ -4720,12 +4800,10 @@ impl RenderOnce for PanelRepoFooter {
 
         // ideally, show the whole branch and repo names but
         // when we can't, use a budget to allocate space between the two
-        let (repo_display_len, branch_display_len) = if branch_actual_len + repo_actual_len
-            <= LABEL_CHARACTER_BUDGET
-        {
-            (repo_actual_len, branch_actual_len)
-        } else {
-            if branch_actual_len <= MAX_BRANCH_LEN {
+        let (repo_display_len, branch_display_len) =
+            if branch_actual_len + repo_actual_len <= LABEL_CHARACTER_BUDGET {
+                (repo_actual_len, branch_actual_len)
+            } else if branch_actual_len <= MAX_BRANCH_LEN {
                 let repo_space = (LABEL_CHARACTER_BUDGET - branch_actual_len).min(MAX_REPO_LEN);
                 (repo_space, branch_actual_len)
             } else if repo_actual_len <= MAX_REPO_LEN {
@@ -4733,8 +4811,7 @@ impl RenderOnce for PanelRepoFooter {
                 (repo_actual_len, branch_space)
             } else {
                 (MAX_REPO_LEN, MAX_BRANCH_LEN)
-            }
-        };
+            };
 
         let truncated_repo_name = if repo_actual_len <= repo_display_len {
             active_repo_name.to_string()
@@ -4743,7 +4820,7 @@ impl RenderOnce for PanelRepoFooter {
         };
 
         let truncated_branch_name = if branch_actual_len <= branch_display_len {
-            branch_name.to_string()
+            branch_name
         } else {
             util::truncate_and_trailoff(branch_name.trim_ascii(), branch_display_len)
         };
@@ -4756,7 +4833,7 @@ impl RenderOnce for PanelRepoFooter {
 
         let repo_selector = PopoverMenu::new("repository-switcher")
             .menu({
-                let project = project.clone();
+                let project = project;
                 move |window, cx| {
                     let project = project.clone()?;
                     Some(cx.new(|cx| RepositorySelector::new(project, rems(16.), window, cx)))
@@ -4808,7 +4885,7 @@ impl RenderOnce for PanelRepoFooter {
                     .items_center()
                     .child(
                         div().child(
-                            Icon::new(IconName::GitBranchSmall)
+                            Icon::new(IconName::GitBranchAlt)
                                 .size(IconSize::Small)
                                 .color(if single_repo {
                                     Color::Disabled
@@ -4927,10 +5004,7 @@ impl Component for PanelRepoFooter {
                                 div()
                                     .w(example_width)
                                     .overflow_hidden()
-                                    .child(PanelRepoFooter::new_preview(
-                                        active_repository(1).clone(),
-                                        None,
-                                    ))
+                                    .child(PanelRepoFooter::new_preview(active_repository(1), None))
                                     .into_any_element(),
                             ),
                             single_example(
@@ -4939,7 +5013,7 @@ impl Component for PanelRepoFooter {
                                     .w(example_width)
                                     .overflow_hidden()
                                     .child(PanelRepoFooter::new_preview(
-                                        active_repository(2).clone(),
+                                        active_repository(2),
                                         Some(branch(unknown_upstream)),
                                     ))
                                     .into_any_element(),
@@ -4950,7 +5024,7 @@ impl Component for PanelRepoFooter {
                                     .w(example_width)
                                     .overflow_hidden()
                                     .child(PanelRepoFooter::new_preview(
-                                        active_repository(3).clone(),
+                                        active_repository(3),
                                         Some(branch(no_remote_upstream)),
                                     ))
                                     .into_any_element(),
@@ -4961,7 +5035,7 @@ impl Component for PanelRepoFooter {
                                     .w(example_width)
                                     .overflow_hidden()
                                     .child(PanelRepoFooter::new_preview(
-                                        active_repository(4).clone(),
+                                        active_repository(4),
                                         Some(branch(not_ahead_or_behind_upstream)),
                                     ))
                                     .into_any_element(),
@@ -4972,7 +5046,7 @@ impl Component for PanelRepoFooter {
                                     .w(example_width)
                                     .overflow_hidden()
                                     .child(PanelRepoFooter::new_preview(
-                                        active_repository(5).clone(),
+                                        active_repository(5),
                                         Some(branch(behind_upstream)),
                                     ))
                                     .into_any_element(),
@@ -4983,7 +5057,7 @@ impl Component for PanelRepoFooter {
                                     .w(example_width)
                                     .overflow_hidden()
                                     .child(PanelRepoFooter::new_preview(
-                                        active_repository(6).clone(),
+                                        active_repository(6),
                                         Some(branch(ahead_of_upstream)),
                                     ))
                                     .into_any_element(),
@@ -4994,7 +5068,7 @@ impl Component for PanelRepoFooter {
                                     .w(example_width)
                                     .overflow_hidden()
                                     .child(PanelRepoFooter::new_preview(
-                                        active_repository(7).clone(),
+                                        active_repository(7),
                                         Some(branch(ahead_and_behind_upstream)),
                                     ))
                                     .into_any_element(),
@@ -5165,7 +5239,7 @@ mod tests {
             project
                 .read(cx)
                 .worktrees(cx)
-                .nth(0)
+                .next()
                 .unwrap()
                 .read(cx)
                 .as_local()
@@ -5290,7 +5364,7 @@ mod tests {
             project
                 .read(cx)
                 .worktrees(cx)
-                .nth(0)
+                .next()
                 .unwrap()
                 .read(cx)
                 .as_local()
@@ -5341,7 +5415,7 @@ mod tests {
             project
                 .read(cx)
                 .worktrees(cx)
-                .nth(0)
+                .next()
                 .unwrap()
                 .read(cx)
                 .as_local()
@@ -5390,7 +5464,7 @@ mod tests {
             project
                 .read(cx)
                 .worktrees(cx)
-                .nth(0)
+                .next()
                 .unwrap()
                 .read(cx)
                 .as_local()

crates/git_ui/src/git_ui.rs 🔗

@@ -10,14 +10,17 @@ use git::{
     status::{FileStatus, StatusCode, UnmergedStatus, UnmergedStatusCode},
 };
 use git_panel_settings::GitPanelSettings;
-use gpui::{Action, App, Context, FocusHandle, Window, actions};
+use gpui::{
+    Action, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Window,
+    actions,
+};
 use onboarding::GitOnboardingModal;
 use project_diff::ProjectDiff;
 use ui::prelude::*;
-use workspace::Workspace;
+use workspace::{ModalView, Workspace};
 use zed_actions;
 
-use crate::text_diff_view::TextDiffView;
+use crate::{git_panel::GitPanel, text_diff_view::TextDiffView};
 
 mod askpass_modal;
 pub mod branch_picker;
@@ -169,6 +172,15 @@ pub fn init(cx: &mut App) {
                 panel.git_init(window, cx);
             });
         });
+        workspace.register_action(|workspace, _action: &git::Clone, window, cx| {
+            let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
+                return;
+            };
+
+            workspace.toggle_modal(window, cx, |window, cx| {
+                GitCloneModal::show(panel, window, cx)
+            });
+        });
         workspace.register_action(|workspace, _: &git::OpenModifiedFiles, window, cx| {
             open_modified_files(workspace, window, cx);
         });
@@ -233,12 +245,12 @@ fn render_remote_button(
             }
             (0, 0) => None,
             (ahead, 0) => Some(remote_button::render_push_button(
-                keybinding_target.clone(),
+                keybinding_target,
                 id,
                 ahead,
             )),
             (ahead, behind) => Some(remote_button::render_pull_button(
-                keybinding_target.clone(),
+                keybinding_target,
                 id,
                 ahead,
                 behind,
@@ -356,7 +368,7 @@ mod remote_button {
             "Publish",
             0,
             0,
-            Some(IconName::ArrowUpFromLine),
+            Some(IconName::ExpandUp),
             keybinding_target.clone(),
             move |_, window, cx| {
                 window.dispatch_action(Box::new(git::Push), cx);
@@ -383,7 +395,7 @@ mod remote_button {
             "Republish",
             0,
             0,
-            Some(IconName::ArrowUpFromLine),
+            Some(IconName::ExpandUp),
             keybinding_target.clone(),
             move |_, window, cx| {
                 window.dispatch_action(Box::new(git::Push), cx);
@@ -413,16 +425,9 @@ mod remote_button {
         let command = command.into();
 
         if let Some(handle) = focus_handle {
-            Tooltip::with_meta_in(
-                label.clone(),
-                Some(action),
-                command.clone(),
-                &handle,
-                window,
-                cx,
-            )
+            Tooltip::with_meta_in(label, Some(action), command, &handle, window, cx)
         } else {
-            Tooltip::with_meta(label.clone(), Some(action), command.clone(), window, cx)
+            Tooltip::with_meta(label, Some(action), command, window, cx)
         }
     }
 
@@ -438,14 +443,14 @@ mod remote_button {
                     .child(
                         div()
                             .px_1()
-                            .child(Icon::new(IconName::ChevronDownSmall).size(IconSize::XSmall)),
+                            .child(Icon::new(IconName::ChevronDown).size(IconSize::XSmall)),
                     ),
             )
             .menu(move |window, cx| {
                 Some(ContextMenu::build(window, cx, |context_menu, _, _| {
                     context_menu
                         .when_some(keybinding_target.clone(), |el, keybinding_target| {
-                            el.context(keybinding_target.clone())
+                            el.context(keybinding_target)
                         })
                         .action("Fetch", git::Fetch.boxed_clone())
                         .action("Fetch From", git::FetchFrom.boxed_clone())
@@ -613,3 +618,88 @@ impl Component for GitStatusIcon {
         )
     }
 }
+
+struct GitCloneModal {
+    panel: Entity<GitPanel>,
+    repo_input: Entity<Editor>,
+    focus_handle: FocusHandle,
+}
+
+impl GitCloneModal {
+    pub fn show(panel: Entity<GitPanel>, window: &mut Window, cx: &mut Context<Self>) -> Self {
+        let repo_input = cx.new(|cx| {
+            let mut editor = Editor::single_line(window, cx);
+            editor.set_placeholder_text("Enter repository URL…", cx);
+            editor
+        });
+        let focus_handle = repo_input.focus_handle(cx);
+
+        window.focus(&focus_handle);
+
+        Self {
+            panel,
+            repo_input,
+            focus_handle,
+        }
+    }
+}
+
+impl Focusable for GitCloneModal {
+    fn focus_handle(&self, _: &App) -> FocusHandle {
+        self.focus_handle.clone()
+    }
+}
+
+impl Render for GitCloneModal {
+    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+        div()
+            .elevation_3(cx)
+            .w(rems(34.))
+            .flex_1()
+            .overflow_hidden()
+            .child(
+                div()
+                    .w_full()
+                    .p_2()
+                    .border_b_1()
+                    .border_color(cx.theme().colors().border_variant)
+                    .child(self.repo_input.clone()),
+            )
+            .child(
+                h_flex()
+                    .w_full()
+                    .p_2()
+                    .gap_0p5()
+                    .rounded_b_sm()
+                    .bg(cx.theme().colors().editor_background)
+                    .child(
+                        Label::new("Clone a repository from GitHub or other sources.")
+                            .color(Color::Muted)
+                            .size(LabelSize::Small),
+                    )
+                    .child(
+                        Button::new("learn-more", "Learn More")
+                            .label_size(LabelSize::Small)
+                            .icon(IconName::ArrowUpRight)
+                            .icon_size(IconSize::XSmall)
+                            .on_click(|_, _, cx| {
+                                cx.open_url("https://github.com/git-guides/git-clone");
+                            }),
+                    ),
+            )
+            .on_action(cx.listener(|_, _: &menu::Cancel, _, cx| {
+                cx.emit(DismissEvent);
+            }))
+            .on_action(cx.listener(|this, _: &menu::Confirm, window, cx| {
+                let repo = this.repo_input.read(cx).text(cx);
+                this.panel.update(cx, |panel, cx| {
+                    panel.git_clone(repo, window, cx);
+                });
+                cx.emit(DismissEvent);
+            }))
+    }
+}
+
+impl EventEmitter<DismissEvent> for GitCloneModal {}
+
+impl ModalView for GitCloneModal {}

crates/git_ui/src/onboarding.rs 🔗

@@ -110,7 +110,7 @@ impl Render for GitOnboardingModal {
                     .child(Headline::new("Native Git Support").size(HeadlineSize::Large)),
             )
             .child(h_flex().absolute().top_2().right_2().child(
-                IconButton::new("cancel", IconName::X).on_click(cx.listener(
+                IconButton::new("cancel", IconName::Close).on_click(cx.listener(
                     |_, _: &ClickEvent, _window, cx| {
                         git_onboarding_event!("Cancelled", trigger = "X click");
                         cx.emit(DismissEvent);

crates/git_ui/src/picker_prompt.rs 🔗

@@ -152,7 +152,7 @@ impl PickerDelegate for PickerPromptDelegate {
                     .all_options
                     .iter()
                     .enumerate()
-                    .map(|(ix, option)| StringMatchCandidate::new(ix, &option))
+                    .map(|(ix, option)| StringMatchCandidate::new(ix, option))
                     .collect::<Vec<StringMatchCandidate>>()
             });
             let Some(candidates) = candidates.log_err() else {

crates/git_ui/src/project_diff.rs 🔗

@@ -242,7 +242,7 @@ impl ProjectDiff {
             TRACKED_NAMESPACE
         };
 
-        let path_key = PathKey::namespaced(namespace, entry.repo_path.0.clone());
+        let path_key = PathKey::namespaced(namespace, entry.repo_path.0);
 
         self.move_to_path(path_key, window, cx)
     }
@@ -280,7 +280,7 @@ impl ProjectDiff {
     fn button_states(&self, cx: &App) -> ButtonStates {
         let editor = self.editor.read(cx);
         let snapshot = self.multibuffer.read(cx).snapshot(cx);
-        let prev_next = snapshot.diff_hunks().skip(1).next().is_some();
+        let prev_next = snapshot.diff_hunks().nth(1).is_some();
         let mut selection = true;
 
         let mut ranges = editor
@@ -329,14 +329,14 @@ impl ProjectDiff {
             })
             .ok();
 
-        return ButtonStates {
+        ButtonStates {
             stage: has_unstaged_hunks,
             unstage: has_staged_hunks,
             prev_next,
             selection,
             stage_all,
             unstage_all,
-        };
+        }
     }
 
     fn handle_editor_event(
@@ -346,27 +346,24 @@ impl ProjectDiff {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        match event {
-            EditorEvent::SelectionsChanged { local: true } => {
-                let Some(project_path) = self.active_path(cx) else {
-                    return;
-                };
-                self.workspace
-                    .update(cx, |workspace, cx| {
-                        if let Some(git_panel) = workspace.panel::<GitPanel>(cx) {
-                            git_panel.update(cx, |git_panel, cx| {
-                                git_panel.select_entry_by_path(project_path, window, cx)
-                            })
-                        }
-                    })
-                    .ok();
-            }
-            _ => {}
+        if let EditorEvent::SelectionsChanged { local: true } = event {
+            let Some(project_path) = self.active_path(cx) else {
+                return;
+            };
+            self.workspace
+                .update(cx, |workspace, cx| {
+                    if let Some(git_panel) = workspace.panel::<GitPanel>(cx) {
+                        git_panel.update(cx, |git_panel, cx| {
+                            git_panel.select_entry_by_path(project_path, window, cx)
+                        })
+                    }
+                })
+                .ok();
         }
-        if editor.focus_handle(cx).contains_focused(window, cx) {
-            if self.multibuffer.read(cx).is_empty() {
-                self.focus_handle.focus(window)
-            }
+        if editor.focus_handle(cx).contains_focused(window, cx)
+            && self.multibuffer.read(cx).is_empty()
+        {
+            self.focus_handle.focus(window)
         }
     }
 
@@ -451,10 +448,10 @@ impl ProjectDiff {
         let diff = diff.read(cx);
         let diff_hunk_ranges = diff
             .hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx)
-            .map(|diff_hunk| diff_hunk.buffer_range.clone());
+            .map(|diff_hunk| diff_hunk.buffer_range);
         let conflicts = conflict_addon
             .conflict_set(snapshot.remote_id())
-            .map(|conflict_set| conflict_set.read(cx).snapshot().conflicts.clone())
+            .map(|conflict_set| conflict_set.read(cx).snapshot().conflicts)
             .unwrap_or_default();
         let conflicts = conflicts.iter().map(|conflict| conflict.range.clone());
 
@@ -513,7 +510,7 @@ impl ProjectDiff {
         mut recv: postage::watch::Receiver<()>,
         cx: &mut AsyncWindowContext,
     ) -> Result<()> {
-        while let Some(_) = recv.next().await {
+        while (recv.next().await).is_some() {
             let buffers_to_load = this.update(cx, |this, cx| this.load_buffers(cx))?;
             for buffer_to_load in buffers_to_load {
                 if let Some(buffer) = buffer_to_load.await.log_err() {
@@ -740,7 +737,7 @@ impl Render for ProjectDiff {
                 } else {
                     None
                 };
-                let keybinding_focus_handle = self.focus_handle(cx).clone();
+                let keybinding_focus_handle = self.focus_handle(cx);
                 el.child(
                     v_flex()
                         .gap_1()
@@ -1073,8 +1070,7 @@ pub struct ProjectDiffEmptyState {
 impl RenderOnce for ProjectDiffEmptyState {
     fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
         let status_against_remote = |ahead_by: usize, behind_by: usize| -> bool {
-            match self.current_branch {
-                Some(Branch {
+            matches!(self.current_branch, Some(Branch {
                     upstream:
                         Some(Upstream {
                             tracking:
@@ -1084,9 +1080,7 @@ impl RenderOnce for ProjectDiffEmptyState {
                             ..
                         }),
                     ..
-                }) if (ahead > 0) == (ahead_by > 0) && (behind > 0) == (behind_by > 0) => true,
-                _ => false,
-            }
+                }) if (ahead > 0) == (ahead_by > 0) && (behind > 0) == (behind_by > 0))
         };
 
         let change_count = |current_branch: &Branch| -> (usize, usize) {
@@ -1173,7 +1167,7 @@ impl RenderOnce for ProjectDiffEmptyState {
                             .child(Label::new("No Changes").color(Color::Muted))
                     } else {
                         this.when_some(self.current_branch.as_ref(), |this, branch| {
-                            this.child(has_branch_container(&branch))
+                            this.child(has_branch_container(branch))
                         })
                     }
                 }),
@@ -1332,14 +1326,14 @@ fn merge_anchor_ranges<'a>(
         loop {
             if let Some(left_range) = left
                 .peek()
-                .filter(|range| range.start.cmp(&next_range.end, &snapshot).is_le())
+                .filter(|range| range.start.cmp(&next_range.end, snapshot).is_le())
                 .cloned()
             {
                 left.next();
                 next_range.end = left_range.end;
             } else if let Some(right_range) = right
                 .peek()
-                .filter(|range| range.start.cmp(&next_range.end, &snapshot).is_le())
+                .filter(|range| range.start.cmp(&next_range.end, snapshot).is_le())
                 .cloned()
             {
                 right.next();

crates/git_ui/src/text_diff_view.rs 🔗

@@ -48,7 +48,7 @@ impl TextDiffView {
 
         let selection_data = source_editor.update(cx, |editor, cx| {
             let multibuffer = editor.buffer().read(cx);
-            let source_buffer = multibuffer.as_singleton()?.clone();
+            let source_buffer = multibuffer.as_singleton()?;
             let selections = editor.selections.all::<Point>(cx);
             let buffer_snapshot = source_buffer.read(cx);
             let first_selection = selections.first()?;
@@ -207,7 +207,7 @@ impl TextDiffView {
             path: Some(format!("Clipboard ↔ {selection_location_path}").into()),
             buffer_changes_tx,
             _recalculate_diff_task: cx.spawn(async move |_, cx| {
-                while let Ok(_) = buffer_changes_rx.recv().await {
+                while buffer_changes_rx.recv().await.is_ok() {
                     loop {
                         let mut timer = cx
                             .background_executor()
@@ -259,7 +259,7 @@ async fn update_diff_buffer(
     let source_buffer_snapshot = source_buffer.read_with(cx, |buffer, _| buffer.snapshot())?;
 
     let base_buffer_snapshot = clipboard_buffer.read_with(cx, |buffer, _| buffer.snapshot())?;
-    let base_text = base_buffer_snapshot.text().to_string();
+    let base_text = base_buffer_snapshot.text();
 
     let diff_snapshot = cx
         .update(|cx| {
@@ -686,7 +686,7 @@ mod tests {
 
         let project = Project::test(fs, [project_root.as_ref()], cx).await;
 
-        let (workspace, mut cx) =
+        let (workspace, cx) =
             cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
 
         let buffer = project
@@ -725,7 +725,7 @@ mod tests {
 
         assert_state_with_diff(
             &diff_view.read_with(cx, |diff_view, _| diff_view.diff_editor.clone()),
-            &mut cx,
+            cx,
             expected_diff,
         );
 

crates/go_to_line/src/cursor_position.rs 🔗

@@ -1,4 +1,4 @@
-use editor::{Editor, MultiBufferSnapshot};
+use editor::{Editor, EditorSettings, MultiBufferSnapshot};
 use gpui::{App, Entity, FocusHandle, Focusable, Subscription, Task, WeakEntity};
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
@@ -95,10 +95,8 @@ impl CursorPosition {
                 .ok()
                 .unwrap_or(true);
 
-            if !is_singleton {
-                if let Some(debounce) = debounce {
-                    cx.background_executor().timer(debounce).await;
-                }
+            if !is_singleton && let Some(debounce) = debounce {
+                cx.background_executor().timer(debounce).await;
             }
 
             editor
@@ -108,7 +106,7 @@ impl CursorPosition {
                         cursor_position.selected_count.selections = editor.selections.count();
                         match editor.mode() {
                             editor::EditorMode::AutoHeight { .. }
-                            | editor::EditorMode::SingleLine { .. }
+                            | editor::EditorMode::SingleLine
                             | editor::EditorMode::Minimap { .. } => {
                                 cursor_position.position = None;
                                 cursor_position.context = None;
@@ -131,7 +129,7 @@ impl CursorPosition {
                                                 cursor_position.selected_count.lines += 1;
                                             }
                                         }
-                                        if last_selection.as_ref().map_or(true, |last_selection| {
+                                        if last_selection.as_ref().is_none_or(|last_selection| {
                                             selection.id > last_selection.id
                                         }) {
                                             last_selection = Some(selection);
@@ -209,6 +207,13 @@ impl CursorPosition {
 
 impl Render for CursorPosition {
     fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+        if !EditorSettings::get_global(cx)
+            .status_bar
+            .cursor_position_button
+        {
+            return div();
+        }
+
         div().when_some(self.position, |el, position| {
             let mut text = format!(
                 "{}{FILE_ROW_COLUMN_DELIMITER}{}",
@@ -227,13 +232,11 @@ impl Render for CursorPosition {
                                 if let Some(editor) = workspace
                                     .active_item(cx)
                                     .and_then(|item| item.act_as::<Editor>(cx))
+                                    && let Some((_, buffer, _)) = editor.read(cx).active_excerpt(cx)
                                 {
-                                    if let Some((_, buffer, _)) = editor.read(cx).active_excerpt(cx)
-                                    {
-                                        workspace.toggle_modal(window, cx, |window, cx| {
-                                            crate::GoToLine::new(editor, buffer, window, cx)
-                                        })
-                                    }
+                                    workspace.toggle_modal(window, cx, |window, cx| {
+                                        crate::GoToLine::new(editor, buffer, window, cx)
+                                    })
                                 }
                             });
                         }

crates/go_to_line/src/go_to_line.rs 🔗

@@ -103,11 +103,11 @@ impl GoToLine {
                             return;
                         };
                         editor.update(cx, |editor, cx| {
-                            if let Some(placeholder_text) = editor.placeholder_text() {
-                                if editor.text(cx).is_empty() {
-                                    let placeholder_text = placeholder_text.to_string();
-                                    editor.set_text(placeholder_text, window, cx);
-                                }
+                            if let Some(placeholder_text) = editor.placeholder_text()
+                                && editor.text(cx).is_empty()
+                            {
+                                let placeholder_text = placeholder_text.to_string();
+                                editor.set_text(placeholder_text, window, cx);
                             }
                         });
                     }
@@ -157,7 +157,7 @@ impl GoToLine {
                 self.prev_scroll_position.take();
                 cx.emit(DismissEvent)
             }
-            editor::EditorEvent::BufferEdited { .. } => self.highlight_current_line(cx),
+            editor::EditorEvent::BufferEdited => self.highlight_current_line(cx),
             _ => {}
         }
     }
@@ -712,7 +712,7 @@ mod tests {
     ) -> Entity<GoToLine> {
         cx.dispatch_action(editor::actions::ToggleGoToLine);
         workspace.update(cx, |workspace, cx| {
-            workspace.active_modal::<GoToLine>(cx).unwrap().clone()
+            workspace.active_modal::<GoToLine>(cx).unwrap()
         })
     }
 

crates/google_ai/src/google_ai.rs 🔗

@@ -106,10 +106,9 @@ pub fn validate_generate_content_request(request: &GenerateContentRequest) -> Re
         .contents
         .iter()
         .find(|content| content.role == Role::User)
+        && user_content.parts.is_empty()
     {
-        if user_content.parts.is_empty() {
-            bail!("User content must contain at least one part");
-        }
+        bail!("User content must contain at least one part");
     }
 
     Ok(())
@@ -267,7 +266,7 @@ pub struct CitationMetadata {
 pub struct PromptFeedback {
     #[serde(skip_serializing_if = "Option::is_none")]
     pub block_reason: Option<String>,
-    pub safety_ratings: Vec<SafetyRating>,
+    pub safety_ratings: Option<Vec<SafetyRating>>,
     #[serde(skip_serializing_if = "Option::is_none")]
     pub block_reason_message: Option<String>,
 }
@@ -478,10 +477,10 @@ impl<'de> Deserialize<'de> for ModelName {
                 model_id: id.to_string(),
             })
         } else {
-            return Err(serde::de::Error::custom(format!(
+            Err(serde::de::Error::custom(format!(
                 "Expected model name to begin with {}, got: {}",
                 MODEL_NAME_PREFIX, string
-            )));
+            )))
         }
     }
 }

crates/gpui/Cargo.toml 🔗

@@ -119,6 +119,7 @@ serde_json.workspace = true
 slotmap = "1.0.6"
 smallvec.workspace = true
 smol.workspace = true
+stacksafe.workspace = true
 strum.workspace = true
 sum_tree.workspace = true
 taffy = "=0.9.0"
@@ -209,7 +210,7 @@ xkbcommon = { version = "0.8.0", features = [
     "wayland",
     "x11",
 ], optional = true }
-xim = { git = "https://github.com/XDeme1/xim-rs", rev = "d50d461764c2213655cd9cf65a0ea94c70d3c4fd", features = [
+xim = { git = "https://github.com/zed-industries/xim-rs", rev = "c0a70c1bd2ce197364216e5e818a2cb3adb99a8d" , features = [
     "x11rb-xcb",
     "x11rb-client",
 ], optional = true }
@@ -305,3 +306,7 @@ path = "examples/uniform_list.rs"
 [[example]]
 name = "window_shadow"
 path = "examples/window_shadow.rs"
+
+[[example]]
+name = "grid_layout"
+path = "examples/grid_layout.rs"

crates/gpui/build.rs 🔗

@@ -327,10 +327,10 @@ mod windows {
     /// You can set the `GPUI_FXC_PATH` environment variable to specify the path to the fxc.exe compiler.
     fn find_fxc_compiler() -> String {
         // Check environment variable
-        if let Ok(path) = std::env::var("GPUI_FXC_PATH") {
-            if Path::new(&path).exists() {
-                return path;
-            }
+        if let Ok(path) = std::env::var("GPUI_FXC_PATH")
+            && Path::new(&path).exists()
+        {
+            return path;
         }
 
         // Try to find in PATH
@@ -338,11 +338,10 @@ mod windows {
         if let Ok(output) = std::process::Command::new("where.exe")
             .arg("fxc.exe")
             .output()
+            && output.status.success()
         {
-            if output.status.success() {
-                let path = String::from_utf8_lossy(&output.stdout);
-                return path.trim().to_string();
-            }
+            let path = String::from_utf8_lossy(&output.stdout);
+            return path.trim().to_string();
         }
 
         // Check the default path
@@ -374,7 +373,7 @@ mod windows {
             shader_path,
             "vs_4_1",
         );
-        generate_rust_binding(&const_name, &output_file, &rust_binding_path);
+        generate_rust_binding(&const_name, &output_file, rust_binding_path);
 
         // Compile fragment shader
         let output_file = format!("{}/{}_ps.h", out_dir, module);
@@ -387,7 +386,7 @@ mod windows {
             shader_path,
             "ps_4_1",
         );
-        generate_rust_binding(&const_name, &output_file, &rust_binding_path);
+        generate_rust_binding(&const_name, &output_file, rust_binding_path);
     }
 
     fn compile_shader_impl(

crates/gpui/examples/grid_layout.rs 🔗

@@ -0,0 +1,80 @@
+use gpui::{
+    App, Application, Bounds, Context, Hsla, Window, WindowBounds, WindowOptions, div, prelude::*,
+    px, rgb, size,
+};
+
+// https://en.wikipedia.org/wiki/Holy_grail_(web_design)
+struct HolyGrailExample {}
+
+impl Render for HolyGrailExample {
+    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
+        let block = |color: Hsla| {
+            div()
+                .size_full()
+                .bg(color)
+                .border_1()
+                .border_dashed()
+                .rounded_md()
+                .border_color(gpui::white())
+                .items_center()
+        };
+
+        div()
+            .gap_1()
+            .grid()
+            .bg(rgb(0x505050))
+            .size(px(500.0))
+            .shadow_lg()
+            .border_1()
+            .size_full()
+            .grid_cols(5)
+            .grid_rows(5)
+            .child(
+                block(gpui::white())
+                    .row_span(1)
+                    .col_span_full()
+                    .child("Header"),
+            )
+            .child(
+                block(gpui::red())
+                    .col_span(1)
+                    .h_56()
+                    .child("Table of contents"),
+            )
+            .child(
+                block(gpui::green())
+                    .col_span(3)
+                    .row_span(3)
+                    .child("Content"),
+            )
+            .child(
+                block(gpui::blue())
+                    .col_span(1)
+                    .row_span(3)
+                    .child("AD :(")
+                    .text_color(gpui::white()),
+            )
+            .child(
+                block(gpui::black())
+                    .row_span(1)
+                    .col_span_full()
+                    .text_color(gpui::white())
+                    .child("Footer"),
+            )
+    }
+}
+
+fn main() {
+    Application::new().run(|cx: &mut App| {
+        let bounds = Bounds::centered(None, size(px(500.), px(500.0)), cx);
+        cx.open_window(
+            WindowOptions {
+                window_bounds: Some(WindowBounds::Windowed(bounds)),
+                ..Default::default()
+            },
+            |_, cx| cx.new(|_| HolyGrailExample {}),
+        )
+        .unwrap();
+        cx.activate(true);
+    });
+}

crates/gpui/examples/input.rs 🔗

@@ -137,14 +137,14 @@ impl TextInput {
     fn copy(&mut self, _: &Copy, _: &mut Window, cx: &mut Context<Self>) {
         if !self.selected_range.is_empty() {
             cx.write_to_clipboard(ClipboardItem::new_string(
-                (&self.content[self.selected_range.clone()]).to_string(),
+                self.content[self.selected_range.clone()].to_string(),
             ));
         }
     }
     fn cut(&mut self, _: &Cut, window: &mut Window, cx: &mut Context<Self>) {
         if !self.selected_range.is_empty() {
             cx.write_to_clipboard(ClipboardItem::new_string(
-                (&self.content[self.selected_range.clone()]).to_string(),
+                self.content[self.selected_range.clone()].to_string(),
             ));
             self.replace_text_in_range(None, "", window, cx)
         }
@@ -446,7 +446,7 @@ impl Element for TextElement {
         let (display_text, text_color) = if content.is_empty() {
             (input.placeholder.clone(), hsla(0., 0., 0., 0.2))
         } else {
-            (content.clone(), style.color)
+            (content, style.color)
         };
 
         let run = TextRun {
@@ -474,7 +474,7 @@ impl Element for TextElement {
                 },
                 TextRun {
                     len: display_text.len() - marked_range.end,
-                    ..run.clone()
+                    ..run
                 },
             ]
             .into_iter()
@@ -549,10 +549,10 @@ impl Element for TextElement {
         line.paint(bounds.origin, window.line_height(), window, cx)
             .unwrap();
 
-        if focus_handle.is_focused(window) {
-            if let Some(cursor) = prepaint.cursor.take() {
-                window.paint_quad(cursor);
-            }
+        if focus_handle.is_focused(window)
+            && let Some(cursor) = prepaint.cursor.take()
+        {
+            window.paint_quad(cursor);
         }
 
         self.input.update(cx, |input, _cx| {
@@ -595,9 +595,7 @@ impl Render for TextInput {
                     .w_full()
                     .p(px(4.))
                     .bg(white())
-                    .child(TextElement {
-                        input: cx.entity().clone(),
-                    }),
+                    .child(TextElement { input: cx.entity() }),
             )
     }
 }

crates/gpui/examples/set_menus.rs 🔗

@@ -1,5 +1,6 @@
 use gpui::{
-    App, Application, Context, Menu, MenuItem, Window, WindowOptions, actions, div, prelude::*, rgb,
+    App, Application, Context, Menu, MenuItem, SystemMenuType, Window, WindowOptions, actions, div,
+    prelude::*, rgb,
 };
 
 struct SetMenus;
@@ -27,7 +28,11 @@ fn main() {
         // Add menu items
         cx.set_menus(vec![Menu {
             name: "set_menus".into(),
-            items: vec![MenuItem::action("Quit", Quit)],
+            items: vec![
+                MenuItem::os_submenu("Services", SystemMenuType::Services),
+                MenuItem::separator(),
+                MenuItem::action("Quit", Quit),
+            ],
         }]);
         cx.open_window(WindowOptions::default(), |_, cx| cx.new(|_| SetMenus {}))
             .unwrap();

crates/gpui/examples/text.rs 🔗

@@ -155,7 +155,7 @@ impl RenderOnce for Specimen {
             .text_size(px(font_size * scale))
             .line_height(relative(line_height))
             .p(px(10.0))
-            .child(self.string.clone())
+            .child(self.string)
     }
 }
 

crates/gpui/src/action.rs 🔗

@@ -73,18 +73,18 @@ macro_rules! actions {
 /// - `name = "ActionName"` overrides the action's name. This must not contain `::`.
 ///
 /// - `no_json` causes the `build` method to always error and `action_json_schema` to return `None`,
-/// and allows actions not implement `serde::Serialize` and `schemars::JsonSchema`.
+///   and allows actions not implement `serde::Serialize` and `schemars::JsonSchema`.
 ///
 /// - `no_register` skips registering the action. This is useful for implementing the `Action` trait
-/// while not supporting invocation by name or JSON deserialization.
+///   while not supporting invocation by name or JSON deserialization.
 ///
 /// - `deprecated_aliases = ["editor::SomeAction"]` specifies deprecated old names for the action.
-/// These action names should *not* correspond to any actions that are registered. These old names
-/// can then still be used to refer to invoke this action. In Zed, the keymap JSON schema will
-/// accept these old names and provide warnings.
+///   These action names should *not* correspond to any actions that are registered. These old names
+///   can then still be used to refer to invoke this action. In Zed, the keymap JSON schema will
+///   accept these old names and provide warnings.
 ///
 /// - `deprecated = "Message about why this action is deprecation"` specifies a deprecation message.
-/// In Zed, the keymap JSON schema will cause this to be displayed as a warning.
+///   In Zed, the keymap JSON schema will cause this to be displayed as a warning.
 ///
 /// # Manual Implementation
 ///

crates/gpui/src/app.rs 🔗

@@ -277,6 +277,8 @@ pub struct App {
     pub(crate) release_listeners: SubscriberSet<EntityId, ReleaseListener>,
     pub(crate) global_observers: SubscriberSet<TypeId, Handler>,
     pub(crate) quit_observers: SubscriberSet<(), QuitHandler>,
+    pub(crate) restart_observers: SubscriberSet<(), Handler>,
+    pub(crate) restart_path: Option<PathBuf>,
     pub(crate) window_closed_observers: SubscriberSet<(), WindowClosedHandler>,
     pub(crate) layout_id_buffer: Vec<LayoutId>, // We recycle this memory across layout requests.
     pub(crate) propagate_event: bool,
@@ -349,6 +351,8 @@ impl App {
                 keyboard_layout_observers: SubscriberSet::new(),
                 global_observers: SubscriberSet::new(),
                 quit_observers: SubscriberSet::new(),
+                restart_observers: SubscriberSet::new(),
+                restart_path: None,
                 window_closed_observers: SubscriberSet::new(),
                 layout_id_buffer: Default::default(),
                 propagate_event: true,
@@ -364,7 +368,7 @@ impl App {
             }),
         });
 
-        init_app_menus(platform.as_ref(), &mut app.borrow_mut());
+        init_app_menus(platform.as_ref(), &app.borrow());
 
         platform.on_keyboard_layout_change(Box::new({
             let app = Rc::downgrade(&app);
@@ -812,8 +816,9 @@ impl App {
     pub fn prompt_for_new_path(
         &self,
         directory: &Path,
+        suggested_name: Option<&str>,
     ) -> oneshot::Receiver<Result<Option<PathBuf>>> {
-        self.platform.prompt_for_new_path(directory)
+        self.platform.prompt_for_new_path(directory, suggested_name)
     }
 
     /// Reveals the specified path at the platform level, such as in Finder on macOS.
@@ -832,8 +837,16 @@ impl App {
     }
 
     /// Restarts the application.
-    pub fn restart(&self, binary_path: Option<PathBuf>) {
-        self.platform.restart(binary_path)
+    pub fn restart(&mut self) {
+        self.restart_observers
+            .clone()
+            .retain(&(), |observer| observer(self));
+        self.platform.restart(self.restart_path.take())
+    }
+
+    /// Sets the path to use when restarting the application.
+    pub fn set_restart_path(&mut self, path: PathBuf) {
+        self.restart_path = Some(path);
     }
 
     /// Returns the HTTP client for the application.
@@ -1297,7 +1310,7 @@ impl App {
         T: 'static,
     {
         let window_handle = window.handle;
-        self.observe_release(&handle, move |entity, cx| {
+        self.observe_release(handle, move |entity, cx| {
             let _ = window_handle.update(cx, |_, window, cx| on_release(entity, window, cx));
         })
     }
@@ -1319,7 +1332,7 @@ impl App {
         }
 
         inner(
-            &mut self.keystroke_observers,
+            &self.keystroke_observers,
             Box::new(move |event, window, cx| {
                 f(event, window, cx);
                 true
@@ -1345,7 +1358,7 @@ impl App {
         }
 
         inner(
-            &mut self.keystroke_interceptors,
+            &self.keystroke_interceptors,
             Box::new(move |event, window, cx| {
                 f(event, window, cx);
                 true
@@ -1466,6 +1479,21 @@ impl App {
         subscription
     }
 
+    /// Register a callback to be invoked when the application is about to restart.
+    ///
+    /// These callbacks are called before any `on_app_quit` callbacks.
+    pub fn on_app_restart(&self, mut on_restart: impl 'static + FnMut(&mut App)) -> Subscription {
+        let (subscription, activate) = self.restart_observers.insert(
+            (),
+            Box::new(move |cx| {
+                on_restart(cx);
+                true
+            }),
+        );
+        activate();
+        subscription
+    }
+
     /// Register a callback to be invoked when a window is closed
     /// The window is no longer accessible at the point this callback is invoked.
     pub fn on_window_closed(&self, mut on_closed: impl FnMut(&mut App) + 'static) -> Subscription {
@@ -1488,12 +1516,11 @@ impl App {
     /// the bindings in the element tree, and any global action listeners.
     pub fn is_action_available(&mut self, action: &dyn Action) -> bool {
         let mut action_available = false;
-        if let Some(window) = self.active_window() {
-            if let Ok(window_action_available) =
+        if let Some(window) = self.active_window()
+            && let Ok(window_action_available) =
                 window.update(self, |_, window, cx| window.is_action_available(action, cx))
-            {
-                action_available = window_action_available;
-            }
+        {
+            action_available = window_action_available;
         }
 
         action_available
@@ -1578,27 +1605,26 @@ impl App {
                 .insert(action.as_any().type_id(), global_listeners);
         }
 
-        if self.propagate_event {
-            if let Some(mut global_listeners) = self
+        if self.propagate_event
+            && let Some(mut global_listeners) = self
                 .global_action_listeners
                 .remove(&action.as_any().type_id())
-            {
-                for listener in global_listeners.iter().rev() {
-                    listener(action.as_any(), DispatchPhase::Bubble, self);
-                    if !self.propagate_event {
-                        break;
-                    }
+        {
+            for listener in global_listeners.iter().rev() {
+                listener(action.as_any(), DispatchPhase::Bubble, self);
+                if !self.propagate_event {
+                    break;
                 }
+            }
 
-                global_listeners.extend(
-                    self.global_action_listeners
-                        .remove(&action.as_any().type_id())
-                        .unwrap_or_default(),
-                );
-
+            global_listeners.extend(
                 self.global_action_listeners
-                    .insert(action.as_any().type_id(), global_listeners);
-            }
+                    .remove(&action.as_any().type_id())
+                    .unwrap_or_default(),
+            );
+
+            self.global_action_listeners
+                .insert(action.as_any().type_id(), global_listeners);
         }
     }
 
@@ -1681,8 +1707,8 @@ impl App {
             .unwrap_or_else(|| {
                 is_first = true;
                 let future = A::load(source.clone(), self);
-                let task = self.background_executor().spawn(future).shared();
-                task
+
+                self.background_executor().spawn(future).shared()
             });
 
         self.loading_assets.insert(asset_id, Box::new(task.clone()));
@@ -1889,7 +1915,7 @@ impl AppContext for App {
         G: Global,
     {
         let mut g = self.global::<G>();
-        callback(&g, self)
+        callback(g, self)
     }
 }
 

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

@@ -465,7 +465,7 @@ impl VisualContext for AsyncWindowContext {
         V: Focusable,
     {
         self.window.update(self, |_, window, cx| {
-            view.read(cx).focus_handle(cx).clone().focus(window);
+            view.read(cx).focus_handle(cx).focus(window);
         })
     }
 }

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

@@ -164,6 +164,20 @@ impl<'a, T: 'static> Context<'a, T> {
         subscription
     }
 
+    /// Register a callback to be invoked when the application is about to restart.
+    pub fn on_app_restart(
+        &self,
+        mut on_restart: impl FnMut(&mut T, &mut App) + 'static,
+    ) -> Subscription
+    where
+        T: 'static,
+    {
+        let handle = self.weak_entity();
+        self.app.on_app_restart(move |cx| {
+            handle.update(cx, |entity, cx| on_restart(entity, cx)).ok();
+        })
+    }
+
     /// Arrange for the given function to be invoked whenever the application is quit.
     /// The future returned from this callback will be polled for up to [crate::SHUTDOWN_TIMEOUT] until the app fully quits.
     pub fn on_app_quit<Fut>(
@@ -175,20 +189,15 @@ impl<'a, T: 'static> Context<'a, T> {
         T: 'static,
     {
         let handle = self.weak_entity();
-        let (subscription, activate) = self.app.quit_observers.insert(
-            (),
-            Box::new(move |cx| {
-                let future = handle.update(cx, |entity, cx| on_quit(entity, cx)).ok();
-                async move {
-                    if let Some(future) = future {
-                        future.await;
-                    }
+        self.app.on_app_quit(move |cx| {
+            let future = handle.update(cx, |entity, cx| on_quit(entity, cx)).ok();
+            async move {
+                if let Some(future) = future {
+                    future.await;
                 }
-                .boxed_local()
-            }),
-        );
-        activate();
-        subscription
+            }
+            .boxed_local()
+        })
     }
 
     /// Tell GPUI that this entity has changed and observers of it should be notified.
@@ -463,7 +472,7 @@ impl<'a, T: 'static> Context<'a, T> {
 
         let view = self.weak_entity();
         inner(
-            &mut self.keystroke_observers,
+            &self.keystroke_observers,
             Box::new(move |event, window, cx| {
                 if let Some(view) = view.upgrade() {
                     view.update(cx, |view, cx| f(view, event, window, cx));
@@ -601,16 +610,16 @@ impl<'a, T: 'static> Context<'a, T> {
         let (subscription, activate) =
             window.new_focus_listener(Box::new(move |event, window, cx| {
                 view.update(cx, |view, cx| {
-                    if let Some(blurred_id) = event.previous_focus_path.last().copied() {
-                        if event.is_focus_out(focus_id) {
-                            let event = FocusOutEvent {
-                                blurred: WeakFocusHandle {
-                                    id: blurred_id,
-                                    handles: Arc::downgrade(&cx.focus_handles),
-                                },
-                            };
-                            listener(view, event, window, cx)
-                        }
+                    if let Some(blurred_id) = event.previous_focus_path.last().copied()
+                        && event.is_focus_out(focus_id)
+                    {
+                        let event = FocusOutEvent {
+                            blurred: WeakFocusHandle {
+                                id: blurred_id,
+                                handles: Arc::downgrade(&cx.focus_handles),
+                            },
+                        };
+                        listener(view, event, window, cx)
                     }
                 })
                 .is_ok()

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

@@ -231,14 +231,15 @@ impl AnyEntity {
         Self {
             entity_id: id,
             entity_type,
-            entity_map: entity_map.clone(),
             #[cfg(any(test, feature = "leak-detection"))]
             handle_id: entity_map
+                .clone()
                 .upgrade()
                 .unwrap()
                 .write()
                 .leak_detector
                 .handle_created(id),
+            entity_map,
         }
     }
 
@@ -661,7 +662,7 @@ pub struct WeakEntity<T> {
 
 impl<T> std::fmt::Debug for WeakEntity<T> {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        f.debug_struct(&type_name::<Self>())
+        f.debug_struct(type_name::<Self>())
             .field("entity_id", &self.any_entity.entity_id)
             .field("entity_type", &type_name::<T>())
             .finish()
@@ -786,7 +787,7 @@ impl<T: 'static> PartialOrd for WeakEntity<T> {
 
 #[cfg(any(test, feature = "leak-detection"))]
 static LEAK_BACKTRACE: std::sync::LazyLock<bool> =
-    std::sync::LazyLock::new(|| std::env::var("LEAK_BACKTRACE").map_or(false, |b| !b.is_empty()));
+    std::sync::LazyLock::new(|| std::env::var("LEAK_BACKTRACE").is_ok_and(|b| !b.is_empty()));
 
 #[cfg(any(test, feature = "leak-detection"))]
 #[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq)]

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

@@ -134,7 +134,7 @@ impl TestAppContext {
             app: App::new_app(platform.clone(), asset_source, http_client),
             background_executor,
             foreground_executor,
-            dispatcher: dispatcher.clone(),
+            dispatcher,
             test_platform: platform,
             text_system,
             fn_name,
@@ -192,6 +192,7 @@ impl TestAppContext {
         &self.foreground_executor
     }
 
+    #[expect(clippy::wrong_self_convention)]
     fn new<T: 'static>(&mut self, build_entity: impl FnOnce(&mut Context<T>) -> T) -> Entity<T> {
         let mut cx = self.app.borrow_mut();
         cx.new(build_entity)
@@ -219,7 +220,7 @@ impl TestAppContext {
         let mut cx = self.app.borrow_mut();
 
         // Some tests rely on the window size matching the bounds of the test display
-        let bounds = Bounds::maximized(None, &mut cx);
+        let bounds = Bounds::maximized(None, &cx);
         cx.open_window(
             WindowOptions {
                 window_bounds: Some(WindowBounds::Windowed(bounds)),
@@ -233,7 +234,7 @@ impl TestAppContext {
     /// Adds a new window with no content.
     pub fn add_empty_window(&mut self) -> &mut VisualTestContext {
         let mut cx = self.app.borrow_mut();
-        let bounds = Bounds::maximized(None, &mut cx);
+        let bounds = Bounds::maximized(None, &cx);
         let window = cx
             .open_window(
                 WindowOptions {
@@ -244,7 +245,7 @@ impl TestAppContext {
             )
             .unwrap();
         drop(cx);
-        let cx = VisualTestContext::from_window(*window.deref(), self).as_mut();
+        let cx = VisualTestContext::from_window(*window.deref(), self).into_mut();
         cx.run_until_parked();
         cx
     }
@@ -261,7 +262,7 @@ impl TestAppContext {
         V: 'static + Render,
     {
         let mut cx = self.app.borrow_mut();
-        let bounds = Bounds::maximized(None, &mut cx);
+        let bounds = Bounds::maximized(None, &cx);
         let window = cx
             .open_window(
                 WindowOptions {
@@ -273,7 +274,7 @@ impl TestAppContext {
             .unwrap();
         drop(cx);
         let view = window.root(self).unwrap();
-        let cx = VisualTestContext::from_window(*window.deref(), self).as_mut();
+        let cx = VisualTestContext::from_window(*window.deref(), self).into_mut();
         cx.run_until_parked();
 
         // it might be nice to try and cleanup these at the end of each test.
@@ -338,7 +339,7 @@ impl TestAppContext {
 
     /// Returns all windows open in the test.
     pub fn windows(&self) -> Vec<AnyWindowHandle> {
-        self.app.borrow().windows().clone()
+        self.app.borrow().windows()
     }
 
     /// Run the given task on the main thread.
@@ -585,7 +586,7 @@ impl<V: 'static> Entity<V> {
         cx.executor().advance_clock(advance_clock_by);
 
         async move {
-            let notification = crate::util::timeout(duration, rx.recv())
+            let notification = crate::util::smol_timeout(duration, rx.recv())
                 .await
                 .expect("next notification timed out");
             drop(subscription);
@@ -618,7 +619,7 @@ impl<V> Entity<V> {
                 }
             }),
             cx.subscribe(self, {
-                let mut tx = tx.clone();
+                let mut tx = tx;
                 move |_, _: &Evt, _| {
                     tx.blocking_send(()).ok();
                 }
@@ -629,7 +630,7 @@ impl<V> Entity<V> {
         let handle = self.downgrade();
 
         async move {
-            crate::util::timeout(Duration::from_secs(1), async move {
+            crate::util::smol_timeout(Duration::from_secs(1), async move {
                 loop {
                     {
                         let cx = cx.borrow();
@@ -882,7 +883,7 @@ impl VisualTestContext {
 
     /// Get an &mut VisualTestContext (which is mostly what you need to pass to other methods).
     /// This method internally retains the VisualTestContext until the end of the test.
-    pub fn as_mut(self) -> &'static mut Self {
+    pub fn into_mut(self) -> &'static mut Self {
         let ptr = Box::into_raw(Box::new(self));
         // safety: on_quit will be called after the test has finished.
         // the executor will ensure that all tasks related to the test have stopped.
@@ -1025,7 +1026,7 @@ impl VisualContext for VisualTestContext {
     fn focus<V: crate::Focusable>(&mut self, view: &Entity<V>) -> Self::Result<()> {
         self.window
             .update(&mut self.cx, |_, window, cx| {
-                view.read(cx).focus_handle(cx).clone().focus(window)
+                view.read(cx).focus_handle(cx).focus(window)
             })
             .unwrap()
     }

crates/gpui/src/arena.rs 🔗

@@ -142,7 +142,7 @@ impl Arena {
                 if self.current_chunk_index >= self.chunks.len() {
                     self.chunks.push(Chunk::new(self.chunk_size));
                     assert_eq!(self.current_chunk_index, self.chunks.len() - 1);
-                    log::info!(
+                    log::trace!(
                         "increased element arena capacity to {}kb",
                         self.capacity() / 1024,
                     );

crates/gpui/src/color.rs 🔗

@@ -905,9 +905,9 @@ mod tests {
         assert_eq!(background.solid, color);
 
         assert_eq!(background.opacity(0.5).solid, color.opacity(0.5));
-        assert_eq!(background.is_transparent(), false);
+        assert!(!background.is_transparent());
         background.solid = hsla(0.0, 0.0, 0.0, 0.0);
-        assert_eq!(background.is_transparent(), true);
+        assert!(background.is_transparent());
     }
 
     #[test]
@@ -921,7 +921,7 @@ mod tests {
 
         assert_eq!(background.opacity(0.5).colors[0], from.opacity(0.5));
         assert_eq!(background.opacity(0.5).colors[1], to.opacity(0.5));
-        assert_eq!(background.is_transparent(), false);
-        assert_eq!(background.opacity(0.0).is_transparent(), true);
+        assert!(!background.is_transparent());
+        assert!(background.opacity(0.0).is_transparent());
     }
 }

crates/gpui/src/element.rs 🔗

@@ -603,10 +603,8 @@ impl AnyElement {
 
         self.0.prepaint(window, cx);
 
-        if !focus_assigned {
-            if let Some(focus_id) = window.next_frame.focus {
-                return FocusHandle::for_id(focus_id, &cx.focus_handles);
-            }
+        if !focus_assigned && let Some(focus_id) = window.next_frame.focus {
+            return FocusHandle::for_id(focus_id, &cx.focus_handles);
         }
 
         None

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

@@ -27,6 +27,7 @@ use crate::{
 use collections::HashMap;
 use refineable::Refineable;
 use smallvec::SmallVec;
+use stacksafe::{StackSafe, stacksafe};
 use std::{
     any::{Any, TypeId},
     cell::RefCell,
@@ -285,21 +286,20 @@ impl Interactivity {
     {
         self.mouse_move_listeners
             .push(Box::new(move |event, phase, hitbox, window, cx| {
-                if phase == DispatchPhase::Capture {
-                    if let Some(drag) = &cx.active_drag {
-                        if drag.value.as_ref().type_id() == TypeId::of::<T>() {
-                            (listener)(
-                                &DragMoveEvent {
-                                    event: event.clone(),
-                                    bounds: hitbox.bounds,
-                                    drag: PhantomData,
-                                    dragged_item: Arc::clone(&drag.value),
-                                },
-                                window,
-                                cx,
-                            );
-                        }
-                    }
+                if phase == DispatchPhase::Capture
+                    && let Some(drag) = &cx.active_drag
+                    && drag.value.as_ref().type_id() == TypeId::of::<T>()
+                {
+                    (listener)(
+                        &DragMoveEvent {
+                            event: event.clone(),
+                            bounds: hitbox.bounds,
+                            drag: PhantomData,
+                            dragged_item: Arc::clone(&drag.value),
+                        },
+                        window,
+                        cx,
+                    );
                 }
             }));
     }
@@ -1195,7 +1195,7 @@ pub fn div() -> Div {
 /// A [`Div`] element, the all-in-one element for building complex UIs in GPUI
 pub struct Div {
     interactivity: Interactivity,
-    children: SmallVec<[AnyElement; 2]>,
+    children: SmallVec<[StackSafe<AnyElement>; 2]>,
     prepaint_listener: Option<Box<dyn Fn(Vec<Bounds<Pixels>>, &mut Window, &mut App) + 'static>>,
     image_cache: Option<Box<dyn ImageCacheProvider>>,
 }
@@ -1256,7 +1256,8 @@ impl InteractiveElement for Div {
 
 impl ParentElement for Div {
     fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
-        self.children.extend(elements)
+        self.children
+            .extend(elements.into_iter().map(StackSafe::new))
     }
 }
 
@@ -1272,6 +1273,7 @@ impl Element for Div {
         self.interactivity.source_location()
     }
 
+    #[stacksafe]
     fn request_layout(
         &mut self,
         global_id: Option<&GlobalElementId>,
@@ -1307,6 +1309,7 @@ impl Element for Div {
         (layout_id, DivFrameState { child_layout_ids })
     }
 
+    #[stacksafe]
     fn prepaint(
         &mut self,
         global_id: Option<&GlobalElementId>,
@@ -1376,6 +1379,7 @@ impl Element for Div {
         )
     }
 
+    #[stacksafe]
     fn paint(
         &mut self,
         global_id: Option<&GlobalElementId>,
@@ -1509,15 +1513,14 @@ impl Interactivity {
                 let mut element_state =
                     element_state.map(|element_state| element_state.unwrap_or_default());
 
-                if let Some(element_state) = element_state.as_ref() {
-                    if cx.has_active_drag() {
-                        if let Some(pending_mouse_down) = element_state.pending_mouse_down.as_ref()
-                        {
-                            *pending_mouse_down.borrow_mut() = None;
-                        }
-                        if let Some(clicked_state) = element_state.clicked_state.as_ref() {
-                            *clicked_state.borrow_mut() = ElementClickedState::default();
-                        }
+                if let Some(element_state) = element_state.as_ref()
+                    && cx.has_active_drag()
+                {
+                    if let Some(pending_mouse_down) = element_state.pending_mouse_down.as_ref() {
+                        *pending_mouse_down.borrow_mut() = None;
+                    }
+                    if let Some(clicked_state) = element_state.clicked_state.as_ref() {
+                        *clicked_state.borrow_mut() = ElementClickedState::default();
                     }
                 }
 
@@ -1525,35 +1528,35 @@ impl Interactivity {
                 // If there's an explicit focus handle we're tracking, use that. Otherwise
                 // create a new handle and store it in the element state, which lives for as
                 // as frames contain an element with this id.
-                if self.focusable && self.tracked_focus_handle.is_none() {
-                    if let Some(element_state) = element_state.as_mut() {
-                        let mut handle = element_state
-                            .focus_handle
-                            .get_or_insert_with(|| cx.focus_handle())
-                            .clone()
-                            .tab_stop(false);
-
-                        if let Some(index) = self.tab_index {
-                            handle = handle.tab_index(index).tab_stop(true);
-                        }
-
-                        self.tracked_focus_handle = Some(handle);
+                if self.focusable
+                    && self.tracked_focus_handle.is_none()
+                    && let Some(element_state) = element_state.as_mut()
+                {
+                    let mut handle = element_state
+                        .focus_handle
+                        .get_or_insert_with(|| cx.focus_handle())
+                        .clone()
+                        .tab_stop(false);
+
+                    if let Some(index) = self.tab_index {
+                        handle = handle.tab_index(index).tab_stop(true);
                     }
+
+                    self.tracked_focus_handle = Some(handle);
                 }
 
                 if let Some(scroll_handle) = self.tracked_scroll_handle.as_ref() {
                     self.scroll_offset = Some(scroll_handle.0.borrow().offset.clone());
-                } else if self.base_style.overflow.x == Some(Overflow::Scroll)
-                    || self.base_style.overflow.y == Some(Overflow::Scroll)
+                } else if (self.base_style.overflow.x == Some(Overflow::Scroll)
+                    || self.base_style.overflow.y == Some(Overflow::Scroll))
+                    && let Some(element_state) = element_state.as_mut()
                 {
-                    if let Some(element_state) = element_state.as_mut() {
-                        self.scroll_offset = Some(
-                            element_state
-                                .scroll_offset
-                                .get_or_insert_with(Rc::default)
-                                .clone(),
-                        );
-                    }
+                    self.scroll_offset = Some(
+                        element_state
+                            .scroll_offset
+                            .get_or_insert_with(Rc::default)
+                            .clone(),
+                    );
                 }
 
                 let style = self.compute_style_internal(None, element_state.as_mut(), window, cx);
@@ -2026,26 +2029,27 @@ impl Interactivity {
             let hitbox = hitbox.clone();
             window.on_mouse_event({
                 move |_: &MouseUpEvent, phase, window, cx| {
-                    if let Some(drag) = &cx.active_drag {
-                        if phase == DispatchPhase::Bubble && hitbox.is_hovered(window) {
-                            let drag_state_type = drag.value.as_ref().type_id();
-                            for (drop_state_type, listener) in &drop_listeners {
-                                if *drop_state_type == drag_state_type {
-                                    let drag = cx
-                                        .active_drag
-                                        .take()
-                                        .expect("checked for type drag state type above");
-
-                                    let mut can_drop = true;
-                                    if let Some(predicate) = &can_drop_predicate {
-                                        can_drop = predicate(drag.value.as_ref(), window, cx);
-                                    }
+                    if let Some(drag) = &cx.active_drag
+                        && phase == DispatchPhase::Bubble
+                        && hitbox.is_hovered(window)
+                    {
+                        let drag_state_type = drag.value.as_ref().type_id();
+                        for (drop_state_type, listener) in &drop_listeners {
+                            if *drop_state_type == drag_state_type {
+                                let drag = cx
+                                    .active_drag
+                                    .take()
+                                    .expect("checked for type drag state type above");
+
+                                let mut can_drop = true;
+                                if let Some(predicate) = &can_drop_predicate {
+                                    can_drop = predicate(drag.value.as_ref(), window, cx);
+                                }
 
-                                    if can_drop {
-                                        listener(drag.value.as_ref(), window, cx);
-                                        window.refresh();
-                                        cx.stop_propagation();
-                                    }
+                                if can_drop {
+                                    listener(drag.value.as_ref(), window, cx);
+                                    window.refresh();
+                                    cx.stop_propagation();
                                 }
                             }
                         }
@@ -2089,31 +2093,24 @@ impl Interactivity {
                         }
 
                         let mut pending_mouse_down = pending_mouse_down.borrow_mut();
-                        if let Some(mouse_down) = pending_mouse_down.clone() {
-                            if !cx.has_active_drag()
-                                && (event.position - mouse_down.position).magnitude()
-                                    > DRAG_THRESHOLD
-                            {
-                                if let Some((drag_value, drag_listener)) = drag_listener.take() {
-                                    *clicked_state.borrow_mut() = ElementClickedState::default();
-                                    let cursor_offset = event.position - hitbox.origin;
-                                    let drag = (drag_listener)(
-                                        drag_value.as_ref(),
-                                        cursor_offset,
-                                        window,
-                                        cx,
-                                    );
-                                    cx.active_drag = Some(AnyDrag {
-                                        view: drag,
-                                        value: drag_value,
-                                        cursor_offset,
-                                        cursor_style: drag_cursor_style,
-                                    });
-                                    pending_mouse_down.take();
-                                    window.refresh();
-                                    cx.stop_propagation();
-                                }
-                            }
+                        if let Some(mouse_down) = pending_mouse_down.clone()
+                            && !cx.has_active_drag()
+                            && (event.position - mouse_down.position).magnitude() > DRAG_THRESHOLD
+                            && let Some((drag_value, drag_listener)) = drag_listener.take()
+                        {
+                            *clicked_state.borrow_mut() = ElementClickedState::default();
+                            let cursor_offset = event.position - hitbox.origin;
+                            let drag =
+                                (drag_listener)(drag_value.as_ref(), cursor_offset, window, cx);
+                            cx.active_drag = Some(AnyDrag {
+                                view: drag,
+                                value: drag_value,
+                                cursor_offset,
+                                cursor_style: drag_cursor_style,
+                            });
+                            pending_mouse_down.take();
+                            window.refresh();
+                            cx.stop_propagation();
                         }
                     }
                 });
@@ -2277,7 +2274,7 @@ impl Interactivity {
                 window.on_mouse_event(move |_: &MouseDownEvent, phase, window, _cx| {
                     if phase == DispatchPhase::Bubble && !window.default_prevented() {
                         let group_hovered = active_group_hitbox
-                            .map_or(false, |group_hitbox_id| group_hitbox_id.is_hovered(window));
+                            .is_some_and(|group_hitbox_id| group_hitbox_id.is_hovered(window));
                         let element_hovered = hitbox.is_hovered(window);
                         if group_hovered || element_hovered {
                             *active_state.borrow_mut() = ElementClickedState {
@@ -2423,33 +2420,32 @@ impl Interactivity {
         style.refine(&self.base_style);
 
         if let Some(focus_handle) = self.tracked_focus_handle.as_ref() {
-            if let Some(in_focus_style) = self.in_focus_style.as_ref() {
-                if focus_handle.within_focused(window, cx) {
-                    style.refine(in_focus_style);
-                }
+            if let Some(in_focus_style) = self.in_focus_style.as_ref()
+                && focus_handle.within_focused(window, cx)
+            {
+                style.refine(in_focus_style);
             }
 
-            if let Some(focus_style) = self.focus_style.as_ref() {
-                if focus_handle.is_focused(window) {
-                    style.refine(focus_style);
-                }
+            if let Some(focus_style) = self.focus_style.as_ref()
+                && focus_handle.is_focused(window)
+            {
+                style.refine(focus_style);
             }
         }
 
         if let Some(hitbox) = hitbox {
             if !cx.has_active_drag() {
-                if let Some(group_hover) = self.group_hover_style.as_ref() {
-                    if let Some(group_hitbox_id) = GroupHitboxes::get(&group_hover.group, cx) {
-                        if group_hitbox_id.is_hovered(window) {
-                            style.refine(&group_hover.style);
-                        }
-                    }
+                if let Some(group_hover) = self.group_hover_style.as_ref()
+                    && let Some(group_hitbox_id) = GroupHitboxes::get(&group_hover.group, cx)
+                    && group_hitbox_id.is_hovered(window)
+                {
+                    style.refine(&group_hover.style);
                 }
 
-                if let Some(hover_style) = self.hover_style.as_ref() {
-                    if hitbox.is_hovered(window) {
-                        style.refine(hover_style);
-                    }
+                if let Some(hover_style) = self.hover_style.as_ref()
+                    && hitbox.is_hovered(window)
+                {
+                    style.refine(hover_style);
                 }
             }
 
@@ -2463,12 +2459,10 @@ impl Interactivity {
                     for (state_type, group_drag_style) in &self.group_drag_over_styles {
                         if let Some(group_hitbox_id) =
                             GroupHitboxes::get(&group_drag_style.group, cx)
+                            && *state_type == drag.value.as_ref().type_id()
+                            && group_hitbox_id.is_hovered(window)
                         {
-                            if *state_type == drag.value.as_ref().type_id()
-                                && group_hitbox_id.is_hovered(window)
-                            {
-                                style.refine(&group_drag_style.style);
-                            }
+                            style.refine(&group_drag_style.style);
                         }
                     }
 
@@ -2490,16 +2484,16 @@ impl Interactivity {
                 .clicked_state
                 .get_or_insert_with(Default::default)
                 .borrow();
-            if clicked_state.group {
-                if let Some(group) = self.group_active_style.as_ref() {
-                    style.refine(&group.style)
-                }
+            if clicked_state.group
+                && let Some(group) = self.group_active_style.as_ref()
+            {
+                style.refine(&group.style)
             }
 
-            if let Some(active_style) = self.active_style.as_ref() {
-                if clicked_state.element {
-                    style.refine(active_style)
-                }
+            if let Some(active_style) = self.active_style.as_ref()
+                && clicked_state.element
+            {
+                style.refine(active_style)
             }
         }
 
@@ -2620,7 +2614,7 @@ pub(crate) fn register_tooltip_mouse_handlers(
     window.on_mouse_event({
         let active_tooltip = active_tooltip.clone();
         move |_: &MouseDownEvent, _phase, window: &mut Window, _cx| {
-            if !tooltip_id.map_or(false, |tooltip_id| tooltip_id.is_hovered(window)) {
+            if !tooltip_id.is_some_and(|tooltip_id| tooltip_id.is_hovered(window)) {
                 clear_active_tooltip_if_not_hoverable(&active_tooltip, window);
             }
         }
@@ -2629,7 +2623,7 @@ pub(crate) fn register_tooltip_mouse_handlers(
     window.on_mouse_event({
         let active_tooltip = active_tooltip.clone();
         move |_: &ScrollWheelEvent, _phase, window: &mut Window, _cx| {
-            if !tooltip_id.map_or(false, |tooltip_id| tooltip_id.is_hovered(window)) {
+            if !tooltip_id.is_some_and(|tooltip_id| tooltip_id.is_hovered(window)) {
                 clear_active_tooltip_if_not_hoverable(&active_tooltip, window);
             }
         }
@@ -2785,7 +2779,7 @@ fn handle_tooltip_check_visible_and_update(
 
     match action {
         Action::None => {}
-        Action::Hide => clear_active_tooltip(&active_tooltip, window),
+        Action::Hide => clear_active_tooltip(active_tooltip, window),
         Action::ScheduleHide(tooltip) => {
             let delayed_hide_task = window.spawn(cx, {
                 let active_tooltip = active_tooltip.clone();

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

@@ -64,7 +64,7 @@ mod any_image_cache {
         cx: &mut App,
     ) -> Option<Result<Arc<RenderImage>, ImageCacheError>> {
         let image_cache = image_cache.clone().downcast::<I>().unwrap();
-        return image_cache.update(cx, |image_cache, cx| image_cache.load(resource, window, cx));
+        image_cache.update(cx, |image_cache, cx| image_cache.load(resource, window, cx))
     }
 }
 
@@ -297,10 +297,10 @@ impl RetainAllImageCache {
     /// Remove the image from the cache by the given source.
     pub fn remove(&mut self, source: &Resource, window: &mut Window, cx: &mut App) {
         let hash = hash(source);
-        if let Some(mut item) = self.0.remove(&hash) {
-            if let Some(Ok(image)) = item.get() {
-                cx.drop_image(image, Some(window));
-            }
+        if let Some(mut item) = self.0.remove(&hash)
+            && let Some(Ok(image)) = item.get()
+        {
+            cx.drop_image(image, Some(window));
         }
     }
 

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

@@ -379,13 +379,12 @@ impl Element for Img {
                         None => {
                             if let Some(state) = &mut state {
                                 if let Some((started_loading, _)) = state.started_loading {
-                                    if started_loading.elapsed() > LOADING_DELAY {
-                                        if let Some(loading) = self.style.loading.as_ref() {
-                                            let mut element = loading();
-                                            replacement_id =
-                                                Some(element.request_layout(window, cx));
-                                            layout_state.replacement = Some(element);
-                                        }
+                                    if started_loading.elapsed() > LOADING_DELAY
+                                        && let Some(loading) = self.style.loading.as_ref()
+                                    {
+                                        let mut element = loading();
+                                        replacement_id = Some(element.request_layout(window, cx));
+                                        layout_state.replacement = Some(element);
                                     }
                                 } else {
                                     let current_view = window.current_view();
@@ -476,7 +475,7 @@ impl Element for Img {
                         .paint_image(
                             new_bounds,
                             corner_radii,
-                            data.clone(),
+                            data,
                             layout_state.frame_index,
                             self.style.grayscale,
                         )

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

@@ -732,46 +732,44 @@ impl StateInner {
                         item.element.prepaint_at(item_origin, window, cx);
                     });
 
-                    if let Some(autoscroll_bounds) = window.take_autoscroll() {
-                        if autoscroll {
-                            if autoscroll_bounds.top() < bounds.top() {
-                                return Err(ListOffset {
-                                    item_ix: item.index,
-                                    offset_in_item: autoscroll_bounds.top() - item_origin.y,
-                                });
-                            } else if autoscroll_bounds.bottom() > bounds.bottom() {
-                                let mut cursor = self.items.cursor::<Count>(&());
-                                cursor.seek(&Count(item.index), Bias::Right);
-                                let mut height = bounds.size.height - padding.top - padding.bottom;
-
-                                // Account for the height of the element down until the autoscroll bottom.
-                                height -= autoscroll_bounds.bottom() - item_origin.y;
-
-                                // Keep decreasing the scroll top until we fill all the available space.
-                                while height > Pixels::ZERO {
-                                    cursor.prev();
-                                    let Some(item) = cursor.item() else { break };
-
-                                    let size = item.size().unwrap_or_else(|| {
-                                        let mut item = render_item(cursor.start().0, window, cx);
-                                        let item_available_size = size(
-                                            bounds.size.width.into(),
-                                            AvailableSpace::MinContent,
-                                        );
-                                        item.layout_as_root(item_available_size, window, cx)
-                                    });
-                                    height -= size.height;
-                                }
-
-                                return Err(ListOffset {
-                                    item_ix: cursor.start().0,
-                                    offset_in_item: if height < Pixels::ZERO {
-                                        -height
-                                    } else {
-                                        Pixels::ZERO
-                                    },
+                    if let Some(autoscroll_bounds) = window.take_autoscroll()
+                        && autoscroll
+                    {
+                        if autoscroll_bounds.top() < bounds.top() {
+                            return Err(ListOffset {
+                                item_ix: item.index,
+                                offset_in_item: autoscroll_bounds.top() - item_origin.y,
+                            });
+                        } else if autoscroll_bounds.bottom() > bounds.bottom() {
+                            let mut cursor = self.items.cursor::<Count>(&());
+                            cursor.seek(&Count(item.index), Bias::Right);
+                            let mut height = bounds.size.height - padding.top - padding.bottom;
+
+                            // Account for the height of the element down until the autoscroll bottom.
+                            height -= autoscroll_bounds.bottom() - item_origin.y;
+
+                            // Keep decreasing the scroll top until we fill all the available space.
+                            while height > Pixels::ZERO {
+                                cursor.prev();
+                                let Some(item) = cursor.item() else { break };
+
+                                let size = item.size().unwrap_or_else(|| {
+                                    let mut item = render_item(cursor.start().0, window, cx);
+                                    let item_available_size =
+                                        size(bounds.size.width.into(), AvailableSpace::MinContent);
+                                    item.layout_as_root(item_available_size, window, cx)
                                 });
+                                height -= size.height;
                             }
+
+                            return Err(ListOffset {
+                                item_ix: cursor.start().0,
+                                offset_in_item: if height < Pixels::ZERO {
+                                    -height
+                                } else {
+                                    Pixels::ZERO
+                                },
+                            });
                         }
                     }
 
@@ -940,9 +938,10 @@ impl Element for List {
         let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
 
         // If the width of the list has changed, invalidate all cached item heights
-        if state.last_layout_bounds.map_or(true, |last_bounds| {
-            last_bounds.size.width != bounds.size.width
-        }) {
+        if state
+            .last_layout_bounds
+            .is_none_or(|last_bounds| last_bounds.size.width != bounds.size.width)
+        {
             let new_items = SumTree::from_iter(
                 state.items.iter().map(|item| ListItem::Unmeasured {
                     focus_handle: item.focus_handle(),

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

@@ -326,7 +326,7 @@ impl TextLayout {
             vec![text_style.to_run(text.len())]
         };
 
-        let layout_id = window.request_measured_layout(Default::default(), {
+        window.request_measured_layout(Default::default(), {
             let element_state = self.clone();
 
             move |known_dimensions, available_space, window, cx| {
@@ -356,12 +356,11 @@ impl TextLayout {
                         (None, "".into())
                     };
 
-                if let Some(text_layout) = element_state.0.borrow().as_ref() {
-                    if text_layout.size.is_some()
-                        && (wrap_width.is_none() || wrap_width == text_layout.wrap_width)
-                    {
-                        return text_layout.size.unwrap();
-                    }
+                if let Some(text_layout) = element_state.0.borrow().as_ref()
+                    && text_layout.size.is_some()
+                    && (wrap_width.is_none() || wrap_width == text_layout.wrap_width)
+                {
+                    return text_layout.size.unwrap();
                 }
 
                 let mut line_wrapper = cx.text_system().line_wrapper(text_style.font(), font_size);
@@ -417,9 +416,7 @@ impl TextLayout {
 
                 size
             }
-        });
-
-        layout_id
+        })
     }
 
     fn prepaint(&self, bounds: Bounds<Pixels>, text: &str) {
@@ -763,14 +760,13 @@ impl Element for InteractiveText {
                 let mut interactive_state = interactive_state.unwrap_or_default();
                 if let Some(click_listener) = self.click_listener.take() {
                     let mouse_position = window.mouse_position();
-                    if let Ok(ix) = text_layout.index_for_position(mouse_position) {
-                        if self
+                    if let Ok(ix) = text_layout.index_for_position(mouse_position)
+                        && self
                             .clickable_ranges
                             .iter()
                             .any(|range| range.contains(&ix))
-                        {
-                            window.set_cursor_style(crate::CursorStyle::PointingHand, hitbox)
-                        }
+                    {
+                        window.set_cursor_style(crate::CursorStyle::PointingHand, hitbox)
                     }
 
                     let text_layout = text_layout.clone();
@@ -803,13 +799,13 @@ impl Element for InteractiveText {
                     } else {
                         let hitbox = hitbox.clone();
                         window.on_mouse_event(move |event: &MouseDownEvent, phase, window, _| {
-                            if phase == DispatchPhase::Bubble && hitbox.is_hovered(window) {
-                                if let Ok(mouse_down_index) =
+                            if phase == DispatchPhase::Bubble
+                                && hitbox.is_hovered(window)
+                                && let Ok(mouse_down_index) =
                                     text_layout.index_for_position(event.position)
-                                {
-                                    mouse_down.set(Some(mouse_down_index));
-                                    window.refresh();
-                                }
+                            {
+                                mouse_down.set(Some(mouse_down_index));
+                                window.refresh();
                             }
                         });
                     }

crates/gpui/src/geometry.rs 🔗

@@ -9,12 +9,14 @@ use refineable::Refineable;
 use schemars::{JsonSchema, json_schema};
 use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
 use std::borrow::Cow;
+use std::ops::Range;
 use std::{
     cmp::{self, PartialOrd},
     fmt::{self, Display},
     hash::Hash,
     ops::{Add, Div, Mul, MulAssign, Neg, Sub},
 };
+use taffy::prelude::{TaffyGridLine, TaffyGridSpan};
 
 use crate::{App, DisplayId};
 
@@ -1044,7 +1046,7 @@ where
             size: self.size.clone()
                 + size(
                     amount.left.clone() + amount.right.clone(),
-                    amount.top.clone() + amount.bottom.clone(),
+                    amount.top.clone() + amount.bottom,
                 ),
         }
     }
@@ -1157,10 +1159,10 @@ where
     /// Computes the space available within outer bounds.
     pub fn space_within(&self, outer: &Self) -> Edges<T> {
         Edges {
-            top: self.top().clone() - outer.top().clone(),
-            right: outer.right().clone() - self.right().clone(),
-            bottom: outer.bottom().clone() - self.bottom().clone(),
-            left: self.left().clone() - outer.left().clone(),
+            top: self.top() - outer.top(),
+            right: outer.right() - self.right(),
+            bottom: outer.bottom() - self.bottom(),
+            left: self.left() - outer.left(),
         }
     }
 }
@@ -1639,7 +1641,7 @@ impl Bounds<Pixels> {
     }
 
     /// Convert the bounds from logical pixels to physical pixels
-    pub fn to_device_pixels(&self, factor: f32) -> Bounds<DevicePixels> {
+    pub fn to_device_pixels(self, factor: f32) -> Bounds<DevicePixels> {
         Bounds {
             origin: point(
                 DevicePixels((self.origin.x.0 * factor).round() as i32),
@@ -1710,7 +1712,7 @@ where
             top: self.top.clone() * rhs.top,
             right: self.right.clone() * rhs.right,
             bottom: self.bottom.clone() * rhs.bottom,
-            left: self.left.clone() * rhs.left,
+            left: self.left * rhs.left,
         }
     }
 }
@@ -1955,7 +1957,7 @@ impl Edges<DefiniteLength> {
     /// assert_eq!(edges_in_pixels.bottom, px(32.0)); // 2 rems
     /// assert_eq!(edges_in_pixels.left, px(50.0)); // 25% of parent width
     /// ```
-    pub fn to_pixels(&self, parent_size: Size<AbsoluteLength>, rem_size: Pixels) -> Edges<Pixels> {
+    pub fn to_pixels(self, parent_size: Size<AbsoluteLength>, rem_size: Pixels) -> Edges<Pixels> {
         Edges {
             top: self.top.to_pixels(parent_size.height, rem_size),
             right: self.right.to_pixels(parent_size.width, rem_size),
@@ -2025,7 +2027,7 @@ impl Edges<AbsoluteLength> {
     /// assert_eq!(edges_in_pixels.bottom, px(20.0)); // Already in pixels
     /// assert_eq!(edges_in_pixels.left, px(32.0)); // 2 rems converted to pixels
     /// ```
-    pub fn to_pixels(&self, rem_size: Pixels) -> Edges<Pixels> {
+    pub fn to_pixels(self, rem_size: Pixels) -> Edges<Pixels> {
         Edges {
             top: self.top.to_pixels(rem_size),
             right: self.right.to_pixels(rem_size),
@@ -2270,7 +2272,7 @@ impl Corners<AbsoluteLength> {
     /// assert_eq!(corners_in_pixels.bottom_right, Pixels(30.0));
     /// assert_eq!(corners_in_pixels.bottom_left, Pixels(32.0)); // 2 rems converted to pixels
     /// ```
-    pub fn to_pixels(&self, rem_size: Pixels) -> Corners<Pixels> {
+    pub fn to_pixels(self, rem_size: Pixels) -> Corners<Pixels> {
         Corners {
             top_left: self.top_left.to_pixels(rem_size),
             top_right: self.top_right.to_pixels(rem_size),
@@ -2409,7 +2411,7 @@ where
             top_left: self.top_left.clone() * rhs.top_left,
             top_right: self.top_right.clone() * rhs.top_right,
             bottom_right: self.bottom_right.clone() * rhs.bottom_right,
-            bottom_left: self.bottom_left.clone() * rhs.bottom_left,
+            bottom_left: self.bottom_left * rhs.bottom_left,
         }
     }
 }
@@ -2856,7 +2858,7 @@ impl DevicePixels {
     /// let total_bytes = pixels.to_bytes(bytes_per_pixel);
     /// assert_eq!(total_bytes, 40); // 10 pixels * 4 bytes/pixel = 40 bytes
     /// ```
-    pub fn to_bytes(&self, bytes_per_pixel: u8) -> u32 {
+    pub fn to_bytes(self, bytes_per_pixel: u8) -> u32 {
         self.0 as u32 * bytes_per_pixel as u32
     }
 }
@@ -3071,8 +3073,8 @@ pub struct Rems(pub f32);
 
 impl Rems {
     /// Convert this Rem value to pixels.
-    pub fn to_pixels(&self, rem_size: Pixels) -> Pixels {
-        *self * rem_size
+    pub fn to_pixels(self, rem_size: Pixels) -> Pixels {
+        self * rem_size
     }
 }
 
@@ -3166,9 +3168,9 @@ impl AbsoluteLength {
     /// assert_eq!(length_in_pixels.to_pixels(rem_size), Pixels(42.0));
     /// assert_eq!(length_in_rems.to_pixels(rem_size), Pixels(32.0));
     /// ```
-    pub fn to_pixels(&self, rem_size: Pixels) -> Pixels {
+    pub fn to_pixels(self, rem_size: Pixels) -> Pixels {
         match self {
-            AbsoluteLength::Pixels(pixels) => *pixels,
+            AbsoluteLength::Pixels(pixels) => pixels,
             AbsoluteLength::Rems(rems) => rems.to_pixels(rem_size),
         }
     }
@@ -3182,10 +3184,10 @@ impl AbsoluteLength {
     /// # Returns
     ///
     /// Returns the `AbsoluteLength` as `Pixels`.
-    pub fn to_rems(&self, rem_size: Pixels) -> Rems {
+    pub fn to_rems(self, rem_size: Pixels) -> Rems {
         match self {
             AbsoluteLength::Pixels(pixels) => Rems(pixels.0 / rem_size.0),
-            AbsoluteLength::Rems(rems) => *rems,
+            AbsoluteLength::Rems(rems) => rems,
         }
     }
 }
@@ -3313,12 +3315,12 @@ impl DefiniteLength {
     /// assert_eq!(length_in_rems.to_pixels(base_size, rem_size), Pixels(32.0));
     /// assert_eq!(length_as_fraction.to_pixels(base_size, rem_size), Pixels(50.0));
     /// ```
-    pub fn to_pixels(&self, base_size: AbsoluteLength, rem_size: Pixels) -> Pixels {
+    pub fn to_pixels(self, base_size: AbsoluteLength, rem_size: Pixels) -> Pixels {
         match self {
             DefiniteLength::Absolute(size) => size.to_pixels(rem_size),
             DefiniteLength::Fraction(fraction) => match base_size {
-                AbsoluteLength::Pixels(px) => px * *fraction,
-                AbsoluteLength::Rems(rems) => rems * rem_size * *fraction,
+                AbsoluteLength::Pixels(px) => px * fraction,
+                AbsoluteLength::Rems(rems) => rems * rem_size * fraction,
             },
         }
     }
@@ -3608,6 +3610,37 @@ impl From<()> for Length {
     }
 }
 
+/// A location in a grid layout.
+#[derive(Clone, PartialEq, Debug, Serialize, Deserialize, JsonSchema, Default)]
+pub struct GridLocation {
+    /// The rows this item uses within the grid.
+    pub row: Range<GridPlacement>,
+    /// The columns this item uses within the grid.
+    pub column: Range<GridPlacement>,
+}
+
+/// The placement of an item within a grid layout's column or row.
+#[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize, JsonSchema, Default)]
+pub enum GridPlacement {
+    /// The grid line index to place this item.
+    Line(i16),
+    /// The number of grid lines to span.
+    Span(u16),
+    /// Automatically determine the placement, equivalent to Span(1)
+    #[default]
+    Auto,
+}
+
+impl From<GridPlacement> for taffy::GridPlacement {
+    fn from(placement: GridPlacement) -> Self {
+        match placement {
+            GridPlacement::Line(index) => taffy::GridPlacement::from_line_index(index),
+            GridPlacement::Span(span) => taffy::GridPlacement::from_span(span),
+            GridPlacement::Auto => taffy::GridPlacement::Auto,
+        }
+    }
+}
+
 /// Provides a trait for types that can calculate half of their value.
 ///
 /// The `Half` trait is used for types that can be evenly divided, returning a new instance of the same type

crates/gpui/src/gpui.rs 🔗

@@ -157,7 +157,7 @@ pub use taffy::{AvailableSpace, LayoutId};
 #[cfg(any(test, feature = "test-support"))]
 pub use test::*;
 pub use text_system::*;
-pub use util::arc_cow::ArcCow;
+pub use util::{FutureExt, Timeout, arc_cow::ArcCow};
 pub use view::*;
 pub use window::*;
 
@@ -172,6 +172,10 @@ pub trait AppContext {
     type Result<T>;
 
     /// Create a new entity in the app context.
+    #[expect(
+        clippy::wrong_self_convention,
+        reason = "`App::new` is an ubiquitous function for creating entities"
+    )]
     fn new<T: 'static>(
         &mut self,
         build_entity: impl FnOnce(&mut Context<T>) -> T,

crates/gpui/src/inspector.rs 🔗

@@ -164,7 +164,7 @@ mod conditional {
                     if let Some(render_inspector) = cx
                         .inspector_element_registry
                         .renderers_by_type_id
-                        .remove(&type_id)
+                        .remove(type_id)
                     {
                         let mut element = (render_inspector)(
                             active_element.id.clone(),

crates/gpui/src/key_dispatch.rs 🔗

@@ -408,7 +408,7 @@ impl DispatchTree {
         keymap
             .bindings_for_action(action)
             .filter(|binding| {
-                Self::binding_matches_predicate_and_not_shadowed(&keymap, &binding, context_stack)
+                Self::binding_matches_predicate_and_not_shadowed(&keymap, binding, context_stack)
             })
             .cloned()
             .collect()
@@ -426,7 +426,7 @@ impl DispatchTree {
             .bindings_for_action(action)
             .rev()
             .find(|binding| {
-                Self::binding_matches_predicate_and_not_shadowed(&keymap, &binding, context_stack)
+                Self::binding_matches_predicate_and_not_shadowed(&keymap, binding, context_stack)
             })
             .cloned()
     }
@@ -458,7 +458,7 @@ impl DispatchTree {
             .keymap
             .borrow()
             .bindings_for_input(input, &context_stack);
-        return (bindings, partial, context_stack);
+        (bindings, partial, context_stack)
     }
 
     /// dispatch_key processes the keystroke
@@ -611,9 +611,17 @@ impl DispatchTree {
 
 #[cfg(test)]
 mod tests {
-    use std::{cell::RefCell, rc::Rc};
-
-    use crate::{Action, ActionRegistry, DispatchTree, KeyBinding, KeyContext, Keymap};
+    use crate::{
+        self as gpui, Element, ElementId, GlobalElementId, InspectorElementId, LayoutId, Style,
+    };
+    use core::panic;
+    use std::{cell::RefCell, ops::Range, rc::Rc};
+
+    use crate::{
+        Action, ActionRegistry, App, Bounds, Context, DispatchTree, FocusHandle, InputHandler,
+        IntoElement, KeyBinding, KeyContext, Keymap, Pixels, Point, Render, TestAppContext,
+        UTF16Selection, Window,
+    };
 
     #[derive(PartialEq, Eq)]
     struct TestAction;
@@ -631,10 +639,7 @@ mod tests {
         }
 
         fn partial_eq(&self, action: &dyn Action) -> bool {
-            action
-                .as_any()
-                .downcast_ref::<Self>()
-                .map_or(false, |a| self == a)
+            action.as_any().downcast_ref::<Self>() == Some(self)
         }
 
         fn boxed_clone(&self) -> std::boxed::Box<dyn Action> {
@@ -674,4 +679,165 @@ mod tests {
 
         assert!(keybinding[0].action.partial_eq(&TestAction))
     }
+
+    #[crate::test]
+    fn test_input_handler_pending(cx: &mut TestAppContext) {
+        #[derive(Clone)]
+        struct CustomElement {
+            focus_handle: FocusHandle,
+            text: Rc<RefCell<String>>,
+        }
+        impl CustomElement {
+            fn new(cx: &mut Context<Self>) -> Self {
+                Self {
+                    focus_handle: cx.focus_handle(),
+                    text: Rc::default(),
+                }
+            }
+        }
+        impl Element for CustomElement {
+            type RequestLayoutState = ();
+
+            type PrepaintState = ();
+
+            fn id(&self) -> Option<ElementId> {
+                Some("custom".into())
+            }
+            fn source_location(&self) -> Option<&'static panic::Location<'static>> {
+                None
+            }
+            fn request_layout(
+                &mut self,
+                _: Option<&GlobalElementId>,
+                _: Option<&InspectorElementId>,
+                window: &mut Window,
+                cx: &mut App,
+            ) -> (LayoutId, Self::RequestLayoutState) {
+                (window.request_layout(Style::default(), [], cx), ())
+            }
+            fn prepaint(
+                &mut self,
+                _: Option<&GlobalElementId>,
+                _: Option<&InspectorElementId>,
+                _: Bounds<Pixels>,
+                _: &mut Self::RequestLayoutState,
+                window: &mut Window,
+                cx: &mut App,
+            ) -> Self::PrepaintState {
+                window.set_focus_handle(&self.focus_handle, cx);
+            }
+            fn paint(
+                &mut self,
+                _: Option<&GlobalElementId>,
+                _: Option<&InspectorElementId>,
+                _: Bounds<Pixels>,
+                _: &mut Self::RequestLayoutState,
+                _: &mut Self::PrepaintState,
+                window: &mut Window,
+                cx: &mut App,
+            ) {
+                let mut key_context = KeyContext::default();
+                key_context.add("Terminal");
+                window.set_key_context(key_context);
+                window.handle_input(&self.focus_handle, self.clone(), cx);
+                window.on_action(std::any::TypeId::of::<TestAction>(), |_, _, _, _| {});
+            }
+        }
+        impl IntoElement for CustomElement {
+            type Element = Self;
+
+            fn into_element(self) -> Self::Element {
+                self
+            }
+        }
+
+        impl InputHandler for CustomElement {
+            fn selected_text_range(
+                &mut self,
+                _: bool,
+                _: &mut Window,
+                _: &mut App,
+            ) -> Option<UTF16Selection> {
+                None
+            }
+
+            fn marked_text_range(&mut self, _: &mut Window, _: &mut App) -> Option<Range<usize>> {
+                None
+            }
+
+            fn text_for_range(
+                &mut self,
+                _: Range<usize>,
+                _: &mut Option<Range<usize>>,
+                _: &mut Window,
+                _: &mut App,
+            ) -> Option<String> {
+                None
+            }
+
+            fn replace_text_in_range(
+                &mut self,
+                replacement_range: Option<Range<usize>>,
+                text: &str,
+                _: &mut Window,
+                _: &mut App,
+            ) {
+                if replacement_range.is_some() {
+                    unimplemented!()
+                }
+                self.text.borrow_mut().push_str(text)
+            }
+
+            fn replace_and_mark_text_in_range(
+                &mut self,
+                replacement_range: Option<Range<usize>>,
+                new_text: &str,
+                _: Option<Range<usize>>,
+                _: &mut Window,
+                _: &mut App,
+            ) {
+                if replacement_range.is_some() {
+                    unimplemented!()
+                }
+                self.text.borrow_mut().push_str(new_text)
+            }
+
+            fn unmark_text(&mut self, _: &mut Window, _: &mut App) {}
+
+            fn bounds_for_range(
+                &mut self,
+                _: Range<usize>,
+                _: &mut Window,
+                _: &mut App,
+            ) -> Option<Bounds<Pixels>> {
+                None
+            }
+
+            fn character_index_for_point(
+                &mut self,
+                _: Point<Pixels>,
+                _: &mut Window,
+                _: &mut App,
+            ) -> Option<usize> {
+                None
+            }
+        }
+        impl Render for CustomElement {
+            fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
+                self.clone()
+            }
+        }
+
+        cx.update(|cx| {
+            cx.bind_keys([KeyBinding::new("ctrl-b", TestAction, Some("Terminal"))]);
+            cx.bind_keys([KeyBinding::new("ctrl-b h", TestAction, Some("Terminal"))]);
+        });
+        let (test, cx) = cx.add_window_view(|_, cx| CustomElement::new(cx));
+        cx.update(|window, cx| {
+            window.focus(&test.read(cx).focus_handle);
+            window.activate_window();
+        });
+        cx.simulate_keystrokes("ctrl-b [");
+        test.update(cx, |test, _| assert_eq!(test.text.borrow().as_str(), "["))
+    }
 }

crates/gpui/src/keymap.rs 🔗

@@ -148,7 +148,7 @@ impl Keymap {
         let mut pending_bindings = SmallVec::<[(BindingIndex, &KeyBinding); 1]>::new();
 
         for (ix, binding) in self.bindings().enumerate().rev() {
-            let Some(depth) = self.binding_enabled(binding, &context_stack) else {
+            let Some(depth) = self.binding_enabled(binding, context_stack) else {
                 continue;
             };
             let Some(pending) = binding.match_keystrokes(input) else {
@@ -264,7 +264,7 @@ mod tests {
         ];
 
         let mut keymap = Keymap::default();
-        keymap.add_bindings(bindings.clone());
+        keymap.add_bindings(bindings);
 
         let (result, pending) = keymap.bindings_for_input(
             &[Keystroke::parse("ctrl-a").unwrap()],
@@ -290,7 +290,7 @@ mod tests {
         ];
 
         let mut keymap = Keymap::default();
-        keymap.add_bindings(bindings.clone());
+        keymap.add_bindings(bindings);
 
         // binding is only enabled in a specific context
         assert!(
@@ -344,7 +344,7 @@ mod tests {
         ];
 
         let mut keymap = Keymap::default();
-        keymap.add_bindings(bindings.clone());
+        keymap.add_bindings(bindings);
 
         let space = || Keystroke::parse("space").unwrap();
         let w = || Keystroke::parse("w").unwrap();
@@ -364,29 +364,29 @@ mod tests {
         // Ensure `space` results in pending input on the workspace, but not editor
         let space_workspace = keymap.bindings_for_input(&[space()], &workspace_context());
         assert!(space_workspace.0.is_empty());
-        assert_eq!(space_workspace.1, true);
+        assert!(space_workspace.1);
 
         let space_editor = keymap.bindings_for_input(&[space()], &editor_workspace_context());
         assert!(space_editor.0.is_empty());
-        assert_eq!(space_editor.1, false);
+        assert!(!space_editor.1);
 
         // Ensure `space w` results in pending input on the workspace, but not editor
         let space_w_workspace = keymap.bindings_for_input(&space_w, &workspace_context());
         assert!(space_w_workspace.0.is_empty());
-        assert_eq!(space_w_workspace.1, true);
+        assert!(space_w_workspace.1);
 
         let space_w_editor = keymap.bindings_for_input(&space_w, &editor_workspace_context());
         assert!(space_w_editor.0.is_empty());
-        assert_eq!(space_w_editor.1, false);
+        assert!(!space_w_editor.1);
 
         // Ensure `space w w` results in the binding in the workspace, but not in the editor
         let space_w_w_workspace = keymap.bindings_for_input(&space_w_w, &workspace_context());
         assert!(!space_w_w_workspace.0.is_empty());
-        assert_eq!(space_w_w_workspace.1, false);
+        assert!(!space_w_w_workspace.1);
 
         let space_w_w_editor = keymap.bindings_for_input(&space_w_w, &editor_workspace_context());
         assert!(space_w_w_editor.0.is_empty());
-        assert_eq!(space_w_w_editor.1, false);
+        assert!(!space_w_w_editor.1);
 
         // Now test what happens if we have another binding defined AFTER the NoAction
         // that should result in pending
@@ -396,11 +396,11 @@ mod tests {
             KeyBinding::new("space w x", ActionAlpha {}, Some("editor")),
         ];
         let mut keymap = Keymap::default();
-        keymap.add_bindings(bindings.clone());
+        keymap.add_bindings(bindings);
 
         let space_editor = keymap.bindings_for_input(&[space()], &editor_workspace_context());
         assert!(space_editor.0.is_empty());
-        assert_eq!(space_editor.1, true);
+        assert!(space_editor.1);
 
         // Now test what happens if we have another binding defined BEFORE the NoAction
         // that should result in pending
@@ -410,11 +410,11 @@ mod tests {
             KeyBinding::new("space w w", NoAction {}, Some("editor")),
         ];
         let mut keymap = Keymap::default();
-        keymap.add_bindings(bindings.clone());
+        keymap.add_bindings(bindings);
 
         let space_editor = keymap.bindings_for_input(&[space()], &editor_workspace_context());
         assert!(space_editor.0.is_empty());
-        assert_eq!(space_editor.1, true);
+        assert!(space_editor.1);
 
         // Now test what happens if we have another binding defined at a higher context
         // that should result in pending
@@ -424,11 +424,11 @@ mod tests {
             KeyBinding::new("space w w", NoAction {}, Some("editor")),
         ];
         let mut keymap = Keymap::default();
-        keymap.add_bindings(bindings.clone());
+        keymap.add_bindings(bindings);
 
         let space_editor = keymap.bindings_for_input(&[space()], &editor_workspace_context());
         assert!(space_editor.0.is_empty());
-        assert_eq!(space_editor.1, true);
+        assert!(space_editor.1);
     }
 
     #[test]
@@ -439,7 +439,7 @@ mod tests {
         ];
 
         let mut keymap = Keymap::default();
-        keymap.add_bindings(bindings.clone());
+        keymap.add_bindings(bindings);
 
         // Ensure `space` results in pending input on the workspace, but not editor
         let (result, pending) = keymap.bindings_for_input(
@@ -447,7 +447,7 @@ mod tests {
             &[KeyContext::parse("editor").unwrap()],
         );
         assert!(result.is_empty());
-        assert_eq!(pending, true);
+        assert!(pending);
 
         let bindings = [
             KeyBinding::new("ctrl-w left", ActionAlpha {}, Some("editor")),
@@ -455,7 +455,7 @@ mod tests {
         ];
 
         let mut keymap = Keymap::default();
-        keymap.add_bindings(bindings.clone());
+        keymap.add_bindings(bindings);
 
         // Ensure `space` results in pending input on the workspace, but not editor
         let (result, pending) = keymap.bindings_for_input(
@@ -463,7 +463,7 @@ mod tests {
             &[KeyContext::parse("editor").unwrap()],
         );
         assert_eq!(result.len(), 1);
-        assert_eq!(pending, false);
+        assert!(!pending);
     }
 
     #[test]
@@ -474,7 +474,7 @@ mod tests {
         ];
 
         let mut keymap = Keymap::default();
-        keymap.add_bindings(bindings.clone());
+        keymap.add_bindings(bindings);
 
         // Ensure `space` results in pending input on the workspace, but not editor
         let (result, pending) = keymap.bindings_for_input(
@@ -482,7 +482,7 @@ mod tests {
             &[KeyContext::parse("editor").unwrap()],
         );
         assert!(result.is_empty());
-        assert_eq!(pending, false);
+        assert!(!pending);
     }
 
     #[test]
@@ -494,7 +494,7 @@ mod tests {
         ];
 
         let mut keymap = Keymap::default();
-        keymap.add_bindings(bindings.clone());
+        keymap.add_bindings(bindings);
 
         // Ensure `space` results in pending input on the workspace, but not editor
         let (result, pending) = keymap.bindings_for_input(
@@ -505,7 +505,7 @@ mod tests {
             ],
         );
         assert_eq!(result.len(), 1);
-        assert_eq!(pending, false);
+        assert!(!pending);
     }
 
     #[test]
@@ -516,7 +516,7 @@ mod tests {
         ];
 
         let mut keymap = Keymap::default();
-        keymap.add_bindings(bindings.clone());
+        keymap.add_bindings(bindings);
 
         // Ensure `space` results in pending input on the workspace, but not editor
         let (result, pending) = keymap.bindings_for_input(
@@ -527,7 +527,7 @@ mod tests {
             ],
         );
         assert_eq!(result.len(), 0);
-        assert_eq!(pending, false);
+        assert!(!pending);
     }
 
     #[test]
@@ -537,7 +537,7 @@ mod tests {
             KeyBinding::new("ctrl-x 0", ActionAlpha, Some("Workspace")),
         ];
         let mut keymap = Keymap::default();
-        keymap.add_bindings(bindings.clone());
+        keymap.add_bindings(bindings);
 
         let matched = keymap.bindings_for_input(
             &[Keystroke::parse("ctrl-x")].map(Result::unwrap),
@@ -560,7 +560,7 @@ mod tests {
             KeyBinding::new("ctrl-x 0", NoAction, Some("Workspace")),
         ];
         let mut keymap = Keymap::default();
-        keymap.add_bindings(bindings.clone());
+        keymap.add_bindings(bindings);
 
         let matched = keymap.bindings_for_input(
             &[Keystroke::parse("ctrl-x")].map(Result::unwrap),
@@ -579,7 +579,7 @@ mod tests {
             KeyBinding::new("ctrl-x 0", NoAction, Some("vim_mode == normal")),
         ];
         let mut keymap = Keymap::default();
-        keymap.add_bindings(bindings.clone());
+        keymap.add_bindings(bindings);
 
         let matched = keymap.bindings_for_input(
             &[Keystroke::parse("ctrl-x")].map(Result::unwrap),
@@ -602,7 +602,7 @@ mod tests {
             KeyBinding::new("ctrl-x", ActionBeta, Some("vim_mode == normal")),
         ];
         let mut keymap = Keymap::default();
-        keymap.add_bindings(bindings.clone());
+        keymap.add_bindings(bindings);
 
         let matched = keymap.bindings_for_input(
             &[Keystroke::parse("ctrl-x")].map(Result::unwrap),
@@ -629,7 +629,7 @@ mod tests {
         ];
 
         let mut keymap = Keymap::default();
-        keymap.add_bindings(bindings.clone());
+        keymap.add_bindings(bindings);
 
         assert_bindings(&keymap, &ActionAlpha {}, &["ctrl-a"]);
         assert_bindings(&keymap, &ActionBeta {}, &[]);

crates/gpui/src/keymap/binding.rs 🔗

@@ -30,11 +30,8 @@ impl Clone for KeyBinding {
 impl KeyBinding {
     /// Construct a new keybinding from the given data. Panics on parse error.
     pub fn new<A: Action>(keystrokes: &str, action: A, context: Option<&str>) -> Self {
-        let context_predicate = if let Some(context) = context {
-            Some(KeyBindingContextPredicate::parse(context).unwrap().into())
-        } else {
-            None
-        };
+        let context_predicate =
+            context.map(|context| KeyBindingContextPredicate::parse(context).unwrap().into());
         Self::load(keystrokes, Box::new(action), context_predicate, None, None).unwrap()
     }
 
@@ -53,10 +50,10 @@ impl KeyBinding {
 
         if let Some(equivalents) = key_equivalents {
             for keystroke in keystrokes.iter_mut() {
-                if keystroke.key.chars().count() == 1 {
-                    if let Some(key) = equivalents.get(&keystroke.key.chars().next().unwrap()) {
-                        keystroke.key = key.to_string();
-                    }
+                if keystroke.key.chars().count() == 1
+                    && let Some(key) = equivalents.get(&keystroke.key.chars().next().unwrap())
+                {
+                    keystroke.key = key.to_string();
                 }
             }
         }

crates/gpui/src/keymap/context.rs 🔗

@@ -287,7 +287,7 @@ impl KeyBindingContextPredicate {
                         return false;
                     }
                 }
-                return true;
+                true
             }
             // Workspace > Pane > Editor
             //
@@ -305,7 +305,7 @@ impl KeyBindingContextPredicate {
                         return true;
                     }
                 }
-                return false;
+                false
             }
             Self::And(left, right) => {
                 left.eval_inner(contexts, all_contexts) && right.eval_inner(contexts, all_contexts)
@@ -461,6 +461,8 @@ fn skip_whitespace(source: &str) -> &str {
 
 #[cfg(test)]
 mod tests {
+    use core::slice;
+
     use super::*;
     use crate as gpui;
     use KeyBindingContextPredicate::*;
@@ -666,20 +668,16 @@ mod tests {
         let contexts = vec![other_context.clone(), child_context.clone()];
         assert!(!predicate.eval(&contexts));
 
-        let contexts = vec![
-            parent_context.clone(),
-            other_context.clone(),
-            child_context.clone(),
-        ];
+        let contexts = vec![parent_context.clone(), other_context, child_context.clone()];
         assert!(predicate.eval(&contexts));
 
         assert!(!predicate.eval(&[]));
-        assert!(!predicate.eval(&[child_context.clone()]));
+        assert!(!predicate.eval(slice::from_ref(&child_context)));
         assert!(!predicate.eval(&[parent_context]));
 
         let zany_predicate = KeyBindingContextPredicate::parse("child > child").unwrap();
-        assert!(!zany_predicate.eval(&[child_context.clone()]));
-        assert!(zany_predicate.eval(&[child_context.clone(), child_context.clone()]));
+        assert!(!zany_predicate.eval(slice::from_ref(&child_context)));
+        assert!(zany_predicate.eval(&[child_context.clone(), child_context]));
     }
 
     #[test]
@@ -690,13 +688,13 @@ mod tests {
         let parent_context = KeyContext::try_from("parent").unwrap();
         let child_context = KeyContext::try_from("child").unwrap();
 
-        assert!(not_predicate.eval(&[workspace_context.clone()]));
-        assert!(!not_predicate.eval(&[editor_context.clone()]));
+        assert!(not_predicate.eval(slice::from_ref(&workspace_context)));
+        assert!(!not_predicate.eval(slice::from_ref(&editor_context)));
         assert!(!not_predicate.eval(&[editor_context.clone(), workspace_context.clone()]));
         assert!(!not_predicate.eval(&[workspace_context.clone(), editor_context.clone()]));
 
         let complex_not = KeyBindingContextPredicate::parse("!editor && workspace").unwrap();
-        assert!(complex_not.eval(&[workspace_context.clone()]));
+        assert!(complex_not.eval(slice::from_ref(&workspace_context)));
         assert!(!complex_not.eval(&[editor_context.clone(), workspace_context.clone()]));
 
         let not_mode_predicate = KeyBindingContextPredicate::parse("!(mode == full)").unwrap();
@@ -709,18 +707,18 @@ mod tests {
         assert!(not_mode_predicate.eval(&[other_mode_context]));
 
         let not_descendant = KeyBindingContextPredicate::parse("!(parent > child)").unwrap();
-        assert!(not_descendant.eval(&[parent_context.clone()]));
-        assert!(not_descendant.eval(&[child_context.clone()]));
+        assert!(not_descendant.eval(slice::from_ref(&parent_context)));
+        assert!(not_descendant.eval(slice::from_ref(&child_context)));
         assert!(!not_descendant.eval(&[parent_context.clone(), child_context.clone()]));
 
         let not_descendant = KeyBindingContextPredicate::parse("parent > !child").unwrap();
-        assert!(!not_descendant.eval(&[parent_context.clone()]));
-        assert!(!not_descendant.eval(&[child_context.clone()]));
-        assert!(!not_descendant.eval(&[parent_context.clone(), child_context.clone()]));
+        assert!(!not_descendant.eval(slice::from_ref(&parent_context)));
+        assert!(!not_descendant.eval(slice::from_ref(&child_context)));
+        assert!(!not_descendant.eval(&[parent_context, child_context]));
 
         let double_not = KeyBindingContextPredicate::parse("!!editor").unwrap();
-        assert!(double_not.eval(&[editor_context.clone()]));
-        assert!(!double_not.eval(&[workspace_context.clone()]));
+        assert!(double_not.eval(slice::from_ref(&editor_context)));
+        assert!(!double_not.eval(slice::from_ref(&workspace_context)));
 
         // Test complex descendant cases
         let workspace_context = KeyContext::try_from("Workspace").unwrap();
@@ -754,9 +752,9 @@ mod tests {
 
         // !Workspace - shouldn't match when Workspace is in the context
         let not_workspace = KeyBindingContextPredicate::parse("!Workspace").unwrap();
-        assert!(!not_workspace.eval(&[workspace_context.clone()]));
-        assert!(not_workspace.eval(&[pane_context.clone()]));
-        assert!(not_workspace.eval(&[editor_context.clone()]));
+        assert!(!not_workspace.eval(slice::from_ref(&workspace_context)));
+        assert!(not_workspace.eval(slice::from_ref(&pane_context)));
+        assert!(not_workspace.eval(slice::from_ref(&editor_context)));
         assert!(!not_workspace.eval(&workspace_pane_editor));
     }
 }

crates/gpui/src/path_builder.rs 🔗

@@ -278,7 +278,7 @@ impl PathBuilder {
         options: &StrokeOptions,
     ) -> Result<Path<Pixels>, Error> {
         let path = if let Some(dash_array) = dash_array {
-            let measurements = lyon::algorithms::measure::PathMeasurements::from_path(&path, 0.01);
+            let measurements = lyon::algorithms::measure::PathMeasurements::from_path(path, 0.01);
             let mut sampler = measurements
                 .create_sampler(path, lyon::algorithms::measure::SampleType::Normalized);
             let mut builder = lyon::path::Path::builder();

crates/gpui/src/platform.rs 🔗

@@ -220,7 +220,11 @@ pub(crate) trait Platform: 'static {
         &self,
         options: PathPromptOptions,
     ) -> oneshot::Receiver<Result<Option<Vec<PathBuf>>>>;
-    fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Result<Option<PathBuf>>>;
+    fn prompt_for_new_path(
+        &self,
+        directory: &Path,
+        suggested_name: Option<&str>,
+    ) -> oneshot::Receiver<Result<Option<PathBuf>>>;
     fn can_select_mixed_files_and_dirs(&self) -> bool;
     fn reveal_path(&self, path: &Path);
     fn open_with_system(&self, path: &Path);
@@ -588,7 +592,7 @@ impl PlatformTextSystem for NoopTextSystem {
     }
 
     fn font_id(&self, _descriptor: &Font) -> Result<FontId> {
-        return Ok(FontId(1));
+        Ok(FontId(1))
     }
 
     fn font_metrics(&self, _font_id: FontId) -> FontMetrics {
@@ -669,7 +673,7 @@ impl PlatformTextSystem for NoopTextSystem {
             }
         }
         let mut runs = Vec::default();
-        if glyphs.len() > 0 {
+        if !glyphs.is_empty() {
             runs.push(ShapedRun {
                 font_id: FontId(0),
                 glyphs,
@@ -1274,7 +1278,7 @@ pub enum WindowBackgroundAppearance {
 }
 
 /// The options that can be configured for a file dialog prompt
-#[derive(Copy, Clone, Debug)]
+#[derive(Clone, Debug)]
 pub struct PathPromptOptions {
     /// Should the prompt allow files to be selected?
     pub files: bool,
@@ -1282,6 +1286,8 @@ pub struct PathPromptOptions {
     pub directories: bool,
     /// Should the prompt allow multiple files to be selected?
     pub multiple: bool,
+    /// The prompt to show to a user when selecting a path
+    pub prompt: Option<SharedString>,
 }
 
 /// What kind of prompt styling to show
@@ -1502,7 +1508,7 @@ impl ClipboardItem {
 
         for entry in self.entries.iter() {
             if let ClipboardEntry::String(ClipboardString { text, metadata: _ }) = entry {
-                answer.push_str(&text);
+                answer.push_str(text);
                 any_entries = true;
             }
         }

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

@@ -20,6 +20,34 @@ impl Menu {
     }
 }
 
+/// OS menus are menus that are recognized by the operating system
+/// This allows the operating system to provide specialized items for
+/// these menus
+pub struct OsMenu {
+    /// The name of the menu
+    pub name: SharedString,
+
+    /// The type of menu
+    pub menu_type: SystemMenuType,
+}
+
+impl OsMenu {
+    /// Create an OwnedOsMenu from this OsMenu
+    pub fn owned(self) -> OwnedOsMenu {
+        OwnedOsMenu {
+            name: self.name.to_string().into(),
+            menu_type: self.menu_type,
+        }
+    }
+}
+
+/// The type of system menu
+#[derive(Copy, Clone, Eq, PartialEq)]
+pub enum SystemMenuType {
+    /// The 'Services' menu in the Application menu on macOS
+    Services,
+}
+
 /// The different kinds of items that can be in a menu
 pub enum MenuItem {
     /// A separator between items
@@ -28,6 +56,9 @@ pub enum MenuItem {
     /// A submenu
     Submenu(Menu),
 
+    /// A menu, managed by the system (for example, the Services menu on macOS)
+    SystemMenu(OsMenu),
+
     /// An action that can be performed
     Action {
         /// The name of this menu item
@@ -53,6 +84,14 @@ impl MenuItem {
         Self::Submenu(menu)
     }
 
+    /// Creates a new submenu that is populated by the OS
+    pub fn os_submenu(name: impl Into<SharedString>, menu_type: SystemMenuType) -> Self {
+        Self::SystemMenu(OsMenu {
+            name: name.into(),
+            menu_type,
+        })
+    }
+
     /// Creates a new menu item that invokes an action
     pub fn action(name: impl Into<SharedString>, action: impl Action) -> Self {
         Self::Action {
@@ -89,10 +128,23 @@ impl MenuItem {
                 action,
                 os_action,
             },
+            MenuItem::SystemMenu(os_menu) => OwnedMenuItem::SystemMenu(os_menu.owned()),
         }
     }
 }
 
+/// OS menus are menus that are recognized by the operating system
+/// This allows the operating system to provide specialized items for
+/// these menus
+#[derive(Clone)]
+pub struct OwnedOsMenu {
+    /// The name of the menu
+    pub name: SharedString,
+
+    /// The type of menu
+    pub menu_type: SystemMenuType,
+}
+
 /// A menu of the application, either a main menu or a submenu
 #[derive(Clone)]
 pub struct OwnedMenu {
@@ -111,6 +163,9 @@ pub enum OwnedMenuItem {
     /// A submenu
     Submenu(OwnedMenu),
 
+    /// A menu, managed by the system (for example, the Services menu on macOS)
+    SystemMenu(OwnedOsMenu),
+
     /// An action that can be performed
     Action {
         /// The name of this menu item
@@ -139,6 +194,7 @@ impl Clone for OwnedMenuItem {
                 action: action.boxed_clone(),
                 os_action: *os_action,
             },
+            OwnedMenuItem::SystemMenu(os_menu) => OwnedMenuItem::SystemMenu(os_menu.clone()),
         }
     }
 }

crates/gpui/src/platform/blade/blade_context.rs 🔗

@@ -49,7 +49,7 @@ fn parse_pci_id(id: &str) -> anyhow::Result<u32> {
         "Expected a 4 digit PCI ID in hexadecimal format"
     );
 
-    return u32::from_str_radix(id, 16).context("parsing PCI ID as hex");
+    u32::from_str_radix(id, 16).context("parsing PCI ID as hex")
 }
 
 #[cfg(test)]

crates/gpui/src/platform/blade/blade_renderer.rs 🔗

@@ -434,24 +434,24 @@ impl BladeRenderer {
     }
 
     fn wait_for_gpu(&mut self) {
-        if let Some(last_sp) = self.last_sync_point.take() {
-            if !self.gpu.wait_for(&last_sp, MAX_FRAME_TIME_MS) {
-                log::error!("GPU hung");
-                #[cfg(target_os = "linux")]
-                if self.gpu.device_information().driver_name == "radv" {
-                    log::error!(
-                        "there's a known bug with amdgpu/radv, try setting ZED_PATH_SAMPLE_COUNT=0 as a workaround"
-                    );
-                    log::error!(
-                        "if that helps you're running into https://github.com/zed-industries/zed/issues/26143"
-                    );
-                }
+        if let Some(last_sp) = self.last_sync_point.take()
+            && !self.gpu.wait_for(&last_sp, MAX_FRAME_TIME_MS)
+        {
+            log::error!("GPU hung");
+            #[cfg(target_os = "linux")]
+            if self.gpu.device_information().driver_name == "radv" {
                 log::error!(
-                    "your device information is: {:?}",
-                    self.gpu.device_information()
+                    "there's a known bug with amdgpu/radv, try setting ZED_PATH_SAMPLE_COUNT=0 as a workaround"
+                );
+                log::error!(
+                    "if that helps you're running into https://github.com/zed-industries/zed/issues/26143"
                 );
-                while !self.gpu.wait_for(&last_sp, MAX_FRAME_TIME_MS) {}
             }
+            log::error!(
+                "your device information is: {:?}",
+                self.gpu.device_information()
+            );
+            while !self.gpu.wait_for(&last_sp, MAX_FRAME_TIME_MS) {}
         }
     }
 

crates/gpui/src/platform/blade/shaders.wgsl 🔗

@@ -1057,6 +1057,9 @@ fn vs_underline(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index)
 
 @fragment
 fn fs_underline(input: UnderlineVarying) -> @location(0) vec4<f32> {
+    const WAVE_FREQUENCY: f32 = 2.0;
+    const WAVE_HEIGHT_RATIO: f32 = 0.8;
+
     // Alpha clip first, since we don't have `clip_distance`.
     if (any(input.clip_distances < vec4<f32>(0.0))) {
         return vec4<f32>(0.0);
@@ -1069,9 +1072,11 @@ fn fs_underline(input: UnderlineVarying) -> @location(0) vec4<f32> {
     }
 
     let half_thickness = underline.thickness * 0.5;
+
     let st = (input.position.xy - underline.bounds.origin) / underline.bounds.size.y - vec2<f32>(0.0, 0.5);
-    let frequency = M_PI_F * 3.0 * underline.thickness / 3.0;
-    let amplitude = 1.0 / (4.0 * underline.thickness);
+    let frequency = M_PI_F * WAVE_FREQUENCY * underline.thickness / underline.bounds.size.y;
+    let amplitude = (underline.thickness * WAVE_HEIGHT_RATIO) / underline.bounds.size.y;
+
     let sine = sin(st.x * frequency) * amplitude;
     let dSine = cos(st.x * frequency) * amplitude * frequency;
     let distance = (st.y - sine) / sqrt(1.0 + dSine * dSine);

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

@@ -108,13 +108,13 @@ impl LinuxCommon {
 
         let callbacks = PlatformHandlers::default();
 
-        let dispatcher = Arc::new(LinuxDispatcher::new(main_sender.clone()));
+        let dispatcher = Arc::new(LinuxDispatcher::new(main_sender));
 
         let background_executor = BackgroundExecutor::new(dispatcher.clone());
 
         let common = LinuxCommon {
             background_executor,
-            foreground_executor: ForegroundExecutor::new(dispatcher.clone()),
+            foreground_executor: ForegroundExecutor::new(dispatcher),
             text_system,
             appearance: WindowAppearance::Light,
             auto_hide_scrollbars: false,
@@ -294,6 +294,7 @@ impl<P: LinuxClient + 'static> Platform for P {
                 let request = match ashpd::desktop::file_chooser::OpenFileRequest::default()
                     .modal(true)
                     .title(title)
+                    .accept_label(options.prompt.as_ref().map(crate::SharedString::as_str))
                     .multiple(options.multiple)
                     .directory(options.directories)
                     .send()
@@ -327,26 +328,35 @@ impl<P: LinuxClient + 'static> Platform for P {
         done_rx
     }
 
-    fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Result<Option<PathBuf>>> {
+    fn prompt_for_new_path(
+        &self,
+        directory: &Path,
+        suggested_name: Option<&str>,
+    ) -> oneshot::Receiver<Result<Option<PathBuf>>> {
         let (done_tx, done_rx) = oneshot::channel();
 
         #[cfg(not(any(feature = "wayland", feature = "x11")))]
-        let _ = (done_tx.send(Ok(None)), directory);
+        let _ = (done_tx.send(Ok(None)), directory, suggested_name);
 
         #[cfg(any(feature = "wayland", feature = "x11"))]
         self.foreground_executor()
             .spawn({
                 let directory = directory.to_owned();
+                let suggested_name = suggested_name.map(|s| s.to_owned());
 
                 async move {
-                    let request = match ashpd::desktop::file_chooser::SaveFileRequest::default()
-                        .modal(true)
-                        .title("Save File")
-                        .current_folder(directory)
-                        .expect("pathbuf should not be nul terminated")
-                        .send()
-                        .await
-                    {
+                    let mut request_builder =
+                        ashpd::desktop::file_chooser::SaveFileRequest::default()
+                            .modal(true)
+                            .title("Save File")
+                            .current_folder(directory)
+                            .expect("pathbuf should not be nul terminated");
+
+                    if let Some(suggested_name) = suggested_name {
+                        request_builder = request_builder.current_name(suggested_name.as_str());
+                    }
+
+                    let request = match request_builder.send().await {
                         Ok(request) => request,
                         Err(err) => {
                             let result = match err {
@@ -431,7 +441,7 @@ impl<P: LinuxClient + 'static> Platform for P {
     fn app_path(&self) -> Result<PathBuf> {
         // get the path of the executable of the current process
         let app_path = env::current_exe()?;
-        return Ok(app_path);
+        Ok(app_path)
     }
 
     fn set_menus(&self, menus: Vec<Menu>, _keymap: &Keymap) {
@@ -632,7 +642,7 @@ pub(super) fn get_xkb_compose_state(cx: &xkb::Context) -> Option<xkb::compose::S
     let mut state: Option<xkb::compose::State> = None;
     for locale in locales {
         if let Ok(table) =
-            xkb::compose::Table::new_from_locale(&cx, &locale, xkb::compose::COMPILE_NO_FLAGS)
+            xkb::compose::Table::new_from_locale(cx, &locale, xkb::compose::COMPILE_NO_FLAGS)
         {
             state = Some(xkb::compose::State::new(
                 &table,
@@ -657,7 +667,7 @@ pub(super) const DEFAULT_CURSOR_ICON_NAME: &str = "left_ptr";
 
 impl CursorStyle {
     #[cfg(any(feature = "wayland", feature = "x11"))]
-    pub(super) fn to_icon_names(&self) -> &'static [&'static str] {
+    pub(super) fn to_icon_names(self) -> &'static [&'static str] {
         // Based on cursor names from chromium:
         // https://github.com/chromium/chromium/blob/d3069cf9c973dc3627fa75f64085c6a86c8f41bf/ui/base/cursor/cursor_factory.cc#L113
         match self {
@@ -980,21 +990,18 @@ mod tests {
     #[test]
     fn test_is_within_click_distance() {
         let zero = Point::new(px(0.0), px(0.0));
-        assert_eq!(
-            is_within_click_distance(zero, Point::new(px(5.0), px(5.0))),
-            true
-        );
-        assert_eq!(
-            is_within_click_distance(zero, Point::new(px(-4.9), px(5.0))),
-            true
-        );
-        assert_eq!(
-            is_within_click_distance(Point::new(px(3.0), px(2.0)), Point::new(px(-2.0), px(-2.0))),
-            true
-        );
-        assert_eq!(
-            is_within_click_distance(zero, Point::new(px(5.0), px(5.1))),
-            false
-        );
+        assert!(is_within_click_distance(zero, Point::new(px(5.0), px(5.0))));
+        assert!(is_within_click_distance(
+            zero,
+            Point::new(px(-4.9), px(5.0))
+        ));
+        assert!(is_within_click_distance(
+            Point::new(px(3.0), px(2.0)),
+            Point::new(px(-2.0), px(-2.0))
+        ));
+        assert!(!is_within_click_distance(
+            zero,
+            Point::new(px(5.0), px(5.1))
+        ),);
     }
 }

crates/gpui/src/platform/linux/text_system.rs 🔗

@@ -213,11 +213,7 @@ impl CosmicTextSystemState {
         features: &FontFeatures,
     ) -> Result<SmallVec<[FontId; 4]>> {
         // TODO: Determine the proper system UI font.
-        let name = if name == ".SystemUIFont" {
-            "Zed Plex Sans"
-        } else {
-            name
-        };
+        let name = crate::text_system::font_name_with_fallbacks(name, "IBM Plex Sans");
 
         let families = self
             .font_system

crates/gpui/src/platform/linux/wayland.rs 🔗

@@ -12,7 +12,7 @@ use wayland_protocols::wp::cursor_shape::v1::client::wp_cursor_shape_device_v1::
 use crate::CursorStyle;
 
 impl CursorStyle {
-    pub(super) fn to_shape(&self) -> Shape {
+    pub(super) fn to_shape(self) -> Shape {
         match self {
             CursorStyle::Arrow => Shape::Default,
             CursorStyle::IBeam => Shape::Text,

crates/gpui/src/platform/linux/wayland/client.rs 🔗

@@ -359,13 +359,13 @@ impl WaylandClientStatePtr {
             }
             changed
         };
-        if changed {
-            if let Some(mut callback) = state.common.callbacks.keyboard_layout_change.take() {
-                drop(state);
-                callback();
-                state = client.borrow_mut();
-                state.common.callbacks.keyboard_layout_change = Some(callback);
-            }
+
+        if changed && let Some(mut callback) = state.common.callbacks.keyboard_layout_change.take()
+        {
+            drop(state);
+            callback();
+            state = client.borrow_mut();
+            state.common.callbacks.keyboard_layout_change = Some(callback);
         }
     }
 
@@ -373,15 +373,15 @@ impl WaylandClientStatePtr {
         let mut client = self.get_client();
         let mut state = client.borrow_mut();
         let closed_window = state.windows.remove(surface_id).unwrap();
-        if let Some(window) = state.mouse_focused_window.take() {
-            if !window.ptr_eq(&closed_window) {
-                state.mouse_focused_window = Some(window);
-            }
+        if let Some(window) = state.mouse_focused_window.take()
+            && !window.ptr_eq(&closed_window)
+        {
+            state.mouse_focused_window = Some(window);
         }
-        if let Some(window) = state.keyboard_focused_window.take() {
-            if !window.ptr_eq(&closed_window) {
-                state.keyboard_focused_window = Some(window);
-            }
+        if let Some(window) = state.keyboard_focused_window.take()
+            && !window.ptr_eq(&closed_window)
+        {
+            state.keyboard_focused_window = Some(window);
         }
         if state.windows.is_empty() {
             state.common.signal.stop();
@@ -528,7 +528,7 @@ impl WaylandClient {
 
                             client.common.appearance = appearance;
 
-                            for (_, window) in &mut client.windows {
+                            for window in client.windows.values_mut() {
                                 window.set_appearance(appearance);
                             }
                         }
@@ -710,9 +710,7 @@ impl LinuxClient for WaylandClient {
     fn set_cursor_style(&self, style: CursorStyle) {
         let mut state = self.0.borrow_mut();
 
-        let need_update = state
-            .cursor_style
-            .map_or(true, |current_style| current_style != style);
+        let need_update = state.cursor_style != Some(style);
 
         if need_update {
             let serial = state.serial_tracker.get(SerialKind::MouseEnter);
@@ -951,11 +949,8 @@ impl Dispatch<WlCallback, ObjectId> for WaylandClientStatePtr {
         };
         drop(state);
 
-        match event {
-            wl_callback::Event::Done { .. } => {
-                window.frame();
-            }
-            _ => {}
+        if let wl_callback::Event::Done { .. } = event {
+            window.frame();
         }
     }
 }
@@ -1145,7 +1140,7 @@ impl Dispatch<wl_seat::WlSeat, ()> for WaylandClientStatePtr {
                     .globals
                     .text_input_manager
                     .as_ref()
-                    .map(|text_input_manager| text_input_manager.get_text_input(&seat, qh, ()));
+                    .map(|text_input_manager| text_input_manager.get_text_input(seat, qh, ()));
 
                 if let Some(wl_keyboard) = &state.wl_keyboard {
                     wl_keyboard.release();
@@ -1285,7 +1280,6 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr {
                 let Some(focused_window) = focused_window else {
                     return;
                 };
-                let focused_window = focused_window.clone();
 
                 let keymap_state = state.keymap_state.as_ref().unwrap();
                 let keycode = Keycode::from(key + MIN_KEYCODE);
@@ -1294,7 +1288,7 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr {
                 match key_state {
                     wl_keyboard::KeyState::Pressed if !keysym.is_modifier_key() => {
                         let mut keystroke =
-                            Keystroke::from_xkb(&keymap_state, state.modifiers, keycode);
+                            Keystroke::from_xkb(keymap_state, state.modifiers, keycode);
                         if let Some(mut compose) = state.compose_state.take() {
                             compose.feed(keysym);
                             match compose.status() {
@@ -1538,12 +1532,9 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
                             cursor_shape_device.set_shape(serial, style.to_shape());
                         } else {
                             let scale = window.primary_output_scale();
-                            state.cursor.set_icon(
-                                &wl_pointer,
-                                serial,
-                                style.to_icon_names(),
-                                scale,
-                            );
+                            state
+                                .cursor
+                                .set_icon(wl_pointer, serial, style.to_icon_names(), scale);
                         }
                     }
                     drop(state);
@@ -1580,7 +1571,7 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
                     if state
                         .keyboard_focused_window
                         .as_ref()
-                        .map_or(false, |keyboard_window| window.ptr_eq(&keyboard_window))
+                        .is_some_and(|keyboard_window| window.ptr_eq(keyboard_window))
                     {
                         state.enter_token = None;
                     }
@@ -1787,17 +1778,17 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
                             drop(state);
                             window.handle_input(input);
                         }
-                    } else if let Some(discrete) = discrete {
-                        if let Some(window) = state.mouse_focused_window.clone() {
-                            let input = PlatformInput::ScrollWheel(ScrollWheelEvent {
-                                position: state.mouse_location.unwrap(),
-                                delta: ScrollDelta::Lines(discrete),
-                                modifiers: state.modifiers,
-                                touch_phase: TouchPhase::Moved,
-                            });
-                            drop(state);
-                            window.handle_input(input);
-                        }
+                    } else if let Some(discrete) = discrete
+                        && let Some(window) = state.mouse_focused_window.clone()
+                    {
+                        let input = PlatformInput::ScrollWheel(ScrollWheelEvent {
+                            position: state.mouse_location.unwrap(),
+                            delta: ScrollDelta::Lines(discrete),
+                            modifiers: state.modifiers,
+                            touch_phase: TouchPhase::Moved,
+                        });
+                        drop(state);
+                        window.handle_input(input);
                     }
                 }
             }
@@ -2019,25 +2010,22 @@ impl Dispatch<wl_data_offer::WlDataOffer, ()> for WaylandClientStatePtr {
         let client = this.get_client();
         let mut state = client.borrow_mut();
 
-        match event {
-            wl_data_offer::Event::Offer { mime_type } => {
-                // Drag and drop
-                if mime_type == FILE_LIST_MIME_TYPE {
-                    let serial = state.serial_tracker.get(SerialKind::DataDevice);
-                    let mime_type = mime_type.clone();
-                    data_offer.accept(serial, Some(mime_type));
-                }
+        if let wl_data_offer::Event::Offer { mime_type } = event {
+            // Drag and drop
+            if mime_type == FILE_LIST_MIME_TYPE {
+                let serial = state.serial_tracker.get(SerialKind::DataDevice);
+                let mime_type = mime_type.clone();
+                data_offer.accept(serial, Some(mime_type));
+            }
 
-                // Clipboard
-                if let Some(offer) = state
-                    .data_offers
-                    .iter_mut()
-                    .find(|wrapper| wrapper.inner.id() == data_offer.id())
-                {
-                    offer.add_mime_type(mime_type);
-                }
+            // Clipboard
+            if let Some(offer) = state
+                .data_offers
+                .iter_mut()
+                .find(|wrapper| wrapper.inner.id() == data_offer.id())
+            {
+                offer.add_mime_type(mime_type);
             }
-            _ => {}
         }
     }
 }
@@ -2118,13 +2106,10 @@ impl Dispatch<zwp_primary_selection_offer_v1::ZwpPrimarySelectionOfferV1, ()>
         let client = this.get_client();
         let mut state = client.borrow_mut();
 
-        match event {
-            zwp_primary_selection_offer_v1::Event::Offer { mime_type } => {
-                if let Some(offer) = state.primary_data_offer.as_mut() {
-                    offer.add_mime_type(mime_type);
-                }
-            }
-            _ => {}
+        if let zwp_primary_selection_offer_v1::Event::Offer { mime_type } = event
+            && let Some(offer) = state.primary_data_offer.as_mut()
+        {
+            offer.add_mime_type(mime_type);
         }
     }
 }

crates/gpui/src/platform/linux/wayland/cursor.rs 🔗

@@ -45,10 +45,11 @@ impl Cursor {
     }
 
     fn set_theme_internal(&mut self, theme_name: Option<String>) {
-        if let Some(loaded_theme) = self.loaded_theme.as_ref() {
-            if loaded_theme.name == theme_name && loaded_theme.scaled_size == self.scaled_size {
-                return;
-            }
+        if let Some(loaded_theme) = self.loaded_theme.as_ref()
+            && loaded_theme.name == theme_name
+            && loaded_theme.scaled_size == self.scaled_size
+        {
+            return;
         }
         let result = if let Some(theme_name) = theme_name.as_ref() {
             CursorTheme::load_from_name(
@@ -66,7 +67,7 @@ impl Cursor {
         {
             self.loaded_theme = Some(LoadedTheme {
                 theme,
-                name: theme_name.map(|name| name.to_string()),
+                name: theme_name,
                 scaled_size: self.scaled_size,
             });
         }
@@ -144,7 +145,7 @@ impl Cursor {
             hot_y as i32 / scale,
         );
 
-        self.surface.attach(Some(&buffer), 0, 0);
+        self.surface.attach(Some(buffer), 0, 0);
         self.surface.damage(0, 0, width as i32, height as i32);
         self.surface.commit();
     }

crates/gpui/src/platform/linux/wayland/window.rs 🔗

@@ -355,85 +355,82 @@ impl WaylandWindowStatePtr {
     }
 
     pub fn handle_xdg_surface_event(&self, event: xdg_surface::Event) {
-        match event {
-            xdg_surface::Event::Configure { serial } => {
-                {
-                    let mut state = self.state.borrow_mut();
-                    if let Some(window_controls) = state.in_progress_window_controls.take() {
-                        state.window_controls = window_controls;
-
-                        drop(state);
-                        let mut callbacks = self.callbacks.borrow_mut();
-                        if let Some(appearance_changed) = callbacks.appearance_changed.as_mut() {
-                            appearance_changed();
-                        }
+        if let xdg_surface::Event::Configure { serial } = event {
+            {
+                let mut state = self.state.borrow_mut();
+                if let Some(window_controls) = state.in_progress_window_controls.take() {
+                    state.window_controls = window_controls;
+
+                    drop(state);
+                    let mut callbacks = self.callbacks.borrow_mut();
+                    if let Some(appearance_changed) = callbacks.appearance_changed.as_mut() {
+                        appearance_changed();
                     }
                 }
-                {
-                    let mut state = self.state.borrow_mut();
-
-                    if let Some(mut configure) = state.in_progress_configure.take() {
-                        let got_unmaximized = state.maximized && !configure.maximized;
-                        state.fullscreen = configure.fullscreen;
-                        state.maximized = configure.maximized;
-                        state.tiling = configure.tiling;
-                        // Limit interactive resizes to once per vblank
-                        if configure.resizing && state.resize_throttle {
-                            return;
-                        } else if configure.resizing {
-                            state.resize_throttle = true;
-                        }
-                        if !configure.fullscreen && !configure.maximized {
-                            configure.size = if got_unmaximized {
-                                Some(state.window_bounds.size)
-                            } else {
-                                compute_outer_size(state.inset(), configure.size, state.tiling)
-                            };
-                            if let Some(size) = configure.size {
-                                state.window_bounds = Bounds {
-                                    origin: Point::default(),
-                                    size,
-                                };
-                            }
-                        }
-                        drop(state);
+            }
+            {
+                let mut state = self.state.borrow_mut();
+
+                if let Some(mut configure) = state.in_progress_configure.take() {
+                    let got_unmaximized = state.maximized && !configure.maximized;
+                    state.fullscreen = configure.fullscreen;
+                    state.maximized = configure.maximized;
+                    state.tiling = configure.tiling;
+                    // Limit interactive resizes to once per vblank
+                    if configure.resizing && state.resize_throttle {
+                        return;
+                    } else if configure.resizing {
+                        state.resize_throttle = true;
+                    }
+                    if !configure.fullscreen && !configure.maximized {
+                        configure.size = if got_unmaximized {
+                            Some(state.window_bounds.size)
+                        } else {
+                            compute_outer_size(state.inset(), configure.size, state.tiling)
+                        };
                         if let Some(size) = configure.size {
-                            self.resize(size);
+                            state.window_bounds = Bounds {
+                                origin: Point::default(),
+                                size,
+                            };
                         }
                     }
-                }
-                let mut state = self.state.borrow_mut();
-                state.xdg_surface.ack_configure(serial);
-
-                let window_geometry = inset_by_tiling(
-                    state.bounds.map_origin(|_| px(0.0)),
-                    state.inset(),
-                    state.tiling,
-                )
-                .map(|v| v.0 as i32)
-                .map_size(|v| if v <= 0 { 1 } else { v });
-
-                state.xdg_surface.set_window_geometry(
-                    window_geometry.origin.x,
-                    window_geometry.origin.y,
-                    window_geometry.size.width,
-                    window_geometry.size.height,
-                );
-
-                let request_frame_callback = !state.acknowledged_first_configure;
-                if request_frame_callback {
-                    state.acknowledged_first_configure = true;
                     drop(state);
-                    self.frame();
+                    if let Some(size) = configure.size {
+                        self.resize(size);
+                    }
                 }
             }
-            _ => {}
+            let mut state = self.state.borrow_mut();
+            state.xdg_surface.ack_configure(serial);
+
+            let window_geometry = inset_by_tiling(
+                state.bounds.map_origin(|_| px(0.0)),
+                state.inset(),
+                state.tiling,
+            )
+            .map(|v| v.0 as i32)
+            .map_size(|v| if v <= 0 { 1 } else { v });
+
+            state.xdg_surface.set_window_geometry(
+                window_geometry.origin.x,
+                window_geometry.origin.y,
+                window_geometry.size.width,
+                window_geometry.size.height,
+            );
+
+            let request_frame_callback = !state.acknowledged_first_configure;
+            if request_frame_callback {
+                state.acknowledged_first_configure = true;
+                drop(state);
+                self.frame();
+            }
         }
     }
 
     pub fn handle_toplevel_decoration_event(&self, event: zxdg_toplevel_decoration_v1::Event) {
-        match event {
-            zxdg_toplevel_decoration_v1::Event::Configure { mode } => match mode {
+        if let zxdg_toplevel_decoration_v1::Event::Configure { mode } = event {
+            match mode {
                 WEnum::Value(zxdg_toplevel_decoration_v1::Mode::ServerSide) => {
                     self.state.borrow_mut().decorations = WindowDecorations::Server;
                     if let Some(mut appearance_changed) =
@@ -457,17 +454,13 @@ impl WaylandWindowStatePtr {
                 WEnum::Unknown(v) => {
                     log::warn!("Unknown decoration mode: {}", v);
                 }
-            },
-            _ => {}
+            }
         }
     }
 
     pub fn handle_fractional_scale_event(&self, event: wp_fractional_scale_v1::Event) {
-        match event {
-            wp_fractional_scale_v1::Event::PreferredScale { scale } => {
-                self.rescale(scale as f32 / 120.0);
-            }
-            _ => {}
+        if let wp_fractional_scale_v1::Event::PreferredScale { scale } = event {
+            self.rescale(scale as f32 / 120.0);
         }
     }
 
@@ -669,8 +662,8 @@ impl WaylandWindowStatePtr {
     pub fn set_size_and_scale(&self, size: Option<Size<Pixels>>, scale: Option<f32>) {
         let (size, scale) = {
             let mut state = self.state.borrow_mut();
-            if size.map_or(true, |size| size == state.bounds.size)
-                && scale.map_or(true, |scale| scale == state.scale)
+            if size.is_none_or(|size| size == state.bounds.size)
+                && scale.is_none_or(|scale| scale == state.scale)
             {
                 return;
             }
@@ -713,21 +706,20 @@ impl WaylandWindowStatePtr {
     }
 
     pub fn handle_input(&self, input: PlatformInput) {
-        if let Some(ref mut fun) = self.callbacks.borrow_mut().input {
-            if !fun(input.clone()).propagate {
-                return;
-            }
+        if let Some(ref mut fun) = self.callbacks.borrow_mut().input
+            && !fun(input.clone()).propagate
+        {
+            return;
         }
-        if let PlatformInput::KeyDown(event) = input {
-            if event.keystroke.modifiers.is_subset_of(&Modifiers::shift()) {
-                if let Some(key_char) = &event.keystroke.key_char {
-                    let mut state = self.state.borrow_mut();
-                    if let Some(mut input_handler) = state.input_handler.take() {
-                        drop(state);
-                        input_handler.replace_text_in_range(None, key_char);
-                        self.state.borrow_mut().input_handler = Some(input_handler);
-                    }
-                }
+        if let PlatformInput::KeyDown(event) = input
+            && event.keystroke.modifiers.is_subset_of(&Modifiers::shift())
+            && let Some(key_char) = &event.keystroke.key_char
+        {
+            let mut state = self.state.borrow_mut();
+            if let Some(mut input_handler) = state.input_handler.take() {
+                drop(state);
+                input_handler.replace_text_in_range(None, key_char);
+                self.state.borrow_mut().input_handler = Some(input_handler);
             }
         }
     }
@@ -1147,7 +1139,7 @@ fn update_window(mut state: RefMut<WaylandWindowState>) {
 }
 
 impl WindowDecorations {
-    fn to_xdg(&self) -> zxdg_toplevel_decoration_v1::Mode {
+    fn to_xdg(self) -> zxdg_toplevel_decoration_v1::Mode {
         match self {
             WindowDecorations::Client => zxdg_toplevel_decoration_v1::Mode::ClientSide,
             WindowDecorations::Server => zxdg_toplevel_decoration_v1::Mode::ServerSide,
@@ -1156,7 +1148,7 @@ impl WindowDecorations {
 }
 
 impl ResizeEdge {
-    fn to_xdg(&self) -> xdg_toplevel::ResizeEdge {
+    fn to_xdg(self) -> xdg_toplevel::ResizeEdge {
         match self {
             ResizeEdge::Top => xdg_toplevel::ResizeEdge::Top,
             ResizeEdge::TopRight => xdg_toplevel::ResizeEdge::TopRight,

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

@@ -232,15 +232,12 @@ impl X11ClientStatePtr {
         };
         let mut state = client.0.borrow_mut();
 
-        if let Some(window_ref) = state.windows.remove(&x_window) {
-            match window_ref.refresh_state {
-                Some(RefreshState::PeriodicRefresh {
-                    event_loop_token, ..
-                }) => {
-                    state.loop_handle.remove(event_loop_token);
-                }
-                _ => {}
-            }
+        if let Some(window_ref) = state.windows.remove(&x_window)
+            && let Some(RefreshState::PeriodicRefresh {
+                event_loop_token, ..
+            }) = window_ref.refresh_state
+        {
+            state.loop_handle.remove(event_loop_token);
         }
         if state.mouse_focused_window == Some(x_window) {
             state.mouse_focused_window = None;
@@ -459,7 +456,7 @@ impl X11Client {
                 move |event, _, client| match event {
                     XDPEvent::WindowAppearance(appearance) => {
                         client.with_common(|common| common.appearance = appearance);
-                        for (_, window) in &mut client.0.borrow_mut().windows {
+                        for window in client.0.borrow_mut().windows.values_mut() {
                             window.window.set_appearance(appearance);
                         }
                     }
@@ -565,10 +562,10 @@ impl X11Client {
                                     events.push(last_keymap_change_event);
                                 }
 
-                                if let Some(last_press) = last_key_press.as_ref() {
-                                    if last_press.detail == key_press.detail {
-                                        continue;
-                                    }
+                                if let Some(last_press) = last_key_press.as_ref()
+                                    && last_press.detail == key_press.detail
+                                {
+                                    continue;
                                 }
 
                                 if let Some(Event::KeyRelease(key_release)) =
@@ -642,13 +639,7 @@ impl X11Client {
                 let xim_connected = xim_handler.connected;
                 drop(state);
 
-                let xim_filtered = match ximc.filter_event(&event, &mut xim_handler) {
-                    Ok(handled) => handled,
-                    Err(err) => {
-                        log::error!("XIMClientError: {}", err);
-                        false
-                    }
-                };
+                let xim_filtered = ximc.filter_event(&event, &mut xim_handler);
                 let xim_callback_event = xim_handler.last_callback_event.take();
 
                 let mut state = self.0.borrow_mut();
@@ -659,14 +650,28 @@ impl X11Client {
                     self.handle_xim_callback_event(event);
                 }
 
-                if xim_filtered {
-                    continue;
-                }
-
-                if xim_connected {
-                    self.xim_handle_event(event);
-                } else {
-                    self.handle_event(event);
+                match xim_filtered {
+                    Ok(handled) => {
+                        if handled {
+                            continue;
+                        }
+                        if xim_connected {
+                            self.xim_handle_event(event);
+                        } else {
+                            self.handle_event(event);
+                        }
+                    }
+                    Err(err) => {
+                        // this might happen when xim server crashes on one of the events
+                        // we do lose 1-2 keys when crash happens since there is no reliable way to get that info
+                        // luckily, x11 sends us window not found error when xim server crashes upon further key press
+                        // hence we fall back to handle_event
+                        log::error!("XIMClientError: {}", err);
+                        let mut state = self.0.borrow_mut();
+                        state.take_xim();
+                        drop(state);
+                        self.handle_event(event);
+                    }
                 }
             }
         }
@@ -868,22 +873,19 @@ impl X11Client {
                 let Some(reply) = reply else {
                     return Some(());
                 };
-                match str::from_utf8(&reply.value) {
-                    Ok(file_list) => {
-                        let paths: SmallVec<[_; 2]> = file_list
-                            .lines()
-                            .filter_map(|path| Url::parse(path).log_err())
-                            .filter_map(|url| url.to_file_path().log_err())
-                            .collect();
-                        let input = PlatformInput::FileDrop(FileDropEvent::Entered {
-                            position: state.xdnd_state.position,
-                            paths: crate::ExternalPaths(paths),
-                        });
-                        drop(state);
-                        window.handle_input(input);
-                        self.0.borrow_mut().xdnd_state.retrieved = true;
-                    }
-                    Err(_) => {}
+                if let Ok(file_list) = str::from_utf8(&reply.value) {
+                    let paths: SmallVec<[_; 2]> = file_list
+                        .lines()
+                        .filter_map(|path| Url::parse(path).log_err())
+                        .filter_map(|url| url.to_file_path().log_err())
+                        .collect();
+                    let input = PlatformInput::FileDrop(FileDropEvent::Entered {
+                        position: state.xdnd_state.position,
+                        paths: crate::ExternalPaths(paths),
+                    });
+                    drop(state);
+                    window.handle_input(input);
+                    self.0.borrow_mut().xdnd_state.retrieved = true;
                 }
             }
             Event::ConfigureNotify(event) => {
@@ -1204,7 +1206,7 @@ impl X11Client {
 
                 state = self.0.borrow_mut();
                 if let Some(mut pointer) = state.pointer_device_states.get_mut(&event.sourceid) {
-                    let scroll_delta = get_scroll_delta_and_update_state(&mut pointer, &event);
+                    let scroll_delta = get_scroll_delta_and_update_state(pointer, &event);
                     drop(state);
                     if let Some(scroll_delta) = scroll_delta {
                         window.handle_input(PlatformInput::ScrollWheel(make_scroll_wheel_event(
@@ -1263,7 +1265,7 @@ impl X11Client {
             Event::XinputDeviceChanged(event) => {
                 let mut state = self.0.borrow_mut();
                 if let Some(mut pointer) = state.pointer_device_states.get_mut(&event.sourceid) {
-                    reset_pointer_device_scroll_positions(&mut pointer);
+                    reset_pointer_device_scroll_positions(pointer);
                 }
             }
             _ => {}
@@ -1327,7 +1329,7 @@ impl X11Client {
         state.composing = false;
         drop(state);
         if let Some(mut keystroke) = keystroke {
-            keystroke.key_char = Some(text.clone());
+            keystroke.key_char = Some(text);
             window.handle_input(PlatformInput::KeyDown(crate::KeyDownEvent {
                 keystroke,
                 is_held: false,
@@ -1578,11 +1580,11 @@ impl LinuxClient for X11Client {
 
     fn read_from_primary(&self) -> Option<crate::ClipboardItem> {
         let state = self.0.borrow_mut();
-        return state
+        state
             .clipboard
             .get_any(clipboard::ClipboardKind::Primary)
             .context("X11: Failed to read from clipboard (primary)")
-            .log_with_level(log::Level::Debug);
+            .log_with_level(log::Level::Debug)
     }
 
     fn read_from_clipboard(&self) -> Option<crate::ClipboardItem> {
@@ -1595,11 +1597,11 @@ impl LinuxClient for X11Client {
         {
             return state.clipboard_item.clone();
         }
-        return state
+        state
             .clipboard
             .get_any(clipboard::ClipboardKind::Clipboard)
             .context("X11: Failed to read from clipboard (clipboard)")
-            .log_with_level(log::Level::Debug);
+            .log_with_level(log::Level::Debug)
     }
 
     fn run(&self) {
@@ -2002,12 +2004,12 @@ fn check_gtk_frame_extents_supported(
 }
 
 fn xdnd_is_atom_supported(atom: u32, atoms: &XcbAtoms) -> bool {
-    return atom == atoms.TEXT
+    atom == atoms.TEXT
         || atom == atoms.STRING
         || atom == atoms.UTF8_STRING
         || atom == atoms.TEXT_PLAIN
         || atom == atoms.TEXT_PLAIN_UTF8
-        || atom == atoms.TextUriList;
+        || atom == atoms.TextUriList
 }
 
 fn xdnd_get_supported_atom(
@@ -2027,16 +2029,15 @@ fn xdnd_get_supported_atom(
         ),
     )
     .log_with_level(Level::Warn)
+        && let Some(atoms) = reply.value32()
     {
-        if let Some(atoms) = reply.value32() {
-            for atom in atoms {
-                if xdnd_is_atom_supported(atom, &supported_atoms) {
-                    return atom;
-                }
+        for atom in atoms {
+            if xdnd_is_atom_supported(atom, supported_atoms) {
+                return atom;
             }
         }
     }
-    return 0;
+    0
 }
 
 fn xdnd_send_finished(
@@ -2107,7 +2108,7 @@ fn current_pointer_device_states(
                     .classes
                     .iter()
                     .filter_map(|class| class.data.as_scroll())
-                    .map(|class| *class)
+                    .copied()
                     .rev()
                     .collect::<Vec<_>>();
                 let old_state = scroll_values_to_preserve.get(&info.deviceid);
@@ -2137,7 +2138,7 @@ fn current_pointer_device_states(
     if pointer_device_states.is_empty() {
         log::error!("Found no xinput mouse pointers.");
     }
-    return Some(pointer_device_states);
+    Some(pointer_device_states)
 }
 
 /// Returns true if the device is a pointer device. Does not include pointer device groups.
@@ -2403,11 +2404,13 @@ fn legacy_get_randr_scale_factor(connection: &XCBConnection, root: u32) -> Optio
     let mut crtc_infos: HashMap<randr::Crtc, randr::GetCrtcInfoReply> = HashMap::default();
     let mut valid_outputs: HashSet<randr::Output> = HashSet::new();
     for (crtc, cookie) in crtc_cookies {
-        if let Ok(reply) = cookie.reply() {
-            if reply.width > 0 && reply.height > 0 && !reply.outputs.is_empty() {
-                crtc_infos.insert(crtc, reply.clone());
-                valid_outputs.extend(&reply.outputs);
-            }
+        if let Ok(reply) = cookie.reply()
+            && reply.width > 0
+            && reply.height > 0
+            && !reply.outputs.is_empty()
+        {
+            crtc_infos.insert(crtc, reply.clone());
+            valid_outputs.extend(&reply.outputs);
         }
     }
 

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

@@ -1078,11 +1078,11 @@ impl Clipboard {
         } else {
             String::from_utf8(result.bytes).map_err(|_| Error::ConversionFailure)?
         };
-        return Ok(ClipboardItem::new_string(text));
+        Ok(ClipboardItem::new_string(text))
     }
 
     pub fn is_owner(&self, selection: ClipboardKind) -> bool {
-        return self.inner.is_owner(selection).unwrap_or(false);
+        self.inner.is_owner(selection).unwrap_or(false)
     }
 }
 
@@ -1120,25 +1120,25 @@ impl Drop for Clipboard {
                 log::error!("Failed to flush the clipboard window. Error: {}", e);
                 return;
             }
-            if let Some(global_cb) = global_cb {
-                if let Err(e) = global_cb.server_handle.join() {
-                    // Let's try extracting the error message
-                    let message;
-                    if let Some(msg) = e.downcast_ref::<&'static str>() {
-                        message = Some((*msg).to_string());
-                    } else if let Some(msg) = e.downcast_ref::<String>() {
-                        message = Some(msg.clone());
-                    } else {
-                        message = None;
-                    }
-                    if let Some(message) = message {
-                        log::error!(
-                            "The clipboard server thread panicked. Panic message: '{}'",
-                            message,
-                        );
-                    } else {
-                        log::error!("The clipboard server thread panicked.");
-                    }
+            if let Some(global_cb) = global_cb
+                && let Err(e) = global_cb.server_handle.join()
+            {
+                // Let's try extracting the error message
+                let message;
+                if let Some(msg) = e.downcast_ref::<&'static str>() {
+                    message = Some((*msg).to_string());
+                } else if let Some(msg) = e.downcast_ref::<String>() {
+                    message = Some(msg.clone());
+                } else {
+                    message = None;
+                }
+                if let Some(message) = message {
+                    log::error!(
+                        "The clipboard server thread panicked. Panic message: '{}'",
+                        message,
+                    );
+                } else {
+                    log::error!("The clipboard server thread panicked.");
                 }
             }
         }

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

@@ -73,8 +73,8 @@ pub(crate) fn get_valuator_axis_index(
     // valuator present in this event's axisvalues. Axisvalues is ordered from
     // lowest valuator number to highest, so counting bits before the 1 bit for
     // this valuator yields the index in axisvalues.
-    if bit_is_set_in_vec(&valuator_mask, valuator_number) {
-        Some(popcount_upto_bit_index(&valuator_mask, valuator_number) as usize)
+    if bit_is_set_in_vec(valuator_mask, valuator_number) {
+        Some(popcount_upto_bit_index(valuator_mask, valuator_number) as usize)
     } else {
         None
     }
@@ -104,7 +104,7 @@ fn bit_is_set_in_vec(bit_vec: &Vec<u32>, bit_index: u16) -> bool {
     let array_index = bit_index as usize / 32;
     bit_vec
         .get(array_index)
-        .map_or(false, |bits| bit_is_set(*bits, bit_index % 32))
+        .is_some_and(|bits| bit_is_set(*bits, bit_index % 32))
 }
 
 fn bit_is_set(bits: u32, bit_index: u16) -> bool {

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

@@ -95,7 +95,7 @@ fn query_render_extent(
 }
 
 impl ResizeEdge {
-    fn to_moveresize(&self) -> u32 {
+    fn to_moveresize(self) -> u32 {
         match self {
             ResizeEdge::TopLeft => 0,
             ResizeEdge::Top => 1,
@@ -397,7 +397,7 @@ impl X11WindowState {
             .display_id
             .map_or(x_main_screen_index, |did| did.0 as usize);
 
-        let visual_set = find_visuals(&xcb, x_screen_index);
+        let visual_set = find_visuals(xcb, x_screen_index);
 
         let visual = match visual_set.transparent {
             Some(visual) => visual,
@@ -515,19 +515,19 @@ impl X11WindowState {
                     xcb.configure_window(x_window, &xproto::ConfigureWindowAux::new().x(x).y(y)),
                 )?;
             }
-            if let Some(titlebar) = params.titlebar {
-                if let Some(title) = titlebar.title {
-                    check_reply(
-                        || "X11 ChangeProperty8 on window title failed.",
-                        xcb.change_property8(
-                            xproto::PropMode::REPLACE,
-                            x_window,
-                            xproto::AtomEnum::WM_NAME,
-                            xproto::AtomEnum::STRING,
-                            title.as_bytes(),
-                        ),
-                    )?;
-                }
+            if let Some(titlebar) = params.titlebar
+                && let Some(title) = titlebar.title
+            {
+                check_reply(
+                    || "X11 ChangeProperty8 on window title failed.",
+                    xcb.change_property8(
+                        xproto::PropMode::REPLACE,
+                        x_window,
+                        xproto::AtomEnum::WM_NAME,
+                        xproto::AtomEnum::STRING,
+                        title.as_bytes(),
+                    ),
+                )?;
             }
             if params.kind == WindowKind::PopUp {
                 check_reply(
@@ -604,7 +604,7 @@ impl X11WindowState {
                 ),
             )?;
 
-            xcb_flush(&xcb);
+            xcb_flush(xcb);
 
             let renderer = {
                 let raw_window = RawWindow {
@@ -664,7 +664,7 @@ impl X11WindowState {
                 || "X11 DestroyWindow failed while cleaning it up after setup failure.",
                 xcb.destroy_window(x_window),
             )?;
-            xcb_flush(&xcb);
+            xcb_flush(xcb);
         }
 
         setup_result
@@ -956,10 +956,10 @@ impl X11WindowStatePtr {
     }
 
     pub fn handle_input(&self, input: PlatformInput) {
-        if let Some(ref mut fun) = self.callbacks.borrow_mut().input {
-            if !fun(input.clone()).propagate {
-                return;
-            }
+        if let Some(ref mut fun) = self.callbacks.borrow_mut().input
+            && !fun(input.clone()).propagate
+        {
+            return;
         }
         if let PlatformInput::KeyDown(event) = input {
             // only allow shift modifier when inserting text
@@ -1068,15 +1068,14 @@ impl X11WindowStatePtr {
         }
 
         let mut callbacks = self.callbacks.borrow_mut();
-        if let Some((content_size, scale_factor)) = resize_args {
-            if let Some(ref mut fun) = callbacks.resize {
-                fun(content_size, scale_factor)
-            }
+        if let Some((content_size, scale_factor)) = resize_args
+            && let Some(ref mut fun) = callbacks.resize
+        {
+            fun(content_size, scale_factor)
         }
-        if !is_resize {
-            if let Some(ref mut fun) = callbacks.moved {
-                fun();
-            }
+
+        if !is_resize && let Some(ref mut fun) = callbacks.moved {
+            fun();
         }
 
         Ok(())

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

@@ -311,9 +311,8 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke {
         let mut shift = modifiers.contains(NSEventModifierFlags::NSShiftKeyMask);
         let command = modifiers.contains(NSEventModifierFlags::NSCommandKeyMask);
         let function = modifiers.contains(NSEventModifierFlags::NSFunctionKeyMask)
-            && first_char.map_or(true, |ch| {
-                !(NSUpArrowFunctionKey..=NSModeSwitchFunctionKey).contains(&ch)
-            });
+            && first_char
+                .is_none_or(|ch| !(NSUpArrowFunctionKey..=NSModeSwitchFunctionKey).contains(&ch));
 
         #[allow(non_upper_case_globals)]
         let key = match first_char {
@@ -427,7 +426,7 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke {
                     key_char = Some(chars_for_modified_key(native_event.keyCode(), mods));
                 }
 
-                let mut key = if shift
+                if shift
                     && chars_ignoring_modifiers
                         .chars()
                         .all(|c| c.is_ascii_lowercase())
@@ -438,9 +437,7 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke {
                     chars_with_shift
                 } else {
                     chars_ignoring_modifiers
-                };
-
-                key
+                }
             }
         };
 

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

@@ -314,6 +314,15 @@ impl MetalRenderer {
     }
 
     fn update_path_intermediate_textures(&mut self, size: Size<DevicePixels>) {
+        // We are uncertain when this happens, but sometimes size can be 0 here. Most likely before
+        // the layout pass on window creation. Zero-sized texture creation causes SIGABRT.
+        // https://github.com/zed-industries/zed/issues/36229
+        if size.width.0 <= 0 || size.height.0 <= 0 {
+            self.path_intermediate_texture = None;
+            self.path_intermediate_msaa_texture = None;
+            return;
+        }
+
         let texture_descriptor = metal::TextureDescriptor::new();
         texture_descriptor.set_width(size.width.0 as u64);
         texture_descriptor.set_height(size.height.0 as u64);
@@ -323,7 +332,7 @@ impl MetalRenderer {
         self.path_intermediate_texture = Some(self.device.new_texture(&texture_descriptor));
 
         if self.path_sample_count > 1 {
-            let mut msaa_descriptor = texture_descriptor.clone();
+            let mut msaa_descriptor = texture_descriptor;
             msaa_descriptor.set_texture_type(metal::MTLTextureType::D2Multisample);
             msaa_descriptor.set_storage_mode(metal::MTLStorageMode::Private);
             msaa_descriptor.set_sample_count(self.path_sample_count as _);
@@ -436,14 +445,14 @@ impl MetalRenderer {
                     instance_buffer,
                     &mut instance_offset,
                     viewport_size,
-                    &command_encoder,
+                    command_encoder,
                 ),
                 PrimitiveBatch::Quads(quads) => self.draw_quads(
                     quads,
                     instance_buffer,
                     &mut instance_offset,
                     viewport_size,
-                    &command_encoder,
+                    command_encoder,
                 ),
                 PrimitiveBatch::Paths(paths) => {
                     command_encoder.end_encoding();
@@ -471,7 +480,7 @@ impl MetalRenderer {
                             instance_buffer,
                             &mut instance_offset,
                             viewport_size,
-                            &command_encoder,
+                            command_encoder,
                         )
                     } else {
                         false
@@ -482,7 +491,7 @@ impl MetalRenderer {
                     instance_buffer,
                     &mut instance_offset,
                     viewport_size,
-                    &command_encoder,
+                    command_encoder,
                 ),
                 PrimitiveBatch::MonochromeSprites {
                     texture_id,
@@ -493,7 +502,7 @@ impl MetalRenderer {
                     instance_buffer,
                     &mut instance_offset,
                     viewport_size,
-                    &command_encoder,
+                    command_encoder,
                 ),
                 PrimitiveBatch::PolychromeSprites {
                     texture_id,
@@ -504,14 +513,14 @@ impl MetalRenderer {
                     instance_buffer,
                     &mut instance_offset,
                     viewport_size,
-                    &command_encoder,
+                    command_encoder,
                 ),
                 PrimitiveBatch::Surfaces(surfaces) => self.draw_surfaces(
                     surfaces,
                     instance_buffer,
                     &mut instance_offset,
                     viewport_size,
-                    &command_encoder,
+                    command_encoder,
                 ),
             };
             if !ok {
@@ -754,7 +763,7 @@ impl MetalRenderer {
         viewport_size: Size<DevicePixels>,
         command_encoder: &metal::RenderCommandEncoderRef,
     ) -> bool {
-        let Some(ref first_path) = paths.first() else {
+        let Some(first_path) = paths.first() else {
             return true;
         };
 

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

@@ -35,14 +35,14 @@ pub fn apply_features_and_fallbacks(
     unsafe {
         let mut keys = vec![kCTFontFeatureSettingsAttribute];
         let mut values = vec![generate_feature_array(features)];
-        if let Some(fallbacks) = fallbacks {
-            if !fallbacks.fallback_list().is_empty() {
-                keys.push(kCTFontCascadeListAttribute);
-                values.push(generate_fallback_array(
-                    fallbacks,
-                    font.native_font().as_concrete_TypeRef(),
-                ));
-            }
+        if let Some(fallbacks) = fallbacks
+            && !fallbacks.fallback_list().is_empty()
+        {
+            keys.push(kCTFontCascadeListAttribute);
+            values.push(generate_fallback_array(
+                fallbacks,
+                font.native_font().as_concrete_TypeRef(),
+            ));
         }
         let attrs = CFDictionaryCreate(
             kCFAllocatorDefault,

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

@@ -7,9 +7,9 @@ use super::{
 use crate::{
     Action, AnyWindowHandle, BackgroundExecutor, ClipboardEntry, ClipboardItem, ClipboardString,
     CursorStyle, ForegroundExecutor, Image, ImageFormat, KeyContext, Keymap, MacDispatcher,
-    MacDisplay, MacWindow, Menu, MenuItem, OwnedMenu, PathPromptOptions, Platform, PlatformDisplay,
-    PlatformKeyboardLayout, PlatformTextSystem, PlatformWindow, Result, SemanticVersion, Task,
-    WindowAppearance, WindowParams, hash,
+    MacDisplay, MacWindow, Menu, MenuItem, OsMenu, OwnedMenu, PathPromptOptions, Platform,
+    PlatformDisplay, PlatformKeyboardLayout, PlatformTextSystem, PlatformWindow, Result,
+    SemanticVersion, SystemMenuType, Task, WindowAppearance, WindowParams, hash,
 };
 use anyhow::{Context as _, anyhow};
 use block::ConcreteBlock;
@@ -47,7 +47,7 @@ use objc::{
 use parking_lot::Mutex;
 use ptr::null_mut;
 use std::{
-    cell::{Cell, LazyCell},
+    cell::Cell,
     convert::TryInto,
     ffi::{CStr, OsStr, c_void},
     os::{raw::c_char, unix::ffi::OsStrExt},
@@ -56,7 +56,7 @@ use std::{
     ptr,
     rc::Rc,
     slice, str,
-    sync::Arc,
+    sync::{Arc, OnceLock},
 };
 use strum::IntoEnumIterator;
 use util::ResultExt;
@@ -296,18 +296,7 @@ impl MacPlatform {
         actions: &mut Vec<Box<dyn Action>>,
         keymap: &Keymap,
     ) -> id {
-        const DEFAULT_CONTEXT: LazyCell<Vec<KeyContext>> = LazyCell::new(|| {
-            let mut workspace_context = KeyContext::new_with_defaults();
-            workspace_context.add("Workspace");
-            let mut pane_context = KeyContext::new_with_defaults();
-            pane_context.add("Pane");
-            let mut editor_context = KeyContext::new_with_defaults();
-            editor_context.add("Editor");
-
-            pane_context.extend(&editor_context);
-            workspace_context.extend(&pane_context);
-            vec![workspace_context]
-        });
+        static DEFAULT_CONTEXT: OnceLock<Vec<KeyContext>> = OnceLock::new();
 
         unsafe {
             match item {
@@ -323,9 +312,20 @@ impl MacPlatform {
                     let keystrokes = keymap
                         .bindings_for_action(action.as_ref())
                         .find_or_first(|binding| {
-                            binding
-                                .predicate()
-                                .is_none_or(|predicate| predicate.eval(&DEFAULT_CONTEXT))
+                            binding.predicate().is_none_or(|predicate| {
+                                predicate.eval(DEFAULT_CONTEXT.get_or_init(|| {
+                                    let mut workspace_context = KeyContext::new_with_defaults();
+                                    workspace_context.add("Workspace");
+                                    let mut pane_context = KeyContext::new_with_defaults();
+                                    pane_context.add("Pane");
+                                    let mut editor_context = KeyContext::new_with_defaults();
+                                    editor_context.add("Editor");
+
+                                    pane_context.extend(&editor_context);
+                                    workspace_context.extend(&pane_context);
+                                    vec![workspace_context]
+                                }))
+                            })
                         })
                         .map(|binding| binding.keystrokes());
 
@@ -371,7 +371,7 @@ impl MacPlatform {
 
                             item = NSMenuItem::alloc(nil)
                                 .initWithTitle_action_keyEquivalent_(
-                                    ns_string(&name),
+                                    ns_string(name),
                                     selector,
                                     ns_string(key_to_native(&keystroke.key).as_ref()),
                                 )
@@ -383,7 +383,7 @@ impl MacPlatform {
                         } else {
                             item = NSMenuItem::alloc(nil)
                                 .initWithTitle_action_keyEquivalent_(
-                                    ns_string(&name),
+                                    ns_string(name),
                                     selector,
                                     ns_string(""),
                                 )
@@ -392,7 +392,7 @@ impl MacPlatform {
                     } else {
                         item = NSMenuItem::alloc(nil)
                             .initWithTitle_action_keyEquivalent_(
-                                ns_string(&name),
+                                ns_string(name),
                                 selector,
                                 ns_string(""),
                             )
@@ -412,10 +412,21 @@ impl MacPlatform {
                         submenu.addItem_(Self::create_menu_item(item, delegate, actions, keymap));
                     }
                     item.setSubmenu_(submenu);
-                    item.setTitle_(ns_string(&name));
-                    if name == "Services" {
-                        let app: id = msg_send![APP_CLASS, sharedApplication];
-                        app.setServicesMenu_(item);
+                    item.setTitle_(ns_string(name));
+                    item
+                }
+                MenuItem::SystemMenu(OsMenu { name, menu_type }) => {
+                    let item = NSMenuItem::new(nil).autorelease();
+                    let submenu = NSMenu::new(nil).autorelease();
+                    submenu.setDelegate_(delegate);
+                    item.setSubmenu_(submenu);
+                    item.setTitle_(ns_string(name));
+
+                    match menu_type {
+                        SystemMenuType::Services => {
+                            let app: id = msg_send![APP_CLASS, sharedApplication];
+                            app.setServicesMenu_(item);
+                        }
                     }
 
                     item
@@ -694,6 +705,7 @@ impl Platform for MacPlatform {
                     panel.setCanChooseDirectories_(options.directories.to_objc());
                     panel.setCanChooseFiles_(options.files.to_objc());
                     panel.setAllowsMultipleSelection_(options.multiple.to_objc());
+
                     panel.setCanCreateDirectories(true.to_objc());
                     panel.setResolvesAliases_(false.to_objc());
                     let done_tx = Cell::new(Some(done_tx));
@@ -703,10 +715,10 @@ impl Platform for MacPlatform {
                             let urls = panel.URLs();
                             for i in 0..urls.count() {
                                 let url = urls.objectAtIndex(i);
-                                if url.isFileURL() == YES {
-                                    if let Ok(path) = ns_url_to_path(url) {
-                                        result.push(path)
-                                    }
+                                if url.isFileURL() == YES
+                                    && let Ok(path) = ns_url_to_path(url)
+                                {
+                                    result.push(path)
                                 }
                             }
                             Some(result)
@@ -719,6 +731,11 @@ impl Platform for MacPlatform {
                         }
                     });
                     let block = block.copy();
+
+                    if let Some(prompt) = options.prompt {
+                        let _: () = msg_send![panel, setPrompt: ns_string(&prompt)];
+                    }
+
                     let _: () = msg_send![panel, beginWithCompletionHandler: block];
                 }
             })
@@ -726,8 +743,13 @@ impl Platform for MacPlatform {
         done_rx
     }
 
-    fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Result<Option<PathBuf>>> {
+    fn prompt_for_new_path(
+        &self,
+        directory: &Path,
+        suggested_name: Option<&str>,
+    ) -> oneshot::Receiver<Result<Option<PathBuf>>> {
         let directory = directory.to_owned();
+        let suggested_name = suggested_name.map(|s| s.to_owned());
         let (done_tx, done_rx) = oneshot::channel();
         self.foreground_executor()
             .spawn(async move {
@@ -737,6 +759,11 @@ impl Platform for MacPlatform {
                     let url = NSURL::fileURLWithPath_isDirectory_(nil, path, true.to_objc());
                     panel.setDirectoryURL(url);
 
+                    if let Some(suggested_name) = suggested_name {
+                        let name_string = ns_string(&suggested_name);
+                        let _: () = msg_send![panel, setNameFieldStringValue: name_string];
+                    }
+
                     let done_tx = Cell::new(Some(done_tx));
                     let block = ConcreteBlock::new(move |response: NSModalResponse| {
                         let mut result = None;
@@ -759,17 +786,18 @@ impl Platform for MacPlatform {
                                     // This is conditional on OS version because I'd like to get rid of it, so that
                                     // you can manually create a file called `a.sql.s`. That said it seems better
                                     // to break that use-case than breaking `a.sql`.
-                                    if chunks.len() == 3 && chunks[1].starts_with(chunks[2]) {
-                                        if Self::os_version() >= SemanticVersion::new(15, 0, 0) {
-                                            let new_filename = OsStr::from_bytes(
-                                                &filename.as_bytes()
-                                                    [..chunks[0].len() + 1 + chunks[1].len()],
-                                            )
-                                            .to_owned();
-                                            result.set_file_name(&new_filename);
-                                        }
+                                    if chunks.len() == 3
+                                        && chunks[1].starts_with(chunks[2])
+                                        && Self::os_version() >= SemanticVersion::new(15, 0, 0)
+                                    {
+                                        let new_filename = OsStr::from_bytes(
+                                            &filename.as_bytes()
+                                                [..chunks[0].len() + 1 + chunks[1].len()],
+                                        )
+                                        .to_owned();
+                                        result.set_file_name(&new_filename);
                                     }
-                                    return result;
+                                    result
                                 })
                             }
                         }

crates/gpui/src/platform/mac/shaders.metal 🔗

@@ -567,15 +567,20 @@ vertex UnderlineVertexOutput underline_vertex(
 fragment float4 underline_fragment(UnderlineFragmentInput input [[stage_in]],
                                    constant Underline *underlines
                                    [[buffer(UnderlineInputIndex_Underlines)]]) {
+  const float WAVE_FREQUENCY = 2.0;
+  const float WAVE_HEIGHT_RATIO = 0.8;
+
   Underline underline = underlines[input.underline_id];
   if (underline.wavy) {
     float half_thickness = underline.thickness * 0.5;
     float2 origin =
         float2(underline.bounds.origin.x, underline.bounds.origin.y);
+
     float2 st = ((input.position.xy - origin) / underline.bounds.size.height) -
                 float2(0., 0.5);
-    float frequency = (M_PI_F * (3. * underline.thickness)) / 8.;
-    float amplitude = 1. / (2. * underline.thickness);
+    float frequency = (M_PI_F * WAVE_FREQUENCY * underline.thickness) / underline.bounds.size.height;
+    float amplitude = (underline.thickness * WAVE_HEIGHT_RATIO) / underline.bounds.size.height;
+
     float sine = sin(st.x * frequency) * amplitude;
     float dSine = cos(st.x * frequency) * amplitude * frequency;
     float distance = (st.y - sine) / sqrt(1. + dSine * dSine);

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

@@ -211,11 +211,7 @@ impl MacTextSystemState {
         features: &FontFeatures,
         fallbacks: Option<&FontFallbacks>,
     ) -> Result<SmallVec<[FontId; 4]>> {
-        let name = if name == ".SystemUIFont" {
-            ".AppleSystemUIFont"
-        } else {
-            name
-        };
+        let name = crate::text_system::font_name_with_fallbacks(name, ".AppleSystemUIFont");
 
         let mut font_ids = SmallVec::new();
         let family = self
@@ -323,7 +319,7 @@ impl MacTextSystemState {
     fn is_emoji(&self, font_id: FontId) -> bool {
         self.postscript_names_by_font_id
             .get(&font_id)
-            .map_or(false, |postscript_name| {
+            .is_some_and(|postscript_name| {
                 postscript_name == "AppleColorEmoji" || postscript_name == ".AppleColorEmojiUI"
             })
     }

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

@@ -653,7 +653,7 @@ impl MacWindow {
                     .and_then(|titlebar| titlebar.traffic_light_position),
                 transparent_titlebar: titlebar
                     .as_ref()
-                    .map_or(true, |titlebar| titlebar.appears_transparent),
+                    .is_none_or(|titlebar| titlebar.appears_transparent),
                 previous_modifiers_changed_event: None,
                 keystroke_for_do_command: None,
                 do_command_handled: None,
@@ -688,7 +688,7 @@ impl MacWindow {
                 });
             }
 
-            if titlebar.map_or(true, |titlebar| titlebar.appears_transparent) {
+            if titlebar.is_none_or(|titlebar| titlebar.appears_transparent) {
                 native_window.setTitlebarAppearsTransparent_(YES);
                 native_window.setTitleVisibility_(NSWindowTitleVisibility::NSWindowTitleHidden);
             }
@@ -1090,7 +1090,7 @@ impl PlatformWindow for MacWindow {
                         NSView::removeFromSuperview(blur_view);
                         this.blurred_view = None;
                     }
-                } else if this.blurred_view == None {
+                } else if this.blurred_view.is_none() {
                     let content_view = this.native_window.contentView();
                     let frame = NSView::bounds(content_view);
                     let mut blur_view: id = msg_send![BLURRED_VIEW_CLASS, alloc];
@@ -1478,18 +1478,18 @@ extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent:
                 return YES;
             }
 
-            if key_down_event.is_held {
-                if let Some(key_char) = key_down_event.keystroke.key_char.as_ref() {
-                    let handled = with_input_handler(&this, |input_handler| {
-                        if !input_handler.apple_press_and_hold_enabled() {
-                            input_handler.replace_text_in_range(None, &key_char);
-                            return YES;
-                        }
-                        NO
-                    });
-                    if handled == Some(YES) {
+            if key_down_event.is_held
+                && let Some(key_char) = key_down_event.keystroke.key_char.as_ref()
+            {
+                let handled = with_input_handler(this, |input_handler| {
+                    if !input_handler.apple_press_and_hold_enabled() {
+                        input_handler.replace_text_in_range(None, key_char);
                         return YES;
                     }
+                    NO
+                });
+                if handled == Some(YES) {
+                    return YES;
                 }
             }
 
@@ -1624,10 +1624,10 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
                     modifiers: prev_modifiers,
                     capslock: prev_capslock,
                 })) = &lock.previous_modifiers_changed_event
+                    && prev_modifiers == modifiers
+                    && prev_capslock == capslock
                 {
-                    if prev_modifiers == modifiers && prev_capslock == capslock {
-                        return;
-                    }
+                    return;
                 }
 
                 lock.previous_modifiers_changed_event = Some(event.clone());
@@ -1949,7 +1949,7 @@ extern "C" fn insert_text(this: &Object, _: Sel, text: id, replacement_range: NS
         let text = text.to_str();
         let replacement_range = replacement_range.to_range();
         with_input_handler(this, |input_handler| {
-            input_handler.replace_text_in_range(replacement_range, &text)
+            input_handler.replace_text_in_range(replacement_range, text)
         });
     }
 }
@@ -1973,7 +1973,7 @@ extern "C" fn set_marked_text(
         let replacement_range = replacement_range.to_range();
         let text = text.to_str();
         with_input_handler(this, |input_handler| {
-            input_handler.replace_and_mark_text_in_range(replacement_range, &text, selected_range)
+            input_handler.replace_and_mark_text_in_range(replacement_range, text, selected_range)
         });
     }
 }
@@ -1995,10 +1995,10 @@ extern "C" fn attributed_substring_for_proposed_range(
         let mut adjusted: Option<Range<usize>> = None;
 
         let selected_text = input_handler.text_for_range(range.clone(), &mut adjusted)?;
-        if let Some(adjusted) = adjusted {
-            if adjusted != range {
-                unsafe { (actual_range as *mut NSRange).write(NSRange::from(adjusted)) };
-            }
+        if let Some(adjusted) = adjusted
+            && adjusted != range
+        {
+            unsafe { (actual_range as *mut NSRange).write(NSRange::from(adjusted)) };
         }
         unsafe {
             let string: id = msg_send![class!(NSAttributedString), alloc];
@@ -2063,8 +2063,8 @@ fn screen_point_to_gpui_point(this: &Object, position: NSPoint) -> Point<Pixels>
     let frame = get_frame(this);
     let window_x = position.x - frame.origin.x;
     let window_y = frame.size.height - (position.y - frame.origin.y);
-    let position = point(px(window_x as f32), px(window_y as f32));
-    position
+
+    point(px(window_x as f32), px(window_y as f32))
 }
 
 extern "C" fn dragging_entered(this: &Object, _: Sel, dragging_info: id) -> NSDragOperation {
@@ -2073,11 +2073,10 @@ extern "C" fn dragging_entered(this: &Object, _: Sel, dragging_info: id) -> NSDr
     let paths = external_paths_from_event(dragging_info);
     if let Some(event) =
         paths.map(|paths| PlatformInput::FileDrop(FileDropEvent::Entered { position, paths }))
+        && send_new_event(&window_state, event)
     {
-        if send_new_event(&window_state, event) {
-            window_state.lock().external_files_dragged = true;
-            return NSDragOperationCopy;
-        }
+        window_state.lock().external_files_dragged = true;
+        return NSDragOperationCopy;
     }
     NSDragOperationNone
 }

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

@@ -228,7 +228,7 @@ fn run_capture(
         display,
         size,
     }));
-    if let Err(_) = stream_send_result {
+    if stream_send_result.is_err() {
         return;
     }
     while !cancel_stream.load(std::sync::atomic::Ordering::SeqCst) {

crates/gpui/src/platform/test/dispatcher.rs 🔗

@@ -78,11 +78,11 @@ impl TestDispatcher {
             let state = self.state.lock();
             let next_due_time = state.delayed.first().map(|(time, _)| *time);
             drop(state);
-            if let Some(due_time) = next_due_time {
-                if due_time <= new_now {
-                    self.state.lock().time = due_time;
-                    continue;
-                }
+            if let Some(due_time) = next_due_time
+                && due_time <= new_now
+            {
+                self.state.lock().time = due_time;
+                continue;
             }
             break;
         }
@@ -270,9 +270,7 @@ impl PlatformDispatcher for TestDispatcher {
     fn dispatch(&self, runnable: Runnable, label: Option<TaskLabel>) {
         {
             let mut state = self.state.lock();
-            if label.map_or(false, |label| {
-                state.deprioritized_task_labels.contains(&label)
-            }) {
+            if label.is_some_and(|label| state.deprioritized_task_labels.contains(&label)) {
                 state.deprioritized_background.push(runnable);
             } else {
                 state.background.push(runnable);

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

@@ -187,24 +187,24 @@ impl TestPlatform {
             .push_back(TestPrompt {
                 msg: msg.to_string(),
                 detail: detail.map(|s| s.to_string()),
-                answers: answers.clone(),
+                answers,
                 tx,
             });
         rx
     }
 
     pub(crate) fn set_active_window(&self, window: Option<TestWindow>) {
-        let executor = self.foreground_executor().clone();
+        let executor = self.foreground_executor();
         let previous_window = self.active_window.borrow_mut().take();
         self.active_window.borrow_mut().clone_from(&window);
 
         executor
             .spawn(async move {
                 if let Some(previous_window) = previous_window {
-                    if let Some(window) = window.as_ref() {
-                        if Rc::ptr_eq(&previous_window.0, &window.0) {
-                            return;
-                        }
+                    if let Some(window) = window.as_ref()
+                        && Rc::ptr_eq(&previous_window.0, &window.0)
+                    {
+                        return;
                     }
                     previous_window.simulate_active_status_change(false);
                 }
@@ -336,6 +336,7 @@ impl Platform for TestPlatform {
     fn prompt_for_new_path(
         &self,
         directory: &std::path::Path,
+        _suggested_name: Option<&str>,
     ) -> oneshot::Receiver<Result<Option<std::path::PathBuf>>> {
         let (tx, rx) = oneshot::channel();
         self.background_executor()

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

@@ -10,6 +10,7 @@ mod keyboard;
 mod platform;
 mod system_settings;
 mod util;
+mod vsync;
 mod window;
 mod wrapper;
 
@@ -25,6 +26,7 @@ pub(crate) use keyboard::*;
 pub(crate) use platform::*;
 pub(crate) use system_settings::*;
 pub(crate) use util::*;
+pub(crate) use vsync::*;
 pub(crate) use window::*;
 pub(crate) use wrapper::*;
 

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

@@ -498,8 +498,9 @@ impl DirectWriteState {
                 )
                 .unwrap()
             } else {
+                let family = self.system_ui_font_name.clone();
                 self.find_font_id(
-                    target_font.family.as_ref(),
+                    font_name_with_fallbacks(target_font.family.as_ref(), family.as_ref()),
                     target_font.weight,
                     target_font.style,
                     &target_font.features,
@@ -512,7 +513,6 @@ impl DirectWriteState {
                     }
                     #[cfg(not(any(test, feature = "test-support")))]
                     {
-                        let family = self.system_ui_font_name.clone();
                         log::error!("{} not found, use {} instead.", target_font.family, family);
                         self.get_font_id_from_font_collection(
                             family.as_ref(),
@@ -850,7 +850,7 @@ impl DirectWriteState {
         }
 
         let bitmap_data = if params.is_emoji {
-            if let Ok(color) = self.rasterize_color(&params, glyph_bounds) {
+            if let Ok(color) = self.rasterize_color(params, glyph_bounds) {
                 color
             } else {
                 let monochrome = self.rasterize_monochrome(params, glyph_bounds)?;
@@ -1784,7 +1784,7 @@ fn apply_font_features(
         }
 
         unsafe {
-            direct_write_features.AddFontFeature(make_direct_write_feature(&tag, *value))?;
+            direct_write_features.AddFontFeature(make_direct_write_feature(tag, *value))?;
         }
     }
     unsafe {

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

@@ -207,7 +207,7 @@ impl DirectXRenderer {
 
     fn present(&mut self) -> Result<()> {
         unsafe {
-            let result = self.resources.swap_chain.Present(1, DXGI_PRESENT(0));
+            let result = self.resources.swap_chain.Present(0, DXGI_PRESENT(0));
             // Presenting the swap chain can fail if the DirectX device was removed or reset.
             if result == DXGI_ERROR_DEVICE_REMOVED || result == DXGI_ERROR_DEVICE_RESET {
                 let reason = self.devices.device.GetDeviceRemovedReason();
@@ -758,7 +758,7 @@ impl DirectXRenderPipelines {
 
 impl DirectComposition {
     pub fn new(dxgi_device: &IDXGIDevice, hwnd: HWND) -> Result<Self> {
-        let comp_device = get_comp_device(&dxgi_device)?;
+        let comp_device = get_comp_device(dxgi_device)?;
         let comp_target = unsafe { comp_device.CreateTargetForHwnd(hwnd, true) }?;
         let comp_visual = unsafe { comp_device.CreateVisual() }?;
 
@@ -1144,7 +1144,7 @@ fn create_resources(
     [D3D11_VIEWPORT; 1],
 )> {
     let (render_target, render_target_view) =
-        create_render_target_and_its_view(&swap_chain, &devices.device)?;
+        create_render_target_and_its_view(swap_chain, &devices.device)?;
     let (path_intermediate_texture, path_intermediate_srv) =
         create_path_intermediate_texture(&devices.device, width, height)?;
     let (path_intermediate_msaa_texture, path_intermediate_msaa_view) =
@@ -1624,11 +1624,10 @@ mod nvidia {
         os::raw::{c_char, c_int, c_uint},
     };
 
-    use anyhow::{Context, Result};
-    use windows::{
-        Win32::System::LibraryLoader::{GetProcAddress, LoadLibraryA},
-        core::s,
-    };
+    use anyhow::Result;
+    use windows::{Win32::System::LibraryLoader::GetProcAddress, core::s};
+
+    use crate::with_dll_library;
 
     // https://github.com/NVIDIA/nvapi/blob/7cb76fce2f52de818b3da497af646af1ec16ce27/nvapi_lite_common.h#L180
     const NVAPI_SHORT_STRING_MAX: usize = 64;
@@ -1645,13 +1644,12 @@ mod nvidia {
     ) -> c_int;
 
     pub(super) fn get_driver_version() -> Result<String> {
-        unsafe {
-            // Try to load the NVIDIA driver DLL
-            #[cfg(target_pointer_width = "64")]
-            let nvidia_dll = LoadLibraryA(s!("nvapi64.dll")).context("Can't load nvapi64.dll")?;
-            #[cfg(target_pointer_width = "32")]
-            let nvidia_dll = LoadLibraryA(s!("nvapi.dll")).context("Can't load nvapi.dll")?;
+        #[cfg(target_pointer_width = "64")]
+        let nvidia_dll_name = s!("nvapi64.dll");
+        #[cfg(target_pointer_width = "32")]
+        let nvidia_dll_name = s!("nvapi.dll");
 
+        with_dll_library(nvidia_dll_name, |nvidia_dll| unsafe {
             let nvapi_query_addr = GetProcAddress(nvidia_dll, s!("nvapi_QueryInterface"))
                 .ok_or_else(|| anyhow::anyhow!("Failed to get nvapi_QueryInterface address"))?;
             let nvapi_query: extern "C" fn(u32) -> *mut () = std::mem::transmute(nvapi_query_addr);
@@ -1686,18 +1684,17 @@ mod nvidia {
                 minor,
                 branch_string.to_string_lossy()
             ))
-        }
+        })
     }
 }
 
 mod amd {
     use std::os::raw::{c_char, c_int, c_void};
 
-    use anyhow::{Context, Result};
-    use windows::{
-        Win32::System::LibraryLoader::{GetProcAddress, LoadLibraryA},
-        core::s,
-    };
+    use anyhow::Result;
+    use windows::{Win32::System::LibraryLoader::GetProcAddress, core::s};
+
+    use crate::with_dll_library;
 
     // https://github.com/GPUOpen-LibrariesAndSDKs/AGS_SDK/blob/5d8812d703d0335741b6f7ffc37838eeb8b967f7/ags_lib/inc/amd_ags.h#L145
     const AGS_CURRENT_VERSION: i32 = (6 << 22) | (3 << 12);
@@ -1731,14 +1728,12 @@ mod amd {
     type agsDeInitialize_t = unsafe extern "C" fn(context: *mut AGSContext) -> c_int;
 
     pub(super) fn get_driver_version() -> Result<String> {
-        unsafe {
-            #[cfg(target_pointer_width = "64")]
-            let amd_dll =
-                LoadLibraryA(s!("amd_ags_x64.dll")).context("Failed to load AMD AGS library")?;
-            #[cfg(target_pointer_width = "32")]
-            let amd_dll =
-                LoadLibraryA(s!("amd_ags_x86.dll")).context("Failed to load AMD AGS library")?;
+        #[cfg(target_pointer_width = "64")]
+        let amd_dll_name = s!("amd_ags_x64.dll");
+        #[cfg(target_pointer_width = "32")]
+        let amd_dll_name = s!("amd_ags_x86.dll");
 
+        with_dll_library(amd_dll_name, |amd_dll| unsafe {
             let ags_initialize_addr = GetProcAddress(amd_dll, s!("agsInitialize"))
                 .ok_or_else(|| anyhow::anyhow!("Failed to get agsInitialize address"))?;
             let ags_deinitialize_addr = GetProcAddress(amd_dll, s!("agsDeInitialize"))
@@ -1784,7 +1779,7 @@ mod amd {
 
             ags_deinitialize(context);
             Ok(format!("{} ({})", software_version, driver_version))
-        }
+        })
     }
 }
 

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

@@ -100,6 +100,7 @@ impl WindowsWindowInner {
             WM_SETCURSOR => self.handle_set_cursor(handle, lparam),
             WM_SETTINGCHANGE => self.handle_system_settings_changed(handle, wparam, lparam),
             WM_INPUTLANGCHANGE => self.handle_input_language_changed(lparam),
+            WM_SHOWWINDOW => self.handle_window_visibility_changed(handle, wparam),
             WM_GPUI_CURSOR_STYLE_CHANGED => self.handle_cursor_changed(lparam),
             WM_GPUI_FORCE_UPDATE_WINDOW => self.draw_window(handle, true),
             _ => None,
@@ -700,29 +701,28 @@ impl WindowsWindowInner {
         // Fix auto hide taskbar not showing. This solution is based on the approach
         // used by Chrome. However, it may result in one row of pixels being obscured
         // in our client area. But as Chrome says, "there seems to be no better solution."
-        if is_maximized {
-            if let Some(ref taskbar_position) = self
+        if is_maximized
+            && let Some(ref taskbar_position) = self
                 .state
                 .borrow()
                 .system_settings
                 .auto_hide_taskbar_position
-            {
-                // Fot the auto-hide taskbar, adjust in by 1 pixel on taskbar edge,
-                // so the window isn't treated as a "fullscreen app", which would cause
-                // the taskbar to disappear.
-                match taskbar_position {
-                    AutoHideTaskbarPosition::Left => {
-                        requested_client_rect[0].left += AUTO_HIDE_TASKBAR_THICKNESS_PX
-                    }
-                    AutoHideTaskbarPosition::Top => {
-                        requested_client_rect[0].top += AUTO_HIDE_TASKBAR_THICKNESS_PX
-                    }
-                    AutoHideTaskbarPosition::Right => {
-                        requested_client_rect[0].right -= AUTO_HIDE_TASKBAR_THICKNESS_PX
-                    }
-                    AutoHideTaskbarPosition::Bottom => {
-                        requested_client_rect[0].bottom -= AUTO_HIDE_TASKBAR_THICKNESS_PX
-                    }
+        {
+            // Fot the auto-hide taskbar, adjust in by 1 pixel on taskbar edge,
+            // so the window isn't treated as a "fullscreen app", which would cause
+            // the taskbar to disappear.
+            match taskbar_position {
+                AutoHideTaskbarPosition::Left => {
+                    requested_client_rect[0].left += AUTO_HIDE_TASKBAR_THICKNESS_PX
+                }
+                AutoHideTaskbarPosition::Top => {
+                    requested_client_rect[0].top += AUTO_HIDE_TASKBAR_THICKNESS_PX
+                }
+                AutoHideTaskbarPosition::Right => {
+                    requested_client_rect[0].right -= AUTO_HIDE_TASKBAR_THICKNESS_PX
+                }
+                AutoHideTaskbarPosition::Bottom => {
+                    requested_client_rect[0].bottom -= AUTO_HIDE_TASKBAR_THICKNESS_PX
                 }
             }
         }
@@ -956,7 +956,7 @@ impl WindowsWindowInner {
                 click_count,
                 first_mouse: false,
             });
-            let result = func(input.clone());
+            let result = func(input);
             let handled = !result.propagate || result.default_prevented;
             self.state.borrow_mut().callbacks.input = Some(func);
 
@@ -1124,27 +1124,22 @@ impl WindowsWindowInner {
         // lParam is a pointer to a string that indicates the area containing the system parameter
         // that was changed.
         let parameter = PCWSTR::from_raw(lparam.0 as _);
-        if unsafe { !parameter.is_null() && !parameter.is_empty() } {
-            if let Some(parameter_string) = unsafe { parameter.to_string() }.log_err() {
-                log::info!("System settings changed: {}", parameter_string);
-                match parameter_string.as_str() {
-                    "ImmersiveColorSet" => {
-                        let new_appearance = system_appearance()
-                            .context(
-                                "unable to get system appearance when handling ImmersiveColorSet",
-                            )
-                            .log_err()?;
-                        let mut lock = self.state.borrow_mut();
-                        if new_appearance != lock.appearance {
-                            lock.appearance = new_appearance;
-                            let mut callback = lock.callbacks.appearance_changed.take()?;
-                            drop(lock);
-                            callback();
-                            self.state.borrow_mut().callbacks.appearance_changed = Some(callback);
-                            configure_dwm_dark_mode(handle, new_appearance);
-                        }
-                    }
-                    _ => {}
+        if unsafe { !parameter.is_null() && !parameter.is_empty() }
+            && let Some(parameter_string) = unsafe { parameter.to_string() }.log_err()
+        {
+            log::info!("System settings changed: {}", parameter_string);
+            if parameter_string.as_str() == "ImmersiveColorSet" {
+                let new_appearance = system_appearance()
+                    .context("unable to get system appearance when handling ImmersiveColorSet")
+                    .log_err()?;
+                let mut lock = self.state.borrow_mut();
+                if new_appearance != lock.appearance {
+                    lock.appearance = new_appearance;
+                    let mut callback = lock.callbacks.appearance_changed.take()?;
+                    drop(lock);
+                    callback();
+                    self.state.borrow_mut().callbacks.appearance_changed = Some(callback);
+                    configure_dwm_dark_mode(handle, new_appearance);
                 }
             }
         }
@@ -1160,6 +1155,13 @@ impl WindowsWindowInner {
         Some(0)
     }
 
+    fn handle_window_visibility_changed(&self, handle: HWND, wparam: WPARAM) -> Option<isize> {
+        if wparam.0 == 1 {
+            self.draw_window(handle, false);
+        }
+        None
+    }
+
     fn handle_device_change_msg(&self, handle: HWND, wparam: WPARAM) -> Option<isize> {
         if wparam.0 == DBT_DEVNODES_CHANGED as usize {
             // The reason for sending this message is to actually trigger a redraw of the window.
@@ -1464,7 +1466,7 @@ pub(crate) fn current_modifiers() -> Modifiers {
 #[inline]
 pub(crate) fn current_capslock() -> Capslock {
     let on = unsafe { GetKeyState(VK_CAPITAL.0 as i32) & 1 } > 0;
-    Capslock { on: on }
+    Capslock { on }
 }
 
 fn get_client_area_insets(

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

@@ -1,5 +1,6 @@
 use std::{
     cell::RefCell,
+    ffi::OsStr,
     mem::ManuallyDrop,
     path::{Path, PathBuf},
     rc::Rc,
@@ -32,7 +33,7 @@ use crate::*;
 
 pub(crate) struct WindowsPlatform {
     state: RefCell<WindowsPlatformState>,
-    raw_window_handles: RwLock<SmallVec<[HWND; 4]>>,
+    raw_window_handles: Arc<RwLock<SmallVec<[SafeHwnd; 4]>>>,
     // The below members will never change throughout the entire lifecycle of the app.
     icon: HICON,
     main_receiver: flume::Receiver<Runnable>,
@@ -114,7 +115,7 @@ impl WindowsPlatform {
         };
         let icon = load_icon().unwrap_or_default();
         let state = RefCell::new(WindowsPlatformState::new());
-        let raw_window_handles = RwLock::new(SmallVec::new());
+        let raw_window_handles = Arc::new(RwLock::new(SmallVec::new()));
         let windows_version = WindowsVersion::new().context("Error retrieve windows version")?;
 
         Ok(Self {
@@ -134,22 +135,12 @@ impl WindowsPlatform {
         })
     }
 
-    fn redraw_all(&self) {
-        for handle in self.raw_window_handles.read().iter() {
-            unsafe {
-                RedrawWindow(Some(*handle), None, None, RDW_INVALIDATE | RDW_UPDATENOW)
-                    .ok()
-                    .log_err();
-            }
-        }
-    }
-
     pub fn window_from_hwnd(&self, hwnd: HWND) -> Option<Rc<WindowsWindowInner>> {
         self.raw_window_handles
             .read()
             .iter()
-            .find(|entry| *entry == &hwnd)
-            .and_then(|hwnd| window_from_hwnd(*hwnd))
+            .find(|entry| entry.as_raw() == hwnd)
+            .and_then(|hwnd| window_from_hwnd(hwnd.as_raw()))
     }
 
     #[inline]
@@ -158,7 +149,7 @@ impl WindowsPlatform {
             .read()
             .iter()
             .for_each(|handle| unsafe {
-                PostMessageW(Some(*handle), message, wparam, lparam).log_err();
+                PostMessageW(Some(handle.as_raw()), message, wparam, lparam).log_err();
             });
     }
 
@@ -166,7 +157,7 @@ impl WindowsPlatform {
         let mut lock = self.raw_window_handles.write();
         let index = lock
             .iter()
-            .position(|handle| *handle == target_window)
+            .position(|handle| handle.as_raw() == target_window)
             .unwrap();
         lock.remove(index);
 
@@ -226,19 +217,19 @@ impl WindowsPlatform {
         }
     }
 
-    // Returns true if the app should quit.
-    fn handle_events(&self) -> bool {
+    // Returns if the app should quit.
+    fn handle_events(&self) {
         let mut msg = MSG::default();
         unsafe {
-            while PeekMessageW(&mut msg, None, 0, 0, PM_REMOVE).as_bool() {
+            while GetMessageW(&mut msg, None, 0, 0).as_bool() {
                 match msg.message {
-                    WM_QUIT => return true,
+                    WM_QUIT => return,
                     WM_INPUTLANGCHANGE
                     | WM_GPUI_CLOSE_ONE_WINDOW
                     | WM_GPUI_TASK_DISPATCHED_ON_MAIN_THREAD
                     | WM_GPUI_DOCK_MENU_ACTION => {
-                        if self.handle_gpui_evnets(msg.message, msg.wParam, msg.lParam, &msg) {
-                            return true;
+                        if self.handle_gpui_events(msg.message, msg.wParam, msg.lParam, &msg) {
+                            return;
                         }
                     }
                     _ => {
@@ -247,11 +238,10 @@ impl WindowsPlatform {
                 }
             }
         }
-        false
     }
 
     // Returns true if the app should quit.
-    fn handle_gpui_evnets(
+    fn handle_gpui_events(
         &self,
         message: u32,
         wparam: WPARAM,
@@ -315,8 +305,28 @@ impl WindowsPlatform {
         self.raw_window_handles
             .read()
             .iter()
-            .find(|&&hwnd| hwnd == active_window_hwnd)
-            .copied()
+            .find(|hwnd| hwnd.as_raw() == active_window_hwnd)
+            .map(|hwnd| hwnd.as_raw())
+    }
+
+    fn begin_vsync_thread(&self) {
+        let all_windows = Arc::downgrade(&self.raw_window_handles);
+        std::thread::spawn(move || {
+            let vsync_provider = VSyncProvider::new();
+            loop {
+                vsync_provider.wait_for_vsync();
+                let Some(all_windows) = all_windows.upgrade() else {
+                    break;
+                };
+                for hwnd in all_windows.read().iter() {
+                    unsafe {
+                        RedrawWindow(Some(hwnd.as_raw()), None, None, RDW_INVALIDATE)
+                            .ok()
+                            .log_err();
+                    }
+                }
+            }
+        });
     }
 }
 
@@ -347,12 +357,8 @@ impl Platform for WindowsPlatform {
 
     fn run(&self, on_finish_launching: Box<dyn 'static + FnOnce()>) {
         on_finish_launching();
-        loop {
-            if self.handle_events() {
-                break;
-            }
-            self.redraw_all();
-        }
+        self.begin_vsync_thread();
+        self.handle_events();
 
         if let Some(ref mut callback) = self.state.borrow_mut().callbacks.quit {
             callback();
@@ -365,9 +371,9 @@ impl Platform for WindowsPlatform {
             .detach();
     }
 
-    fn restart(&self, _: Option<PathBuf>) {
+    fn restart(&self, binary_path: Option<PathBuf>) {
         let pid = std::process::id();
-        let Some(app_path) = self.app_path().log_err() else {
+        let Some(app_path) = binary_path.or(self.app_path().log_err()) else {
             return;
         };
         let script = format!(
@@ -445,7 +451,7 @@ impl Platform for WindowsPlatform {
     ) -> Result<Box<dyn PlatformWindow>> {
         let window = WindowsWindow::new(handle, options, self.generate_creation_info())?;
         let handle = window.get_raw_handle();
-        self.raw_window_handles.write().push(handle);
+        self.raw_window_handles.write().push(handle.into());
 
         Ok(Box::new(window))
     }
@@ -455,13 +461,15 @@ impl Platform for WindowsPlatform {
     }
 
     fn open_url(&self, url: &str) {
+        if url.is_empty() {
+            return;
+        }
         let url_string = url.to_string();
         self.background_executor()
             .spawn(async move {
-                if url_string.is_empty() {
-                    return;
-                }
-                open_target(url_string.as_str());
+                open_target(&url_string)
+                    .with_context(|| format!("Opening url: {}", url_string))
+                    .log_err();
             })
             .detach();
     }
@@ -485,13 +493,18 @@ impl Platform for WindowsPlatform {
         rx
     }
 
-    fn prompt_for_new_path(&self, directory: &Path) -> Receiver<Result<Option<PathBuf>>> {
+    fn prompt_for_new_path(
+        &self,
+        directory: &Path,
+        suggested_name: Option<&str>,
+    ) -> Receiver<Result<Option<PathBuf>>> {
         let directory = directory.to_owned();
+        let suggested_name = suggested_name.map(|s| s.to_owned());
         let (tx, rx) = oneshot::channel();
         let window = self.find_current_active_window();
         self.foreground_executor()
             .spawn(async move {
-                let _ = tx.send(file_save_dialog(directory, window));
+                let _ = tx.send(file_save_dialog(directory, suggested_name, window));
             })
             .detach();
 
@@ -504,37 +517,29 @@ impl Platform for WindowsPlatform {
     }
 
     fn reveal_path(&self, path: &Path) {
-        let Ok(file_full_path) = path.canonicalize() else {
-            log::error!("unable to parse file path");
+        if path.as_os_str().is_empty() {
             return;
-        };
+        }
+        let path = path.to_path_buf();
         self.background_executor()
             .spawn(async move {
-                let Some(path) = file_full_path.to_str() else {
-                    return;
-                };
-                if path.is_empty() {
-                    return;
-                }
-                open_target_in_explorer(path);
+                open_target_in_explorer(&path)
+                    .with_context(|| format!("Revealing path {} in explorer", path.display()))
+                    .log_err();
             })
             .detach();
     }
 
     fn open_with_system(&self, path: &Path) {
-        let Ok(full_path) = path.canonicalize() else {
-            log::error!("unable to parse file full path: {}", path.display());
+        if path.as_os_str().is_empty() {
             return;
-        };
+        }
+        let path = path.to_path_buf();
         self.background_executor()
             .spawn(async move {
-                let Some(full_path_str) = full_path.to_str() else {
-                    return;
-                };
-                if full_path_str.is_empty() {
-                    return;
-                };
-                open_target(full_path_str);
+                open_target(&path)
+                    .with_context(|| format!("Opening {} with system", path.display()))
+                    .log_err();
             })
             .detach();
     }
@@ -725,39 +730,67 @@ pub(crate) struct WindowCreationInfo {
     pub(crate) disable_direct_composition: bool,
 }
 
-fn open_target(target: &str) {
-    unsafe {
-        let ret = ShellExecuteW(
+fn open_target(target: impl AsRef<OsStr>) -> Result<()> {
+    let target = target.as_ref();
+    let ret = unsafe {
+        ShellExecuteW(
             None,
             windows::core::w!("open"),
             &HSTRING::from(target),
             None,
             None,
             SW_SHOWDEFAULT,
-        );
-        if ret.0 as isize <= 32 {
-            log::error!("Unable to open target: {}", std::io::Error::last_os_error());
-        }
+        )
+    };
+    if ret.0 as isize <= 32 {
+        Err(anyhow::anyhow!(
+            "Unable to open target: {}",
+            std::io::Error::last_os_error()
+        ))
+    } else {
+        Ok(())
     }
 }
 
-fn open_target_in_explorer(target: &str) {
+fn open_target_in_explorer(target: &Path) -> Result<()> {
+    let dir = target.parent().context("No parent folder found")?;
+    let desktop = unsafe { SHGetDesktopFolder()? };
+
+    let mut dir_item = std::ptr::null_mut();
     unsafe {
-        let ret = ShellExecuteW(
+        desktop.ParseDisplayName(
+            HWND::default(),
             None,
-            windows::core::w!("open"),
-            windows::core::w!("explorer.exe"),
-            &HSTRING::from(format!("/select,{}", target).as_str()),
+            &HSTRING::from(dir),
             None,
-            SW_SHOWDEFAULT,
-        );
-        if ret.0 as isize <= 32 {
-            log::error!(
-                "Unable to open target in explorer: {}",
-                std::io::Error::last_os_error()
-            );
-        }
+            &mut dir_item,
+            std::ptr::null_mut(),
+        )?;
     }
+
+    let mut file_item = std::ptr::null_mut();
+    unsafe {
+        desktop.ParseDisplayName(
+            HWND::default(),
+            None,
+            &HSTRING::from(target),
+            None,
+            &mut file_item,
+            std::ptr::null_mut(),
+        )?;
+    }
+
+    let highlight = [file_item as *const _];
+    unsafe { SHOpenFolderAndSelectItems(dir_item as _, Some(&highlight), 0) }.or_else(|err| {
+        if err.code().0 == ERROR_FILE_NOT_FOUND.0 as i32 {
+            // On some systems, the above call mysteriously fails with "file not
+            // found" even though the file is there.  In these cases, ShellExecute()
+            // seems to work as a fallback (although it won't select the file).
+            open_target(dir).context("Opening target parent folder")
+        } else {
+            Err(anyhow::anyhow!("Can not open target path: {}", err))
+        }
+    })
 }
 
 fn file_open_dialog(
@@ -777,6 +810,12 @@ fn file_open_dialog(
 
     unsafe {
         folder_dialog.SetOptions(dialog_options)?;
+
+        if let Some(prompt) = options.prompt {
+            let prompt: &str = &prompt;
+            folder_dialog.SetOkButtonLabel(&HSTRING::from(prompt))?;
+        }
+
         if folder_dialog.Show(window).is_err() {
             // User cancelled
             return Ok(None);
@@ -799,17 +838,26 @@ fn file_open_dialog(
     Ok(Some(paths))
 }
 
-fn file_save_dialog(directory: PathBuf, window: Option<HWND>) -> Result<Option<PathBuf>> {
+fn file_save_dialog(
+    directory: PathBuf,
+    suggested_name: Option<String>,
+    window: Option<HWND>,
+) -> Result<Option<PathBuf>> {
     let dialog: IFileSaveDialog = unsafe { CoCreateInstance(&FileSaveDialog, None, CLSCTX_ALL)? };
-    if !directory.to_string_lossy().is_empty() {
-        if let Some(full_path) = directory.canonicalize().log_err() {
-            let full_path = SanitizedPath::from(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() };
-        }
+    if !directory.to_string_lossy().is_empty()
+        && let Some(full_path) = directory.canonicalize().log_err()
+    {
+        let full_path = SanitizedPath::from(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() };
     }
+
+    if let Some(suggested_name) = suggested_name {
+        unsafe { dialog.SetFileName(&HSTRING::from(suggested_name)).log_err() };
+    }
+
     unsafe {
         dialog.SetFileTypes(&[Common::COMDLG_FILTERSPEC {
             pszName: windows::core::w!("All files"),

crates/gpui/src/platform/windows/shaders.hlsl 🔗

@@ -914,7 +914,7 @@ float4 path_rasterization_fragment(PathFragmentInput input): SV_Target {
     float2 dx = ddx(input.st_position);
     float2 dy = ddy(input.st_position);
     PathRasterizationSprite sprite = path_rasterization_sprites[input.vertex_id];
-    
+
     Background background = sprite.color;
     Bounds bounds = sprite.bounds;
 
@@ -1021,13 +1021,18 @@ UnderlineVertexOutput underline_vertex(uint vertex_id: SV_VertexID, uint underli
 }
 
 float4 underline_fragment(UnderlineFragmentInput input): SV_Target {
+    const float WAVE_FREQUENCY = 2.0;
+    const float WAVE_HEIGHT_RATIO = 0.8;
+
     Underline underline = underlines[input.underline_id];
     if (underline.wavy) {
         float half_thickness = underline.thickness * 0.5;
         float2 origin = underline.bounds.origin;
+
         float2 st = ((input.position.xy - origin) / underline.bounds.size.y) - float2(0., 0.5);
-        float frequency = (M_PI_F * (3. * underline.thickness)) / 8.;
-        float amplitude = 1. / (2. * underline.thickness);
+        float frequency = (M_PI_F * WAVE_FREQUENCY * underline.thickness) / underline.bounds.size.y;
+        float amplitude = (underline.thickness * WAVE_HEIGHT_RATIO) / underline.bounds.size.y;
+
         float sine = sin(st.x * frequency) * amplitude;
         float dSine = cos(st.x * frequency) * amplitude * frequency;
         float distance = (st.y - sine) / sqrt(1. + dSine * dSine);

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

@@ -1,14 +1,18 @@
 use std::sync::OnceLock;
 
 use ::util::ResultExt;
+use anyhow::Context;
 use windows::{
     UI::{
         Color,
         ViewManagement::{UIColorType, UISettings},
     },
     Wdk::System::SystemServices::RtlGetVersion,
-    Win32::{Foundation::*, Graphics::Dwm::*, UI::WindowsAndMessaging::*},
-    core::{BOOL, HSTRING},
+    Win32::{
+        Foundation::*, Graphics::Dwm::*, System::LibraryLoader::LoadLibraryA,
+        UI::WindowsAndMessaging::*,
+    },
+    core::{BOOL, HSTRING, PCSTR},
 };
 
 use crate::*;
@@ -197,3 +201,19 @@ pub(crate) fn show_error(title: &str, content: String) {
         )
     };
 }
+
+pub(crate) fn with_dll_library<R, F>(dll_name: PCSTR, f: F) -> Result<R>
+where
+    F: FnOnce(HMODULE) -> Result<R>,
+{
+    let library = unsafe {
+        LoadLibraryA(dll_name).with_context(|| format!("Loading dll: {}", dll_name.display()))?
+    };
+    let result = f(library);
+    unsafe {
+        FreeLibrary(library)
+            .with_context(|| format!("Freeing dll: {}", dll_name.display()))
+            .log_err();
+    }
+    result
+}

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

@@ -0,0 +1,174 @@
+use std::{
+    sync::LazyLock,
+    time::{Duration, Instant},
+};
+
+use anyhow::{Context, Result};
+use util::ResultExt;
+use windows::{
+    Win32::{
+        Foundation::{HANDLE, HWND},
+        Graphics::{
+            DirectComposition::{
+                COMPOSITION_FRAME_ID_COMPLETED, COMPOSITION_FRAME_ID_TYPE, COMPOSITION_FRAME_STATS,
+                COMPOSITION_TARGET_ID,
+            },
+            Dwm::{DWM_TIMING_INFO, DwmFlush, DwmGetCompositionTimingInfo},
+        },
+        System::{
+            LibraryLoader::{GetModuleHandleA, GetProcAddress},
+            Performance::QueryPerformanceFrequency,
+            Threading::INFINITE,
+        },
+    },
+    core::{HRESULT, s},
+};
+
+static QPC_TICKS_PER_SECOND: LazyLock<u64> = LazyLock::new(|| {
+    let mut frequency = 0;
+    // On systems that run Windows XP or later, the function will always succeed and
+    // will thus never return zero.
+    unsafe { QueryPerformanceFrequency(&mut frequency).unwrap() };
+    frequency as u64
+});
+
+const VSYNC_INTERVAL_THRESHOLD: Duration = Duration::from_millis(1);
+const DEFAULT_VSYNC_INTERVAL: Duration = Duration::from_micros(16_666); // ~60Hz
+
+// Here we are using dynamic loading of DirectComposition functions,
+// or the app will refuse to start on windows systems that do not support DirectComposition.
+type DCompositionGetFrameId =
+    unsafe extern "system" fn(frameidtype: COMPOSITION_FRAME_ID_TYPE, frameid: *mut u64) -> HRESULT;
+type DCompositionGetStatistics = unsafe extern "system" fn(
+    frameid: u64,
+    framestats: *mut COMPOSITION_FRAME_STATS,
+    targetidcount: u32,
+    targetids: *mut COMPOSITION_TARGET_ID,
+    actualtargetidcount: *mut u32,
+) -> HRESULT;
+type DCompositionWaitForCompositorClock =
+    unsafe extern "system" fn(count: u32, handles: *const HANDLE, timeoutinms: u32) -> u32;
+
+pub(crate) struct VSyncProvider {
+    interval: Duration,
+    f: Box<dyn Fn() -> bool>,
+}
+
+impl VSyncProvider {
+    pub(crate) fn new() -> Self {
+        if let Some((get_frame_id, get_statistics, wait_for_comp_clock)) =
+            initialize_direct_composition()
+                .context("Retrieving DirectComposition functions")
+                .log_with_level(log::Level::Warn)
+        {
+            let interval = get_dwm_interval_from_direct_composition(get_frame_id, get_statistics)
+                .context("Failed to get DWM interval from DirectComposition")
+                .log_err()
+                .unwrap_or(DEFAULT_VSYNC_INTERVAL);
+            log::info!(
+                "DirectComposition is supported for VSync, interval: {:?}",
+                interval
+            );
+            let f = Box::new(move || unsafe {
+                wait_for_comp_clock(0, std::ptr::null(), INFINITE) == 0
+            });
+            Self { interval, f }
+        } else {
+            let interval = get_dwm_interval()
+                .context("Failed to get DWM interval")
+                .log_err()
+                .unwrap_or(DEFAULT_VSYNC_INTERVAL);
+            log::info!(
+                "DirectComposition is not supported for VSync, falling back to DWM, interval: {:?}",
+                interval
+            );
+            let f = Box::new(|| unsafe { DwmFlush().is_ok() });
+            Self { interval, f }
+        }
+    }
+
+    pub(crate) fn wait_for_vsync(&self) {
+        let vsync_start = Instant::now();
+        let wait_succeeded = (self.f)();
+        let elapsed = vsync_start.elapsed();
+        // DwmFlush and DCompositionWaitForCompositorClock returns very early
+        // instead of waiting until vblank when the monitor goes to sleep or is
+        // unplugged (nothing to present due to desktop occlusion). We use 1ms as
+        // a threshhold for the duration of the wait functions and fallback to
+        // Sleep() if it returns before that. This could happen during normal
+        // operation for the first call after the vsync thread becomes non-idle,
+        // but it shouldn't happen often.
+        if !wait_succeeded || elapsed < VSYNC_INTERVAL_THRESHOLD {
+            log::trace!("VSyncProvider::wait_for_vsync() took less time than expected");
+            std::thread::sleep(self.interval);
+        }
+    }
+}
+
+fn initialize_direct_composition() -> Result<(
+    DCompositionGetFrameId,
+    DCompositionGetStatistics,
+    DCompositionWaitForCompositorClock,
+)> {
+    unsafe {
+        // Load DLL at runtime since older Windows versions don't have dcomp.
+        let hmodule = GetModuleHandleA(s!("dcomp.dll")).context("Loading dcomp.dll")?;
+        let get_frame_id_addr = GetProcAddress(hmodule, s!("DCompositionGetFrameId"))
+            .context("Function DCompositionGetFrameId not found")?;
+        let get_statistics_addr = GetProcAddress(hmodule, s!("DCompositionGetStatistics"))
+            .context("Function DCompositionGetStatistics not found")?;
+        let wait_for_compositor_clock_addr =
+            GetProcAddress(hmodule, s!("DCompositionWaitForCompositorClock"))
+                .context("Function DCompositionWaitForCompositorClock not found")?;
+        let get_frame_id: DCompositionGetFrameId = std::mem::transmute(get_frame_id_addr);
+        let get_statistics: DCompositionGetStatistics = std::mem::transmute(get_statistics_addr);
+        let wait_for_compositor_clock: DCompositionWaitForCompositorClock =
+            std::mem::transmute(wait_for_compositor_clock_addr);
+        Ok((get_frame_id, get_statistics, wait_for_compositor_clock))
+    }
+}
+
+fn get_dwm_interval_from_direct_composition(
+    get_frame_id: DCompositionGetFrameId,
+    get_statistics: DCompositionGetStatistics,
+) -> Result<Duration> {
+    let mut frame_id = 0;
+    unsafe { get_frame_id(COMPOSITION_FRAME_ID_COMPLETED, &mut frame_id) }.ok()?;
+    let mut stats = COMPOSITION_FRAME_STATS::default();
+    unsafe {
+        get_statistics(
+            frame_id,
+            &mut stats,
+            0,
+            std::ptr::null_mut(),
+            std::ptr::null_mut(),
+        )
+    }
+    .ok()?;
+    Ok(retrieve_duration(stats.framePeriod, *QPC_TICKS_PER_SECOND))
+}
+
+fn get_dwm_interval() -> Result<Duration> {
+    let mut timing_info = DWM_TIMING_INFO {
+        cbSize: std::mem::size_of::<DWM_TIMING_INFO>() as u32,
+        ..Default::default()
+    };
+    unsafe { DwmGetCompositionTimingInfo(HWND::default(), &mut timing_info) }?;
+    let interval = retrieve_duration(timing_info.qpcRefreshPeriod, *QPC_TICKS_PER_SECOND);
+    // Check for interval values that are impossibly low. A 29 microsecond
+    // interval was seen (from a qpcRefreshPeriod of 60).
+    if interval < VSYNC_INTERVAL_THRESHOLD {
+        Ok(retrieve_duration(
+            timing_info.rateRefresh.uiDenominator as u64,
+            timing_info.rateRefresh.uiNumerator as u64,
+        ))
+    } else {
+        Ok(interval)
+    }
+}
+
+#[inline]
+fn retrieve_duration(counts: u64, ticks_per_second: u64) -> Duration {
+    let ticks_per_microsecond = ticks_per_second / 1_000_000;
+    Duration::from_micros(counts / ticks_per_microsecond)
+}

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

@@ -592,10 +592,7 @@ impl PlatformWindow for WindowsWindow {
     ) -> Option<Receiver<usize>> {
         let (done_tx, done_rx) = oneshot::channel();
         let msg = msg.to_string();
-        let detail_string = match detail {
-            Some(info) => Some(info.to_string()),
-            None => None,
-        };
+        let detail_string = detail.map(|detail| detail.to_string());
         let handle = self.0.hwnd;
         let answers = answers.to_vec();
         self.0

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

@@ -1,23 +1,23 @@
 use std::ops::Deref;
 
-use windows::Win32::{Foundation::HANDLE, UI::WindowsAndMessaging::HCURSOR};
+use windows::Win32::{Foundation::HWND, UI::WindowsAndMessaging::HCURSOR};
 
 #[derive(Debug, Clone, Copy)]
-pub(crate) struct SafeHandle {
-    raw: HANDLE,
+pub(crate) struct SafeCursor {
+    raw: HCURSOR,
 }
 
-unsafe impl Send for SafeHandle {}
-unsafe impl Sync for SafeHandle {}
+unsafe impl Send for SafeCursor {}
+unsafe impl Sync for SafeCursor {}
 
-impl From<HANDLE> for SafeHandle {
-    fn from(value: HANDLE) -> Self {
-        SafeHandle { raw: value }
+impl From<HCURSOR> for SafeCursor {
+    fn from(value: HCURSOR) -> Self {
+        SafeCursor { raw: value }
     }
 }
 
-impl Deref for SafeHandle {
-    type Target = HANDLE;
+impl Deref for SafeCursor {
+    type Target = HCURSOR;
 
     fn deref(&self) -> &Self::Target {
         &self.raw
@@ -25,21 +25,27 @@ impl Deref for SafeHandle {
 }
 
 #[derive(Debug, Clone, Copy)]
-pub(crate) struct SafeCursor {
-    raw: HCURSOR,
+pub(crate) struct SafeHwnd {
+    raw: HWND,
 }
 
-unsafe impl Send for SafeCursor {}
-unsafe impl Sync for SafeCursor {}
+impl SafeHwnd {
+    pub(crate) fn as_raw(&self) -> HWND {
+        self.raw
+    }
+}
 
-impl From<HCURSOR> for SafeCursor {
-    fn from(value: HCURSOR) -> Self {
-        SafeCursor { raw: value }
+unsafe impl Send for SafeHwnd {}
+unsafe impl Sync for SafeHwnd {}
+
+impl From<HWND> for SafeHwnd {
+    fn from(value: HWND) -> Self {
+        SafeHwnd { raw: value }
     }
 }
 
-impl Deref for SafeCursor {
-    type Target = HCURSOR;
+impl Deref for SafeHwnd {
+    type Target = HWND;
 
     fn deref(&self) -> &Self::Target {
         &self.raw

crates/gpui/src/scene.rs 🔗

@@ -476,7 +476,7 @@ pub(crate) struct Underline {
     pub content_mask: ContentMask<ScaledPixels>,
     pub color: Hsla,
     pub thickness: ScaledPixels,
-    pub wavy: bool,
+    pub wavy: u32,
 }
 
 impl From<Underline> for Primitive {

crates/gpui/src/shared_string.rs 🔗

@@ -23,6 +23,11 @@ impl SharedString {
     pub fn new(str: impl Into<Arc<str>>) -> Self {
         SharedString(ArcCow::Owned(str.into()))
     }
+
+    /// Get a &str from the underlying string.
+    pub fn as_str(&self) -> &str {
+        &self.0
+    }
 }
 
 impl JsonSchema for SharedString {
@@ -103,7 +108,7 @@ impl From<SharedString> for Arc<str> {
     fn from(val: SharedString) -> Self {
         match val.0 {
             ArcCow::Borrowed(borrowed) => Arc::from(borrowed),
-            ArcCow::Owned(owned) => owned.clone(),
+            ArcCow::Owned(owned) => owned,
         }
     }
 }

crates/gpui/src/style.rs 🔗

@@ -7,7 +7,7 @@ use std::{
 use crate::{
     AbsoluteLength, App, Background, BackgroundTag, BorderStyle, Bounds, ContentMask, Corners,
     CornersRefinement, CursorStyle, DefiniteLength, DevicePixels, Edges, EdgesRefinement, Font,
-    FontFallbacks, FontFeatures, FontStyle, FontWeight, Hsla, Length, Pixels, Point,
+    FontFallbacks, FontFeatures, FontStyle, FontWeight, GridLocation, Hsla, Length, Pixels, Point,
     PointRefinement, Rgba, SharedString, Size, SizeRefinement, Styled, TextRun, Window, black, phi,
     point, quad, rems, size,
 };
@@ -260,6 +260,17 @@ pub struct Style {
     /// The opacity of this element
     pub opacity: Option<f32>,
 
+    /// The grid columns of this element
+    /// Equivalent to the Tailwind `grid-cols-<number>`
+    pub grid_cols: Option<u16>,
+
+    /// The row span of this element
+    /// Equivalent to the Tailwind `grid-rows-<number>`
+    pub grid_rows: Option<u16>,
+
+    /// The grid location of this element
+    pub grid_location: Option<GridLocation>,
+
     /// Whether to draw a red debugging outline around this element
     #[cfg(debug_assertions)]
     pub debug: bool,
@@ -275,6 +286,13 @@ impl Styled for StyleRefinement {
     }
 }
 
+impl StyleRefinement {
+    /// The grid location of this element
+    pub fn grid_location_mut(&mut self) -> &mut GridLocation {
+        self.grid_location.get_or_insert_default()
+    }
+}
+
 /// The value of the visibility property, similar to the CSS property `visibility`
 #[derive(Default, Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
 pub enum Visibility {
@@ -555,7 +573,7 @@ impl Style {
 
                 if self
                     .border_color
-                    .map_or(false, |color| !color.is_transparent())
+                    .is_some_and(|color| !color.is_transparent())
                 {
                     min.x += self.border_widths.left.to_pixels(rem_size);
                     max.x -= self.border_widths.right.to_pixels(rem_size);
@@ -615,7 +633,7 @@ impl Style {
         window.paint_shadows(bounds, corner_radii, &self.box_shadow);
 
         let background_color = self.background.as_ref().and_then(Fill::color);
-        if background_color.map_or(false, |color| !color.is_transparent()) {
+        if background_color.is_some_and(|color| !color.is_transparent()) {
             let mut border_color = match background_color {
                 Some(color) => match color.tag {
                     BackgroundTag::Solid => color.solid,
@@ -711,7 +729,7 @@ impl Style {
 
     fn is_border_visible(&self) -> bool {
         self.border_color
-            .map_or(false, |color| !color.is_transparent())
+            .is_some_and(|color| !color.is_transparent())
             && self.border_widths.any(|length| !length.is_zero())
     }
 }
@@ -757,6 +775,9 @@ impl Default for Style {
             text: TextStyleRefinement::default(),
             mouse_cursor: None,
             opacity: None,
+            grid_rows: None,
+            grid_cols: None,
+            grid_location: None,
 
             #[cfg(debug_assertions)]
             debug: false,

crates/gpui/src/styled.rs 🔗

@@ -1,8 +1,8 @@
 use crate::{
     self as gpui, AbsoluteLength, AlignContent, AlignItems, BorderStyle, CursorStyle,
-    DefiniteLength, Display, Fill, FlexDirection, FlexWrap, Font, FontStyle, FontWeight, Hsla,
-    JustifyContent, Length, SharedString, StrikethroughStyle, StyleRefinement, TextAlign,
-    TextOverflow, TextStyleRefinement, UnderlineStyle, WhiteSpace, px, relative, rems,
+    DefiniteLength, Display, Fill, FlexDirection, FlexWrap, Font, FontStyle, FontWeight,
+    GridPlacement, Hsla, JustifyContent, Length, SharedString, StrikethroughStyle, StyleRefinement,
+    TextAlign, TextOverflow, TextStyleRefinement, UnderlineStyle, WhiteSpace, px, relative, rems,
 };
 pub use gpui_macros::{
     border_style_methods, box_shadow_style_methods, cursor_style_methods, margin_style_methods,
@@ -46,6 +46,13 @@ pub trait Styled: Sized {
         self
     }
 
+    /// Sets the display type of the element to `grid`.
+    /// [Docs](https://tailwindcss.com/docs/display)
+    fn grid(mut self) -> Self {
+        self.style().display = Some(Display::Grid);
+        self
+    }
+
     /// Sets the whitespace of the element to `normal`.
     /// [Docs](https://tailwindcss.com/docs/whitespace#normal)
     fn whitespace_normal(mut self) -> Self {
@@ -640,6 +647,102 @@ pub trait Styled: Sized {
         self
     }
 
+    /// Sets the grid columns of this element.
+    fn grid_cols(mut self, cols: u16) -> Self {
+        self.style().grid_cols = Some(cols);
+        self
+    }
+
+    /// Sets the grid rows of this element.
+    fn grid_rows(mut self, rows: u16) -> Self {
+        self.style().grid_rows = Some(rows);
+        self
+    }
+
+    /// Sets the column start of this element.
+    fn col_start(mut self, start: i16) -> Self {
+        let grid_location = self.style().grid_location_mut();
+        grid_location.column.start = GridPlacement::Line(start);
+        self
+    }
+
+    /// Sets the column start of this element to auto.
+    fn col_start_auto(mut self) -> Self {
+        let grid_location = self.style().grid_location_mut();
+        grid_location.column.start = GridPlacement::Auto;
+        self
+    }
+
+    /// Sets the column end of this element.
+    fn col_end(mut self, end: i16) -> Self {
+        let grid_location = self.style().grid_location_mut();
+        grid_location.column.end = GridPlacement::Line(end);
+        self
+    }
+
+    /// Sets the column end of this element to auto.
+    fn col_end_auto(mut self) -> Self {
+        let grid_location = self.style().grid_location_mut();
+        grid_location.column.end = GridPlacement::Auto;
+        self
+    }
+
+    /// Sets the column span of this element.
+    fn col_span(mut self, span: u16) -> Self {
+        let grid_location = self.style().grid_location_mut();
+        grid_location.column = GridPlacement::Span(span)..GridPlacement::Span(span);
+        self
+    }
+
+    /// Sets the row span of this element.
+    fn col_span_full(mut self) -> Self {
+        let grid_location = self.style().grid_location_mut();
+        grid_location.column = GridPlacement::Line(1)..GridPlacement::Line(-1);
+        self
+    }
+
+    /// Sets the row start of this element.
+    fn row_start(mut self, start: i16) -> Self {
+        let grid_location = self.style().grid_location_mut();
+        grid_location.row.start = GridPlacement::Line(start);
+        self
+    }
+
+    /// Sets the row start of this element to "auto"
+    fn row_start_auto(mut self) -> Self {
+        let grid_location = self.style().grid_location_mut();
+        grid_location.row.start = GridPlacement::Auto;
+        self
+    }
+
+    /// Sets the row end of this element.
+    fn row_end(mut self, end: i16) -> Self {
+        let grid_location = self.style().grid_location_mut();
+        grid_location.row.end = GridPlacement::Line(end);
+        self
+    }
+
+    /// Sets the row end of this element to "auto"
+    fn row_end_auto(mut self) -> Self {
+        let grid_location = self.style().grid_location_mut();
+        grid_location.row.end = GridPlacement::Auto;
+        self
+    }
+
+    /// Sets the row span of this element.
+    fn row_span(mut self, span: u16) -> Self {
+        let grid_location = self.style().grid_location_mut();
+        grid_location.row = GridPlacement::Span(span)..GridPlacement::Span(span);
+        self
+    }
+
+    /// Sets the row span of this element.
+    fn row_span_full(mut self) -> Self {
+        let grid_location = self.style().grid_location_mut();
+        grid_location.row = GridPlacement::Line(1)..GridPlacement::Line(-1);
+        self
+    }
+
     /// Draws a debug border around this element.
     #[cfg(debug_assertions)]
     fn debug(mut self) -> Self {

crates/gpui/src/subscription.rs 🔗

@@ -201,3 +201,9 @@ impl Drop for Subscription {
         }
     }
 }
+
+impl std::fmt::Debug for Subscription {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("Subscription").finish()
+    }
+}

crates/gpui/src/tab_stop.rs 🔗

@@ -45,27 +45,18 @@ impl TabHandles {
             })
             .unwrap_or_default();
 
-        if let Some(next_handle) = self.handles.get(next_ix) {
-            Some(next_handle.clone())
-        } else {
-            None
-        }
+        self.handles.get(next_ix).cloned()
     }
 
     pub(crate) fn prev(&self, focused_id: Option<&FocusId>) -> Option<FocusHandle> {
         let ix = self.current_index(focused_id).unwrap_or_default();
-        let prev_ix;
-        if ix == 0 {
-            prev_ix = self.handles.len().saturating_sub(1);
+        let prev_ix = if ix == 0 {
+            self.handles.len().saturating_sub(1)
         } else {
-            prev_ix = ix.saturating_sub(1);
-        }
+            ix.saturating_sub(1)
+        };
 
-        if let Some(prev_handle) = self.handles.get(prev_ix) {
-            Some(prev_handle.clone())
-        } else {
-            None
-        }
+        self.handles.get(prev_ix).cloned()
     }
 }
 
@@ -90,7 +81,7 @@ mod tests {
         ];
 
         for handle in focus_handles.iter() {
-            tab.insert(&handle);
+            tab.insert(handle);
         }
         assert_eq!(
             tab.handles

crates/gpui/src/taffy.rs 🔗

@@ -3,7 +3,8 @@ use crate::{
 };
 use collections::{FxHashMap, FxHashSet};
 use smallvec::SmallVec;
-use std::fmt::Debug;
+use stacksafe::{StackSafe, stacksafe};
+use std::{fmt::Debug, ops::Range};
 use taffy::{
     TaffyTree, TraversePartialTree as _,
     geometry::{Point as TaffyPoint, Rect as TaffyRect, Size as TaffySize},
@@ -11,8 +12,15 @@ use taffy::{
     tree::NodeId,
 };
 
-type NodeMeasureFn = Box<
-    dyn FnMut(Size<Option<Pixels>>, Size<AvailableSpace>, &mut Window, &mut App) -> Size<Pixels>,
+type NodeMeasureFn = StackSafe<
+    Box<
+        dyn FnMut(
+            Size<Option<Pixels>>,
+            Size<AvailableSpace>,
+            &mut Window,
+            &mut App,
+        ) -> Size<Pixels>,
+    >,
 >;
 
 struct NodeContext {
@@ -50,23 +58,21 @@ impl TaffyLayoutEngine {
         children: &[LayoutId],
     ) -> LayoutId {
         let taffy_style = style.to_taffy(rem_size);
-        let layout_id = if children.is_empty() {
+
+        if children.is_empty() {
             self.taffy
                 .new_leaf(taffy_style)
                 .expect(EXPECT_MESSAGE)
                 .into()
         } else {
-            let parent_id = self
-                .taffy
+            self.taffy
                 // This is safe because LayoutId is repr(transparent) to taffy::tree::NodeId.
                 .new_with_children(taffy_style, unsafe {
                     std::mem::transmute::<&[LayoutId], &[taffy::NodeId]>(children)
                 })
                 .expect(EXPECT_MESSAGE)
-                .into();
-            parent_id
-        };
-        layout_id
+                .into()
+        }
     }
 
     pub fn request_measured_layout(
@@ -83,17 +89,15 @@ impl TaffyLayoutEngine {
     ) -> LayoutId {
         let taffy_style = style.to_taffy(rem_size);
 
-        let layout_id = self
-            .taffy
+        self.taffy
             .new_leaf_with_context(
                 taffy_style,
                 NodeContext {
-                    measure: Box::new(measure),
+                    measure: StackSafe::new(Box::new(measure)),
                 },
             )
             .expect(EXPECT_MESSAGE)
-            .into();
-        layout_id
+            .into()
     }
 
     // Used to understand performance
@@ -143,6 +147,7 @@ impl TaffyLayoutEngine {
         Ok(edges)
     }
 
+    #[stacksafe]
     pub fn compute_layout(
         &mut self,
         id: LayoutId,
@@ -159,7 +164,6 @@ impl TaffyLayoutEngine {
         // for (a, b) in self.get_edges(id)? {
         //     println!("N{} --> N{}", u64::from(a), u64::from(b));
         // }
-        // println!("");
         //
 
         if !self.computed_layouts.insert(id) {
@@ -251,6 +255,25 @@ trait ToTaffy<Output> {
 
 impl ToTaffy<taffy::style::Style> for Style {
     fn to_taffy(&self, rem_size: Pixels) -> taffy::style::Style {
+        use taffy::style_helpers::{fr, length, minmax, repeat};
+
+        fn to_grid_line(
+            placement: &Range<crate::GridPlacement>,
+        ) -> taffy::Line<taffy::GridPlacement> {
+            taffy::Line {
+                start: placement.start.into(),
+                end: placement.end.into(),
+            }
+        }
+
+        fn to_grid_repeat<T: taffy::style::CheapCloneStr>(
+            unit: &Option<u16>,
+        ) -> Vec<taffy::GridTemplateComponent<T>> {
+            // grid-template-columns: repeat(<number>, minmax(0, 1fr));
+            unit.map(|count| vec![repeat(count, vec![minmax(length(0.0), fr(1.0))])])
+                .unwrap_or_default()
+        }
+
         taffy::style::Style {
             display: self.display.into(),
             overflow: self.overflow.into(),
@@ -274,7 +297,19 @@ impl ToTaffy<taffy::style::Style> for Style {
             flex_basis: self.flex_basis.to_taffy(rem_size),
             flex_grow: self.flex_grow,
             flex_shrink: self.flex_shrink,
-            ..Default::default() // Ignore grid properties for now
+            grid_template_rows: to_grid_repeat(&self.grid_rows),
+            grid_template_columns: to_grid_repeat(&self.grid_cols),
+            grid_row: self
+                .grid_location
+                .as_ref()
+                .map(|location| to_grid_line(&location.row))
+                .unwrap_or_default(),
+            grid_column: self
+                .grid_location
+                .as_ref()
+                .map(|location| to_grid_line(&location.column))
+                .unwrap_or_default(),
+            ..Default::default()
         }
     }
 }

crates/gpui/src/text_system.rs 🔗

@@ -65,7 +65,7 @@ impl TextSystem {
             font_runs_pool: Mutex::default(),
             fallback_font_stack: smallvec![
                 // TODO: Remove this when Linux have implemented setting fallbacks.
-                font("Zed Plex Mono"),
+                font(".ZedMono"),
                 font("Helvetica"),
                 font("Segoe UI"),  // Windows
                 font("Cantarell"), // Gnome
@@ -96,7 +96,7 @@ impl TextSystem {
     }
 
     /// Get the FontId for the configure font family and style.
-    pub fn font_id(&self, font: &Font) -> Result<FontId> {
+    fn font_id(&self, font: &Font) -> Result<FontId> {
         fn clone_font_id_result(font_id: &Result<FontId>) -> Result<FontId> {
             match font_id {
                 Ok(font_id) => Ok(*font_id),
@@ -366,15 +366,14 @@ impl WindowTextSystem {
 
         let mut decoration_runs = SmallVec::<[DecorationRun; 32]>::new();
         for run in runs {
-            if let Some(last_run) = decoration_runs.last_mut() {
-                if last_run.color == run.color
-                    && last_run.underline == run.underline
-                    && last_run.strikethrough == run.strikethrough
-                    && last_run.background_color == run.background_color
-                {
-                    last_run.len += run.len as u32;
-                    continue;
-                }
+            if let Some(last_run) = decoration_runs.last_mut()
+                && last_run.color == run.color
+                && last_run.underline == run.underline
+                && last_run.strikethrough == run.strikethrough
+                && last_run.background_color == run.background_color
+            {
+                last_run.len += run.len as u32;
+                continue;
             }
             decoration_runs.push(DecorationRun {
                 len: run.len as u32,
@@ -436,7 +435,7 @@ impl WindowTextSystem {
                     });
                 }
 
-                if decoration_runs.last().map_or(false, |last_run| {
+                if decoration_runs.last().is_some_and(|last_run| {
                     last_run.color == run.color
                         && last_run.underline == run.underline
                         && last_run.strikethrough == run.strikethrough
@@ -492,14 +491,14 @@ impl WindowTextSystem {
         let mut split_lines = text.split('\n');
         let mut processed = false;
 
-        if let Some(first_line) = split_lines.next() {
-            if let Some(second_line) = split_lines.next() {
-                processed = true;
-                process_line(first_line.to_string().into());
-                process_line(second_line.to_string().into());
-                for line_text in split_lines {
-                    process_line(line_text.to_string().into());
-                }
+        if let Some(first_line) = split_lines.next()
+            && let Some(second_line) = split_lines.next()
+        {
+            processed = true;
+            process_line(first_line.to_string().into());
+            process_line(second_line.to_string().into());
+            for line_text in split_lines {
+                process_line(line_text.to_string().into());
             }
         }
 
@@ -534,11 +533,11 @@ impl WindowTextSystem {
         let mut font_runs = self.font_runs_pool.lock().pop().unwrap_or_default();
         for run in runs.iter() {
             let font_id = self.resolve_font(&run.font);
-            if let Some(last_run) = font_runs.last_mut() {
-                if last_run.font_id == font_id {
-                    last_run.len += run.len;
-                    continue;
-                }
+            if let Some(last_run) = font_runs.last_mut()
+                && last_run.font_id == font_id
+            {
+                last_run.len += run.len;
+                continue;
             }
             font_runs.push(FontRun {
                 len: run.len,
@@ -844,3 +843,16 @@ impl FontMetrics {
         (self.bounding_box / self.units_per_em as f32 * font_size.0).map(px)
     }
 }
+
+#[allow(unused)]
+pub(crate) fn font_name_with_fallbacks<'a>(name: &'a str, system: &'a str) -> &'a str {
+    // Note: the "Zed Plex" fonts were deprecated as we are not allowed to use "Plex"
+    // in a derived font name. They are essentially indistinguishable from IBM Plex/Lilex,
+    // and so retained here for backward compatibility.
+    match name {
+        ".SystemUIFont" => system,
+        ".ZedSans" | "Zed Plex Sans" => "IBM Plex Sans",
+        ".ZedMono" | "Zed Plex Mono" => "Lilex",
+        _ => name,
+    }
+}

crates/gpui/src/text_system/line.rs 🔗

@@ -292,10 +292,10 @@ fn paint_line(
                     }
 
                     if let Some(style_run) = style_run {
-                        if let Some((_, underline_style)) = &mut current_underline {
-                            if style_run.underline.as_ref() != Some(underline_style) {
-                                finished_underline = current_underline.take();
-                            }
+                        if let Some((_, underline_style)) = &mut current_underline
+                            && style_run.underline.as_ref() != Some(underline_style)
+                        {
+                            finished_underline = current_underline.take();
                         }
                         if let Some(run_underline) = style_run.underline.as_ref() {
                             current_underline.get_or_insert((
@@ -310,10 +310,10 @@ fn paint_line(
                                 },
                             ));
                         }
-                        if let Some((_, strikethrough_style)) = &mut current_strikethrough {
-                            if style_run.strikethrough.as_ref() != Some(strikethrough_style) {
-                                finished_strikethrough = current_strikethrough.take();
-                            }
+                        if let Some((_, strikethrough_style)) = &mut current_strikethrough
+                            && style_run.strikethrough.as_ref() != Some(strikethrough_style)
+                        {
+                            finished_strikethrough = current_strikethrough.take();
                         }
                         if let Some(run_strikethrough) = style_run.strikethrough.as_ref() {
                             current_strikethrough.get_or_insert((
@@ -509,10 +509,10 @@ fn paint_line_background(
                     }
 
                     if let Some(style_run) = style_run {
-                        if let Some((_, background_color)) = &mut current_background {
-                            if style_run.background_color.as_ref() != Some(background_color) {
-                                finished_background = current_background.take();
-                            }
+                        if let Some((_, background_color)) = &mut current_background
+                            && style_run.background_color.as_ref() != Some(background_color)
+                        {
+                            finished_background = current_background.take();
                         }
                         if let Some(run_background) = style_run.background_color {
                             current_background.get_or_insert((

crates/gpui/src/text_system/line_layout.rs 🔗

@@ -185,10 +185,10 @@ impl LineLayout {
 
             if width > wrap_width && boundary > last_boundary {
                 // When used line_clamp, we should limit the number of lines.
-                if let Some(max_lines) = max_lines {
-                    if boundaries.len() >= max_lines - 1 {
-                        break;
-                    }
+                if let Some(max_lines) = max_lines
+                    && boundaries.len() >= max_lines - 1
+                {
+                    break;
                 }
 
                 if let Some(last_candidate_ix) = last_candidate_ix.take() {

crates/gpui/src/text_system/line_wrapper.rs 🔗

@@ -44,7 +44,7 @@ impl LineWrapper {
         let mut prev_c = '\0';
         let mut index = 0;
         let mut candidates = fragments
-            .into_iter()
+            .iter()
             .flat_map(move |fragment| fragment.wrap_boundary_candidates())
             .peekable();
         iter::from_fn(move || {
@@ -327,7 +327,7 @@ mod tests {
     fn build_wrapper() -> LineWrapper {
         let dispatcher = TestDispatcher::new(StdRng::seed_from_u64(0));
         let cx = TestAppContext::build(dispatcher, None);
-        let id = cx.text_system().font_id(&font("Zed Plex Mono")).unwrap();
+        let id = cx.text_system().resolve_font(&font(".ZedMono"));
         LineWrapper::new(id, px(16.), cx.text_system().platform_text_system.clone())
     }
 

crates/gpui/src/util.rs 🔗

@@ -1,13 +1,11 @@
-use std::sync::atomic::AtomicUsize;
-use std::sync::atomic::Ordering::SeqCst;
-#[cfg(any(test, feature = "test-support"))]
-use std::time::Duration;
-
-#[cfg(any(test, feature = "test-support"))]
-use futures::Future;
-
-#[cfg(any(test, feature = "test-support"))]
-use smol::future::FutureExt;
+use crate::{BackgroundExecutor, Task};
+use std::{
+    future::Future,
+    pin::Pin,
+    sync::atomic::{AtomicUsize, Ordering::SeqCst},
+    task,
+    time::Duration,
+};
 
 pub use util::*;
 
@@ -60,18 +58,63 @@ pub trait FluentBuilder {
     where
         Self: Sized,
     {
-        self.map(|this| {
-            if let Some(_) = option {
-                this
-            } else {
-                then(this)
-            }
-        })
+        self.map(|this| if option.is_some() { this } else { then(this) })
+    }
+}
+
+/// Extensions for Future types that provide additional combinators and utilities.
+pub trait FutureExt {
+    /// Requires a Future to complete before the specified duration has elapsed.
+    /// Similar to tokio::timeout.
+    fn with_timeout(self, timeout: Duration, executor: &BackgroundExecutor) -> WithTimeout<Self>
+    where
+        Self: Sized;
+}
+
+impl<T: Future> FutureExt for T {
+    fn with_timeout(self, timeout: Duration, executor: &BackgroundExecutor) -> WithTimeout<Self>
+    where
+        Self: Sized,
+    {
+        WithTimeout {
+            future: self,
+            timer: executor.timer(timeout),
+        }
+    }
+}
+
+pub struct WithTimeout<T> {
+    future: T,
+    timer: Task<()>,
+}
+
+#[derive(Debug, thiserror::Error)]
+#[error("Timed out before future resolved")]
+/// Error returned by with_timeout when the timeout duration elapsed before the future resolved
+pub struct Timeout;
+
+impl<T: Future> Future for WithTimeout<T> {
+    type Output = Result<T::Output, Timeout>;
+
+    fn poll(self: Pin<&mut Self>, cx: &mut task::Context) -> task::Poll<Self::Output> {
+        // SAFETY: the fields of Timeout are private and we never move the future ourselves
+        // And its already pinned since we are being polled (all futures need to be pinned to be polled)
+        let this = unsafe { self.get_unchecked_mut() };
+        let future = unsafe { Pin::new_unchecked(&mut this.future) };
+        let timer = unsafe { Pin::new_unchecked(&mut this.timer) };
+
+        if let task::Poll::Ready(output) = future.poll(cx) {
+            task::Poll::Ready(Ok(output))
+        } else if timer.poll(cx).is_ready() {
+            task::Poll::Ready(Err(Timeout))
+        } else {
+            task::Poll::Pending
+        }
     }
 }
 
 #[cfg(any(test, feature = "test-support"))]
-pub async fn timeout<F, T>(timeout: Duration, f: F) -> Result<T, ()>
+pub async fn smol_timeout<F, T>(timeout: Duration, f: F) -> Result<T, ()>
 where
     F: Future<Output = T>,
 {
@@ -80,7 +123,7 @@ where
         Err(())
     };
     let future = async move { Ok(f.await) };
-    timer.race(future).await
+    smol::future::FutureExt::race(timer, future).await
 }
 
 /// Increment the given atomic counter if it is not zero.

crates/gpui/src/view.rs 🔗

@@ -205,22 +205,21 @@ impl Element for AnyView {
                     let content_mask = window.content_mask();
                     let text_style = window.text_style();
 
-                    if let Some(mut element_state) = element_state {
-                        if element_state.cache_key.bounds == bounds
-                            && element_state.cache_key.content_mask == content_mask
-                            && element_state.cache_key.text_style == text_style
-                            && !window.dirty_views.contains(&self.entity_id())
-                            && !window.refreshing
-                        {
-                            let prepaint_start = window.prepaint_index();
-                            window.reuse_prepaint(element_state.prepaint_range.clone());
-                            cx.entities
-                                .extend_accessed(&element_state.accessed_entities);
-                            let prepaint_end = window.prepaint_index();
-                            element_state.prepaint_range = prepaint_start..prepaint_end;
-
-                            return (None, element_state);
-                        }
+                    if let Some(mut element_state) = element_state
+                        && element_state.cache_key.bounds == bounds
+                        && element_state.cache_key.content_mask == content_mask
+                        && element_state.cache_key.text_style == text_style
+                        && !window.dirty_views.contains(&self.entity_id())
+                        && !window.refreshing
+                    {
+                        let prepaint_start = window.prepaint_index();
+                        window.reuse_prepaint(element_state.prepaint_range.clone());
+                        cx.entities
+                            .extend_accessed(&element_state.accessed_entities);
+                        let prepaint_end = window.prepaint_index();
+                        element_state.prepaint_range = prepaint_start..prepaint_end;
+
+                        return (None, element_state);
                     }
 
                     let refreshing = mem::replace(&mut window.refreshing, true);

crates/gpui/src/window.rs 🔗

@@ -243,14 +243,14 @@ impl FocusId {
     pub fn contains_focused(&self, window: &Window, cx: &App) -> bool {
         window
             .focused(cx)
-            .map_or(false, |focused| self.contains(focused.id, window))
+            .is_some_and(|focused| self.contains(focused.id, window))
     }
 
     /// Obtains whether the element associated with this handle is contained within the
     /// focused element or is itself focused.
     pub fn within_focused(&self, window: &Window, cx: &App) -> bool {
         let focused = window.focused(cx);
-        focused.map_or(false, |focused| focused.id.contains(*self, window))
+        focused.is_some_and(|focused| focused.id.contains(*self, window))
     }
 
     /// Obtains whether this handle contains the given handle in the most recently rendered frame.
@@ -504,7 +504,7 @@ impl HitboxId {
                 return true;
             }
         }
-        return false;
+        false
     }
 
     /// Checks if the hitbox with this ID contains the mouse and should handle scroll events.
@@ -634,7 +634,7 @@ impl TooltipId {
         window
             .tooltip_bounds
             .as_ref()
-            .map_or(false, |tooltip_bounds| {
+            .is_some_and(|tooltip_bounds| {
                 tooltip_bounds.id == *self
                     && tooltip_bounds.bounds.contains(&window.mouse_position())
             })
@@ -2453,7 +2453,7 @@ impl Window {
     /// time.
     pub fn get_asset<A: Asset>(&mut self, source: &A::Source, cx: &mut App) -> Option<A::Output> {
         let (task, _) = cx.fetch_asset::<A>(source);
-        task.clone().now_or_never()
+        task.now_or_never()
     }
     /// Obtain the current element offset. This method should only be called during the
     /// prepaint phase of element drawing.
@@ -2814,7 +2814,7 @@ impl Window {
             content_mask: content_mask.scale(scale_factor),
             color: style.color.unwrap_or_default().opacity(element_opacity),
             thickness: style.thickness.scale(scale_factor),
-            wavy: style.wavy,
+            wavy: if style.wavy { 1 } else { 0 },
         });
     }
 
@@ -2845,7 +2845,7 @@ impl Window {
             content_mask: content_mask.scale(scale_factor),
             thickness: style.thickness.scale(scale_factor),
             color: style.color.unwrap_or_default().opacity(opacity),
-            wavy: false,
+            wavy: 0,
         });
     }
 
@@ -3044,7 +3044,7 @@ impl Window {
 
         let tile = self
             .sprite_atlas
-            .get_or_insert_with(&params.clone().into(), &mut || {
+            .get_or_insert_with(&params.into(), &mut || {
                 Ok(Some((
                     data.size(frame_index),
                     Cow::Borrowed(
@@ -3401,16 +3401,16 @@ impl Window {
         let focus_id = handle.id;
         let (subscription, activate) =
             self.new_focus_listener(Box::new(move |event, window, cx| {
-                if let Some(blurred_id) = event.previous_focus_path.last().copied() {
-                    if event.is_focus_out(focus_id) {
-                        let event = FocusOutEvent {
-                            blurred: WeakFocusHandle {
-                                id: blurred_id,
-                                handles: Arc::downgrade(&cx.focus_handles),
-                            },
-                        };
-                        listener(event, window, cx)
-                    }
+                if let Some(blurred_id) = event.previous_focus_path.last().copied()
+                    && event.is_focus_out(focus_id)
+                {
+                    let event = FocusOutEvent {
+                        blurred: WeakFocusHandle {
+                            id: blurred_id,
+                            handles: Arc::downgrade(&cx.focus_handles),
+                        },
+                    };
+                    listener(event, window, cx)
                 }
                 true
             }));
@@ -3444,12 +3444,12 @@ impl Window {
             return true;
         }
 
-        if let Some(input) = keystroke.key_char {
-            if let Some(mut input_handler) = self.platform_window.take_input_handler() {
-                input_handler.dispatch_input(&input, self, cx);
-                self.platform_window.set_input_handler(input_handler);
-                return true;
-            }
+        if let Some(input) = keystroke.key_char
+            && let Some(mut input_handler) = self.platform_window.take_input_handler()
+        {
+            input_handler.dispatch_input(&input, self, cx);
+            self.platform_window.set_input_handler(input_handler);
+            return true;
         }
 
         false
@@ -3688,7 +3688,8 @@ impl Window {
         );
 
         if !match_result.to_replay.is_empty() {
-            self.replay_pending_input(match_result.to_replay, cx)
+            self.replay_pending_input(match_result.to_replay, cx);
+            cx.propagate_event = true;
         }
 
         if !match_result.pending.is_empty() {
@@ -3730,7 +3731,7 @@ impl Window {
                 self.dispatch_keystroke_observers(
                     event,
                     Some(binding.action),
-                    match_result.context_stack.clone(),
+                    match_result.context_stack,
                     cx,
                 );
                 self.pending_input_changed(cx);
@@ -3863,11 +3864,11 @@ impl Window {
             if !cx.propagate_event {
                 continue 'replay;
             }
-            if let Some(input) = replay.keystroke.key_char.as_ref().cloned() {
-                if let Some(mut input_handler) = self.platform_window.take_input_handler() {
-                    input_handler.dispatch_input(&input, self, cx);
-                    self.platform_window.set_input_handler(input_handler)
-                }
+            if let Some(input) = replay.keystroke.key_char.as_ref().cloned()
+                && let Some(mut input_handler) = self.platform_window.take_input_handler()
+            {
+                input_handler.dispatch_input(&input, self, cx);
+                self.platform_window.set_input_handler(input_handler)
             }
         }
     }
@@ -4308,15 +4309,15 @@ impl Window {
         cx: &mut App,
         f: impl FnOnce(&mut Option<T>, &mut Self) -> R,
     ) -> R {
-        if let Some(inspector_id) = _inspector_id {
-            if let Some(inspector) = &self.inspector {
-                let inspector = inspector.clone();
-                let active_element_id = inspector.read(cx).active_element_id();
-                if Some(inspector_id) == active_element_id {
-                    return inspector.update(cx, |inspector, _cx| {
-                        inspector.with_active_element_state(self, f)
-                    });
-                }
+        if let Some(inspector_id) = _inspector_id
+            && let Some(inspector) = &self.inspector
+        {
+            let inspector = inspector.clone();
+            let active_element_id = inspector.read(cx).active_element_id();
+            if Some(inspector_id) == active_element_id {
+                return inspector.update(cx, |inspector, _cx| {
+                    inspector.with_active_element_state(self, f)
+                });
             }
         }
         f(&mut None, self)
@@ -4388,15 +4389,13 @@ impl Window {
         if let Some(inspector) = self.inspector.as_ref() {
             let inspector = inspector.read(cx);
             if let Some((hitbox_id, _)) = self.hovered_inspector_hitbox(inspector, &self.next_frame)
-            {
-                if let Some(hitbox) = self
+                && let Some(hitbox) = self
                     .next_frame
                     .hitboxes
                     .iter()
                     .find(|hitbox| hitbox.id == hitbox_id)
-                {
-                    self.paint_quad(crate::fill(hitbox.bounds, crate::rgba(0x61afef4d)));
-                }
+            {
+                self.paint_quad(crate::fill(hitbox.bounds, crate::rgba(0x61afef4d)));
             }
         }
     }
@@ -4443,7 +4442,7 @@ impl Window {
                         if let Some((_, inspector_id)) =
                             self.hovered_inspector_hitbox(inspector, &self.rendered_frame)
                         {
-                            inspector.set_active_element_id(inspector_id.clone(), self);
+                            inspector.set_active_element_id(inspector_id, self);
                         }
                     }
                 });
@@ -4467,7 +4466,7 @@ impl Window {
                 }
             }
         }
-        return None;
+        None
     }
 }
 
@@ -4584,7 +4583,7 @@ impl<V: 'static + Render> WindowHandle<V> {
     where
         C: AppContext,
     {
-        cx.read_window(self, |root_view, _cx| root_view.clone())
+        cx.read_window(self, |root_view, _cx| root_view)
     }
 
     /// Check if this window is 'active'.

crates/gpui_macros/src/derive_inspector_reflection.rs 🔗

@@ -160,16 +160,14 @@ fn extract_doc_comment(attrs: &[Attribute]) -> Option<String> {
     let mut doc_lines = Vec::new();
 
     for attr in attrs {
-        if attr.path().is_ident("doc") {
-            if let Meta::NameValue(meta) = &attr.meta {
-                if let Expr::Lit(expr_lit) = &meta.value {
-                    if let Lit::Str(lit_str) = &expr_lit.lit {
-                        let line = lit_str.value();
-                        let line = line.strip_prefix(' ').unwrap_or(&line);
-                        doc_lines.push(line.to_string());
-                    }
-                }
-            }
+        if attr.path().is_ident("doc")
+            && let Meta::NameValue(meta) = &attr.meta
+            && let Expr::Lit(expr_lit) = &meta.value
+            && let Lit::Str(lit_str) = &expr_lit.lit
+        {
+            let line = lit_str.value();
+            let line = line.strip_prefix(' ').unwrap_or(&line);
+            doc_lines.push(line.to_string());
         }
     }
 
@@ -191,7 +189,7 @@ fn extract_cfg_attributes(attrs: &[Attribute]) -> Vec<Attribute> {
 fn is_called_from_gpui_crate(_span: Span) -> bool {
     // Check if we're being called from within the gpui crate by examining the call site
     // This is a heuristic approach - we check if the current crate name is "gpui"
-    std::env::var("CARGO_PKG_NAME").map_or(false, |name| name == "gpui")
+    std::env::var("CARGO_PKG_NAME").is_ok_and(|name| name == "gpui")
 }
 
 struct MacroExpander;

crates/gpui_macros/src/gpui_macros.rs 🔗

@@ -172,7 +172,7 @@ pub fn box_shadow_style_methods(input: TokenStream) -> TokenStream {
 /// - `#[gpui::test(iterations = 5)]` runs five times, providing as seed the values in the range `0..5`.
 /// - `#[gpui::test(retries = 3)]` runs up to four times if it fails to try and make it pass.
 /// - `#[gpui::test(on_failure = "crate::test::report_failure")]` will call the specified function after the
-///    tests fail so that you can write out more detail about the failure.
+///   tests fail so that you can write out more detail about the failure.
 ///
 /// You can combine `iterations = ...` with `seeds(...)`:
 /// - `#[gpui::test(iterations = 5, seed = 10)]` is equivalent to `#[gpui::test(seeds(0, 1, 2, 3, 4, 10))]`.

crates/gpui_macros/src/test.rs 🔗

@@ -73,7 +73,7 @@ impl Parse for Args {
                 (Meta::NameValue(meta), "seed") => {
                     seeds = vec![parse_usize_from_expr(&meta.value)? as u64]
                 }
-                (Meta::List(list), "seeds") => seeds = parse_u64_array(&list)?,
+                (Meta::List(list), "seeds") => seeds = parse_u64_array(list)?,
                 (Meta::Path(_), _) => {
                     return Err(syn::Error::new(meta.span(), "invalid path argument"));
                 }
@@ -86,7 +86,7 @@ impl Parse for Args {
         Ok(Args {
             seeds,
             max_retries,
-            max_iterations: max_iterations,
+            max_iterations,
             on_failure_fn_name,
         })
     }
@@ -152,27 +152,28 @@ fn generate_test_function(
                         }
                         _ => {}
                     }
-                } else if let Type::Reference(ty) = &*arg.ty {
-                    if let Type::Path(ty) = &*ty.elem {
-                        let last_segment = ty.path.segments.last();
-                        if let Some("TestAppContext") =
-                            last_segment.map(|s| s.ident.to_string()).as_deref()
-                        {
-                            let cx_varname = format_ident!("cx_{}", ix);
-                            cx_vars.extend(quote!(
-                                let mut #cx_varname = gpui::TestAppContext::build(
-                                    dispatcher.clone(),
-                                    Some(stringify!(#outer_fn_name)),
-                                );
-                            ));
-                            cx_teardowns.extend(quote!(
-                                dispatcher.run_until_parked();
-                                #cx_varname.quit();
-                                dispatcher.run_until_parked();
-                            ));
-                            inner_fn_args.extend(quote!(&mut #cx_varname,));
-                            continue;
-                        }
+                } else if let Type::Reference(ty) = &*arg.ty
+                    && let Type::Path(ty) = &*ty.elem
+                {
+                    let last_segment = ty.path.segments.last();
+                    if let Some("TestAppContext") =
+                        last_segment.map(|s| s.ident.to_string()).as_deref()
+                    {
+                        let cx_varname = format_ident!("cx_{}", ix);
+                        cx_vars.extend(quote!(
+                            let mut #cx_varname = gpui::TestAppContext::build(
+                                dispatcher.clone(),
+                                Some(stringify!(#outer_fn_name)),
+                            );
+                        ));
+                        cx_teardowns.extend(quote!(
+                            dispatcher.run_until_parked();
+                            #cx_varname.executor().forbid_parking();
+                            #cx_varname.quit();
+                            dispatcher.run_until_parked();
+                        ));
+                        inner_fn_args.extend(quote!(&mut #cx_varname,));
+                        continue;
                     }
                 }
             }
@@ -214,47 +215,48 @@ fn generate_test_function(
                         inner_fn_args.extend(quote!(rand::SeedableRng::seed_from_u64(_seed),));
                         continue;
                     }
-                } else if let Type::Reference(ty) = &*arg.ty {
-                    if let Type::Path(ty) = &*ty.elem {
-                        let last_segment = ty.path.segments.last();
-                        match last_segment.map(|s| s.ident.to_string()).as_deref() {
-                            Some("App") => {
-                                let cx_varname = format_ident!("cx_{}", ix);
-                                let cx_varname_lock = format_ident!("cx_{}_lock", ix);
-                                cx_vars.extend(quote!(
-                                    let mut #cx_varname = gpui::TestAppContext::build(
-                                       dispatcher.clone(),
-                                       Some(stringify!(#outer_fn_name))
-                                    );
-                                    let mut #cx_varname_lock = #cx_varname.app.borrow_mut();
-                                ));
-                                inner_fn_args.extend(quote!(&mut #cx_varname_lock,));
-                                cx_teardowns.extend(quote!(
+                } else if let Type::Reference(ty) = &*arg.ty
+                    && let Type::Path(ty) = &*ty.elem
+                {
+                    let last_segment = ty.path.segments.last();
+                    match last_segment.map(|s| s.ident.to_string()).as_deref() {
+                        Some("App") => {
+                            let cx_varname = format_ident!("cx_{}", ix);
+                            let cx_varname_lock = format_ident!("cx_{}_lock", ix);
+                            cx_vars.extend(quote!(
+                                let mut #cx_varname = gpui::TestAppContext::build(
+                                   dispatcher.clone(),
+                                   Some(stringify!(#outer_fn_name))
+                                );
+                                let mut #cx_varname_lock = #cx_varname.app.borrow_mut();
+                            ));
+                            inner_fn_args.extend(quote!(&mut #cx_varname_lock,));
+                            cx_teardowns.extend(quote!(
                                     drop(#cx_varname_lock);
                                     dispatcher.run_until_parked();
-                                    #cx_varname.update(|cx| { cx.quit() });
-                                    dispatcher.run_until_parked();
-                                ));
-                                continue;
-                            }
-                            Some("TestAppContext") => {
-                                let cx_varname = format_ident!("cx_{}", ix);
-                                cx_vars.extend(quote!(
-                                    let mut #cx_varname = gpui::TestAppContext::build(
-                                        dispatcher.clone(),
-                                        Some(stringify!(#outer_fn_name))
-                                    );
-                                ));
-                                cx_teardowns.extend(quote!(
-                                    dispatcher.run_until_parked();
-                                    #cx_varname.quit();
+                                    #cx_varname.update(|cx| { cx.background_executor().forbid_parking(); cx.quit(); });
                                     dispatcher.run_until_parked();
                                 ));
-                                inner_fn_args.extend(quote!(&mut #cx_varname,));
-                                continue;
-                            }
-                            _ => {}
+                            continue;
                         }
+                        Some("TestAppContext") => {
+                            let cx_varname = format_ident!("cx_{}", ix);
+                            cx_vars.extend(quote!(
+                                let mut #cx_varname = gpui::TestAppContext::build(
+                                    dispatcher.clone(),
+                                    Some(stringify!(#outer_fn_name))
+                                );
+                            ));
+                            cx_teardowns.extend(quote!(
+                                dispatcher.run_until_parked();
+                                #cx_varname.executor().forbid_parking();
+                                #cx_varname.quit();
+                                dispatcher.run_until_parked();
+                            ));
+                            inner_fn_args.extend(quote!(&mut #cx_varname,));
+                            continue;
+                        }
+                        _ => {}
                     }
                 }
             }

crates/gpui_macros/tests/derive_inspector_reflection.rs 🔗

@@ -34,13 +34,6 @@ trait Transform: Clone {
 
     /// Adds one to the value
     fn add_one(self) -> Self;
-
-    /// cfg attributes are respected
-    #[cfg(all())]
-    fn cfg_included(self) -> Self;
-
-    #[cfg(any())]
-    fn cfg_omitted(self) -> Self;
 }
 
 #[derive(Debug, Clone, PartialEq)]
@@ -70,10 +63,6 @@ impl Transform for Number {
     fn add_one(self) -> Self {
         Number(self.0 + 1)
     }
-
-    fn cfg_included(self) -> Self {
-        Number(self.0)
-    }
 }
 
 #[test]
@@ -83,14 +72,13 @@ fn test_derive_inspector_reflection() {
     // Get all methods that match the pattern fn(self) -> Self or fn(mut self) -> Self
     let methods = methods::<Number>();
 
-    assert_eq!(methods.len(), 6);
+    assert_eq!(methods.len(), 5);
     let method_names: Vec<_> = methods.iter().map(|m| m.name).collect();
     assert!(method_names.contains(&"double"));
     assert!(method_names.contains(&"triple"));
     assert!(method_names.contains(&"increment"));
     assert!(method_names.contains(&"quadruple"));
     assert!(method_names.contains(&"add_one"));
-    assert!(method_names.contains(&"cfg_included"));
 
     // Invoke methods by name
     let num = Number(5);
@@ -106,9 +94,7 @@ fn test_derive_inspector_reflection() {
         .invoke(num.clone());
     assert_eq!(incremented, Number(6));
 
-    let quadrupled = find_method::<Number>("quadruple")
-        .unwrap()
-        .invoke(num.clone());
+    let quadrupled = find_method::<Number>("quadruple").unwrap().invoke(num);
     assert_eq!(quadrupled, Number(20));
 
     // Try to invoke a non-existent method

crates/html_to_markdown/src/markdown.rs 🔗

@@ -34,15 +34,14 @@ impl HandleTag for ParagraphHandler {
         tag: &HtmlElement,
         writer: &mut MarkdownWriter,
     ) -> StartTagOutcome {
-        if tag.is_inline() && writer.is_inside("p") {
-            if let Some(parent) = writer.current_element_stack().iter().last() {
-                if !(parent.is_inline()
-                    || writer.markdown.ends_with(' ')
-                    || writer.markdown.ends_with('\n'))
-                {
-                    writer.push_str(" ");
-                }
-            }
+        if tag.is_inline()
+            && writer.is_inside("p")
+            && let Some(parent) = writer.current_element_stack().iter().last()
+            && !(parent.is_inline()
+                || writer.markdown.ends_with(' ')
+                || writer.markdown.ends_with('\n'))
+        {
+            writer.push_str(" ");
         }
 
         if tag.tag() == "p" {

crates/http_client/src/async_body.rs 🔗

@@ -40,7 +40,7 @@ impl AsyncBody {
     }
 
     pub fn from_bytes(bytes: Bytes) -> Self {
-        Self(Inner::Bytes(Cursor::new(bytes.clone())))
+        Self(Inner::Bytes(Cursor::new(bytes)))
     }
 }
 

crates/http_client/src/github.rs 🔗

@@ -71,11 +71,19 @@ pub async fn latest_github_release(
         }
     };
 
-    releases
+    let mut release = releases
         .into_iter()
         .filter(|release| !require_assets || !release.assets.is_empty())
         .find(|release| release.pre_release == pre_release)
-        .context("finding a prerelease")
+        .context("finding a prerelease")?;
+    release.assets.iter_mut().for_each(|asset| {
+        if let Some(digest) = &mut asset.digest
+            && let Some(stripped) = digest.strip_prefix("sha256:")
+        {
+            *digest = stripped.to_owned();
+        }
+    });
+    Ok(release)
 }
 
 pub async fn get_release_by_tag_name(

crates/http_client/src/http_client.rs 🔗

@@ -435,8 +435,7 @@ impl HttpClient for FakeHttpClient {
         &self,
         req: Request<AsyncBody>,
     ) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
-        let future = (self.handler.lock().as_ref().unwrap())(req);
-        future
+        ((self.handler.lock().as_ref().unwrap())(req)) as _
     }
 
     fn user_agent(&self) -> Option<&HeaderValue> {

crates/icons/README.md 🔗

@@ -0,0 +1,29 @@
+# Zed Icons
+
+## Guidelines
+
+Icons are a big part of Zed, and they're how we convey hundreds of actions without relying on labeled buttons.
+When introducing a new icon, it's important to ensure consistency with the existing set, which follows these guidelines:
+
+1. The SVG view box should be 16x16.
+2. For outlined icons, use a 1.2px stroke width.
+3. Not all icons are mathematically aligned; there's quite a bit of optical adjustment. However, try to keep the icon within an internal 12x12 bounding box as much as possible while ensuring proper visibility.
+4. Use the `filled` and `outlined` terminology when introducing icons that will have these two variants.
+5. Icons that are deeply contextual may have the feature context as their name prefix. For example, `ToolWeb`, `ReplPlay`, `DebugStepInto`, etc.
+6. Avoid complex layer structures in the icon SVG, like clipping masks and similar elements. When the shape becomes too complex, we recommend running the SVG through [SVGOMG](https://jakearchibald.github.io/svgomg/) to clean it up.
+
+## Sourcing
+
+Most icons are created by sourcing them from [Lucide](https://lucide.dev/).
+Then, they're modified, adjusted, cleaned up, and simplified depending on their use and overall fit with Zed.
+
+Sometimes, we may use other sources like [Phosphor](https://phosphoricons.com/), but we also design many icons completely from scratch.
+
+## Contributing
+
+To introduce a new icon, add the `.svg` file to the `assets/icon` directory and then add its corresponding item to the `icons.rs` file within the `crates` directory.
+
+- SVG files in the assets folder follow a snake_case name format.
+- Icons in the `icons.rs` file follow the PascalCase name format.
+
+Make sure to tag a member of Zed's design team so we can review and adjust any newly introduced icon.

crates/icons/src/icons.rs 🔗

@@ -28,16 +28,12 @@ pub enum IconName {
     ArrowCircle,
     ArrowDown,
     ArrowDown10,
-    ArrowDownFromLine,
     ArrowDownRight,
     ArrowLeft,
     ArrowRight,
     ArrowRightLeft,
     ArrowUp,
-    ArrowUpAlt,
-    ArrowUpFromLine,
     ArrowUpRight,
-    ArrowUpRightAlt,
     AudioOff,
     AudioOn,
     Backspace,
@@ -51,28 +47,22 @@ pub enum IconName {
     BoltFilled,
     Book,
     BookCopy,
-    BugOff,
     CaseSensitive,
     Chat,
     Check,
     CheckDouble,
     ChevronDown,
-    /// This chevron indicates a popover menu.
-    ChevronDownSmall,
     ChevronLeft,
     ChevronRight,
     ChevronUp,
     ChevronUpDown,
     Circle,
-    CircleOff,
     CircleHelp,
     Close,
-    Cloud,
     CloudDownload,
     Code,
     Cog,
     Command,
-    Context,
     Control,
     Copilot,
     CopilotDisabled,
@@ -93,16 +83,12 @@ pub enum IconName {
     DebugIgnoreBreakpoints,
     DebugLogBreakpoint,
     DebugPause,
-    DebugRestart,
     DebugStepBack,
     DebugStepInto,
     DebugStepOut,
     DebugStepOver,
-    DebugStop,
-    Delete,
     Diff,
     Disconnected,
-    DocumentText,
     Download,
     EditorAtom,
     EditorCursor,
@@ -113,59 +99,51 @@ pub enum IconName {
     Ellipsis,
     EllipsisVertical,
     Envelope,
-    Equal,
     Eraser,
     Escape,
     Exit,
     ExpandDown,
     ExpandUp,
     ExpandVertical,
-    ExternalLink,
     Eye,
     File,
     FileCode,
-    FileCreate,
     FileDiff,
     FileDoc,
     FileGeneric,
     FileGit,
     FileLock,
+    FileMarkdown,
     FileRust,
-    FileSearch,
-    FileText,
+    FileTextFilled,
+    FileTextOutlined,
     FileToml,
     FileTree,
     Filter,
     Flame,
     Folder,
     FolderOpen,
-    FolderX,
+    FolderSearch,
     Font,
     FontSize,
     FontWeight,
     ForwardArrow,
-    Function,
     GenericClose,
     GenericMaximize,
     GenericMinimize,
     GenericRestore,
     GitBranch,
-    GitBranchSmall,
+    GitBranchAlt,
     Github,
-    Globe,
-    Hammer,
     Hash,
     HistoryRerun,
     Image,
     Indicator,
     Info,
-    InlayHint,
+    Json,
     Keyboard,
-    Layout,
     Library,
-    LightBulb,
     LineHeight,
-    Link,
     ListCollapse,
     ListTodo,
     ListTree,
@@ -173,35 +151,29 @@ pub enum IconName {
     LoadCircle,
     LocationEdit,
     LockOutlined,
-    LspDebug,
-    LspRestart,
-    LspStop,
     MagnifyingGlass,
-    MailOpen,
     Maximize,
     Menu,
     MenuAlt,
+    MenuAltTemp,
     Mic,
     MicMute,
     Minimize,
+    Notepad,
     Option,
     PageDown,
     PageUp,
-    PanelLeft,
-    PanelRight,
     Pencil,
     Person,
-    PersonCircle,
-    PhoneIncoming,
     Pin,
     PlayOutlined,
     PlayFilled,
     Plus,
-    PocketKnife,
     Power,
     Public,
     PullRequest,
     Quote,
+    Reader,
     RefreshTitle,
     Regex,
     ReplNeutral,
@@ -213,28 +185,18 @@ pub enum IconName {
     Return,
     RotateCcw,
     RotateCw,
-    Route,
-    Save,
     Scissors,
     Screen,
-    ScrollText,
-    SearchSelection,
     SelectAll,
     Send,
     Server,
     Settings,
-    SettingsAlt,
     ShieldCheck,
     Shift,
     Slash,
-    SlashSquare,
     Sliders,
-    SlidersVertical,
-    Snip,
     Space,
     Sparkle,
-    SparkleAlt,
-    SparkleFilled,
     Split,
     SplitAlt,
     SquareDot,
@@ -243,7 +205,6 @@ pub enum IconName {
     Star,
     StarFilled,
     Stop,
-    StopFilled,
     Supermaven,
     SupermavenDisabled,
     SupermavenError,
@@ -279,18 +240,15 @@ pub enum IconName {
     TriangleRight,
     Undo,
     Unpin,
-    Update,
     UserCheck,
     UserGroup,
     UserRoundPen,
-    Visible,
-    Wand,
     Warning,
     WholeWord,
-    X,
     XCircle,
+    XCircleFilled,
+    ZedAgent,
     ZedAssistant,
-    ZedAssistantFilled,
     ZedBurnMode,
     ZedBurnModeOn,
     ZedMcpCustom,

crates/indexed_docs/Cargo.toml 🔗

@@ -1,38 +0,0 @@
-[package]
-name = "indexed_docs"
-version = "0.1.0"
-edition.workspace = true
-publish.workspace = true
-license = "GPL-3.0-or-later"
-
-[lints]
-workspace = true
-
-[lib]
-path = "src/indexed_docs.rs"
-
-[dependencies]
-anyhow.workspace = true
-async-trait.workspace = true
-cargo_metadata.workspace = true
-collections.workspace = true
-derive_more.workspace = true
-extension.workspace = true
-fs.workspace = true
-futures.workspace = true
-fuzzy.workspace = true
-gpui.workspace = true
-heed.workspace = true
-html_to_markdown.workspace = true
-http_client.workspace = true
-indexmap.workspace = true
-parking_lot.workspace = true
-paths.workspace = true
-serde.workspace = true
-strum.workspace = true
-util.workspace = true
-workspace-hack.workspace = true
-
-[dev-dependencies]
-indoc.workspace = true
-pretty_assertions.workspace = true

crates/indexed_docs/src/extension_indexed_docs_provider.rs 🔗

@@ -1,81 +0,0 @@
-use std::path::PathBuf;
-use std::sync::Arc;
-
-use anyhow::Result;
-use async_trait::async_trait;
-use extension::{Extension, ExtensionHostProxy, ExtensionIndexedDocsProviderProxy};
-use gpui::App;
-
-use crate::{
-    IndexedDocsDatabase, IndexedDocsProvider, IndexedDocsRegistry, PackageName, ProviderId,
-};
-
-pub fn init(cx: &mut App) {
-    let proxy = ExtensionHostProxy::default_global(cx);
-    proxy.register_indexed_docs_provider_proxy(IndexedDocsRegistryProxy {
-        indexed_docs_registry: IndexedDocsRegistry::global(cx),
-    });
-}
-
-struct IndexedDocsRegistryProxy {
-    indexed_docs_registry: Arc<IndexedDocsRegistry>,
-}
-
-impl ExtensionIndexedDocsProviderProxy for IndexedDocsRegistryProxy {
-    fn register_indexed_docs_provider(&self, extension: Arc<dyn Extension>, provider_id: Arc<str>) {
-        self.indexed_docs_registry
-            .register_provider(Box::new(ExtensionIndexedDocsProvider::new(
-                extension,
-                ProviderId(provider_id),
-            )));
-    }
-
-    fn unregister_indexed_docs_provider(&self, provider_id: Arc<str>) {
-        self.indexed_docs_registry
-            .unregister_provider(&ProviderId(provider_id));
-    }
-}
-
-pub struct ExtensionIndexedDocsProvider {
-    extension: Arc<dyn Extension>,
-    id: ProviderId,
-}
-
-impl ExtensionIndexedDocsProvider {
-    pub fn new(extension: Arc<dyn Extension>, id: ProviderId) -> Self {
-        Self { extension, id }
-    }
-}
-
-#[async_trait]
-impl IndexedDocsProvider for ExtensionIndexedDocsProvider {
-    fn id(&self) -> ProviderId {
-        self.id.clone()
-    }
-
-    fn database_path(&self) -> PathBuf {
-        let mut database_path = PathBuf::from(self.extension.work_dir().as_ref());
-        database_path.push("docs");
-        database_path.push(format!("{}.0.mdb", self.id));
-
-        database_path
-    }
-
-    async fn suggest_packages(&self) -> Result<Vec<PackageName>> {
-        let packages = self
-            .extension
-            .suggest_docs_packages(self.id.0.clone())
-            .await?;
-
-        Ok(packages
-            .into_iter()
-            .map(|package| PackageName::from(package.as_str()))
-            .collect())
-    }
-
-    async fn index(&self, package: PackageName, database: Arc<IndexedDocsDatabase>) -> Result<()> {
-        self.extension
-            .index_docs(self.id.0.clone(), package.as_ref().into(), database)
-            .await
-    }
-}

crates/indexed_docs/src/indexed_docs.rs 🔗

@@ -1,16 +0,0 @@
-mod extension_indexed_docs_provider;
-mod providers;
-mod registry;
-mod store;
-
-use gpui::App;
-
-pub use crate::extension_indexed_docs_provider::ExtensionIndexedDocsProvider;
-pub use crate::providers::rustdoc::*;
-pub use crate::registry::*;
-pub use crate::store::*;
-
-pub fn init(cx: &mut App) {
-    IndexedDocsRegistry::init_global(cx);
-    extension_indexed_docs_provider::init(cx);
-}

crates/indexed_docs/src/providers/rustdoc.rs 🔗

@@ -1,291 +0,0 @@
-mod item;
-mod to_markdown;
-
-use cargo_metadata::MetadataCommand;
-use futures::future::BoxFuture;
-pub use item::*;
-use parking_lot::RwLock;
-pub use to_markdown::convert_rustdoc_to_markdown;
-
-use std::collections::BTreeSet;
-use std::path::PathBuf;
-use std::sync::{Arc, LazyLock};
-use std::time::{Duration, Instant};
-
-use anyhow::{Context as _, Result, bail};
-use async_trait::async_trait;
-use collections::{HashSet, VecDeque};
-use fs::Fs;
-use futures::{AsyncReadExt, FutureExt};
-use http_client::{AsyncBody, HttpClient, HttpClientWithUrl};
-
-use crate::{IndexedDocsDatabase, IndexedDocsProvider, PackageName, ProviderId};
-
-#[derive(Debug)]
-struct RustdocItemWithHistory {
-    pub item: RustdocItem,
-    #[cfg(debug_assertions)]
-    pub history: Vec<String>,
-}
-
-pub struct LocalRustdocProvider {
-    fs: Arc<dyn Fs>,
-    cargo_workspace_root: PathBuf,
-}
-
-impl LocalRustdocProvider {
-    pub fn id() -> ProviderId {
-        ProviderId("rustdoc".into())
-    }
-
-    pub fn new(fs: Arc<dyn Fs>, cargo_workspace_root: PathBuf) -> Self {
-        Self {
-            fs,
-            cargo_workspace_root,
-        }
-    }
-}
-
-#[async_trait]
-impl IndexedDocsProvider for LocalRustdocProvider {
-    fn id(&self) -> ProviderId {
-        Self::id()
-    }
-
-    fn database_path(&self) -> PathBuf {
-        paths::data_dir().join("docs/rust/rustdoc-db.1.mdb")
-    }
-
-    async fn suggest_packages(&self) -> Result<Vec<PackageName>> {
-        static WORKSPACE_CRATES: LazyLock<RwLock<Option<(BTreeSet<PackageName>, Instant)>>> =
-            LazyLock::new(|| RwLock::new(None));
-
-        if let Some((crates, fetched_at)) = &*WORKSPACE_CRATES.read() {
-            if fetched_at.elapsed() < Duration::from_secs(300) {
-                return Ok(crates.iter().cloned().collect());
-            }
-        }
-
-        let workspace = MetadataCommand::new()
-            .manifest_path(self.cargo_workspace_root.join("Cargo.toml"))
-            .exec()
-            .context("failed to load cargo metadata")?;
-
-        let workspace_crates = workspace
-            .packages
-            .into_iter()
-            .map(|package| PackageName::from(package.name.as_str()))
-            .collect::<BTreeSet<_>>();
-
-        *WORKSPACE_CRATES.write() = Some((workspace_crates.clone(), Instant::now()));
-
-        Ok(workspace_crates.into_iter().collect())
-    }
-
-    async fn index(&self, package: PackageName, database: Arc<IndexedDocsDatabase>) -> Result<()> {
-        index_rustdoc(package, database, {
-            move |crate_name, item| {
-                let fs = self.fs.clone();
-                let cargo_workspace_root = self.cargo_workspace_root.clone();
-                let crate_name = crate_name.clone();
-                let item = item.cloned();
-                async move {
-                    let target_doc_path = cargo_workspace_root.join("target/doc");
-                    let mut local_cargo_doc_path = target_doc_path.join(crate_name.as_ref().replace('-', "_"));
-
-                    if !fs.is_dir(&local_cargo_doc_path).await {
-                        let cargo_doc_exists_at_all = fs.is_dir(&target_doc_path).await;
-                        if cargo_doc_exists_at_all {
-                            bail!(
-                                "no docs directory for '{crate_name}'. if this is a valid crate name, try running `cargo doc`"
-                            );
-                        } else {
-                            bail!("no cargo doc directory. run `cargo doc`");
-                        }
-                    }
-
-                    if let Some(item) = item {
-                        local_cargo_doc_path.push(item.url_path());
-                    } else {
-                        local_cargo_doc_path.push("index.html");
-                    }
-
-                    let Ok(contents) = fs.load(&local_cargo_doc_path).await else {
-                        return Ok(None);
-                    };
-
-                    Ok(Some(contents))
-                }
-                .boxed()
-            }
-        })
-        .await
-    }
-}
-
-pub struct DocsDotRsProvider {
-    http_client: Arc<HttpClientWithUrl>,
-}
-
-impl DocsDotRsProvider {
-    pub fn id() -> ProviderId {
-        ProviderId("docs-rs".into())
-    }
-
-    pub fn new(http_client: Arc<HttpClientWithUrl>) -> Self {
-        Self { http_client }
-    }
-}
-
-#[async_trait]
-impl IndexedDocsProvider for DocsDotRsProvider {
-    fn id(&self) -> ProviderId {
-        Self::id()
-    }
-
-    fn database_path(&self) -> PathBuf {
-        paths::data_dir().join("docs/rust/docs-rs-db.1.mdb")
-    }
-
-    async fn suggest_packages(&self) -> Result<Vec<PackageName>> {
-        static POPULAR_CRATES: LazyLock<Vec<PackageName>> = LazyLock::new(|| {
-            include_str!("./rustdoc/popular_crates.txt")
-                .lines()
-                .filter(|line| !line.starts_with('#'))
-                .map(|line| PackageName::from(line.trim()))
-                .collect()
-        });
-
-        Ok(POPULAR_CRATES.clone())
-    }
-
-    async fn index(&self, package: PackageName, database: Arc<IndexedDocsDatabase>) -> Result<()> {
-        index_rustdoc(package, database, {
-            move |crate_name, item| {
-                let http_client = self.http_client.clone();
-                let crate_name = crate_name.clone();
-                let item = item.cloned();
-                async move {
-                    let version = "latest";
-                    let path = format!(
-                        "{crate_name}/{version}/{crate_name}{item_path}",
-                        item_path = item
-                            .map(|item| format!("/{}", item.url_path()))
-                            .unwrap_or_default()
-                    );
-
-                    let mut response = http_client
-                        .get(
-                            &format!("https://docs.rs/{path}"),
-                            AsyncBody::default(),
-                            true,
-                        )
-                        .await?;
-
-                    let mut body = Vec::new();
-                    response
-                        .body_mut()
-                        .read_to_end(&mut body)
-                        .await
-                        .context("error reading docs.rs 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()
-                        );
-                    }
-
-                    Ok(Some(String::from_utf8(body)?))
-                }
-                .boxed()
-            }
-        })
-        .await
-    }
-}
-
-async fn index_rustdoc(
-    package: PackageName,
-    database: Arc<IndexedDocsDatabase>,
-    fetch_page: impl Fn(
-        &PackageName,
-        Option<&RustdocItem>,
-    ) -> BoxFuture<'static, Result<Option<String>>>
-    + Send
-    + Sync,
-) -> Result<()> {
-    let Some(package_root_content) = fetch_page(&package, None).await? else {
-        return Ok(());
-    };
-
-    let (crate_root_markdown, items) =
-        convert_rustdoc_to_markdown(package_root_content.as_bytes())?;
-
-    database
-        .insert(package.to_string(), crate_root_markdown)
-        .await?;
-
-    let mut seen_items = HashSet::from_iter(items.clone());
-    let mut items_to_visit: VecDeque<RustdocItemWithHistory> =
-        VecDeque::from_iter(items.into_iter().map(|item| RustdocItemWithHistory {
-            item,
-            #[cfg(debug_assertions)]
-            history: Vec::new(),
-        }));
-
-    while let Some(item_with_history) = items_to_visit.pop_front() {
-        let item = &item_with_history.item;
-
-        let Some(result) = fetch_page(&package, Some(item)).await.with_context(|| {
-            #[cfg(debug_assertions)]
-            {
-                format!(
-                    "failed to fetch {item:?}: {history:?}",
-                    history = item_with_history.history
-                )
-            }
-
-            #[cfg(not(debug_assertions))]
-            {
-                format!("failed to fetch {item:?}")
-            }
-        })?
-        else {
-            continue;
-        };
-
-        let (markdown, referenced_items) = convert_rustdoc_to_markdown(result.as_bytes())?;
-
-        database
-            .insert(format!("{package}::{}", item.display()), markdown)
-            .await?;
-
-        let parent_item = item;
-        for mut item in referenced_items {
-            if seen_items.contains(&item) {
-                continue;
-            }
-
-            seen_items.insert(item.clone());
-
-            item.path.extend(parent_item.path.clone());
-            if parent_item.kind == RustdocItemKind::Mod {
-                item.path.push(parent_item.name.clone());
-            }
-
-            items_to_visit.push_back(RustdocItemWithHistory {
-                #[cfg(debug_assertions)]
-                history: {
-                    let mut history = item_with_history.history.clone();
-                    history.push(item.url_path());
-                    history
-                },
-                item,
-            });
-        }
-    }
-
-    Ok(())
-}

crates/indexed_docs/src/providers/rustdoc/item.rs 🔗

@@ -1,82 +0,0 @@
-use std::sync::Arc;
-
-use serde::{Deserialize, Serialize};
-use strum::EnumIter;
-
-#[derive(
-    Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize, EnumIter,
-)]
-#[serde(rename_all = "snake_case")]
-pub enum RustdocItemKind {
-    Mod,
-    Macro,
-    Struct,
-    Enum,
-    Constant,
-    Trait,
-    Function,
-    TypeAlias,
-    AttributeMacro,
-    DeriveMacro,
-}
-
-impl RustdocItemKind {
-    pub(crate) const fn class(&self) -> &'static str {
-        match self {
-            Self::Mod => "mod",
-            Self::Macro => "macro",
-            Self::Struct => "struct",
-            Self::Enum => "enum",
-            Self::Constant => "constant",
-            Self::Trait => "trait",
-            Self::Function => "fn",
-            Self::TypeAlias => "type",
-            Self::AttributeMacro => "attr",
-            Self::DeriveMacro => "derive",
-        }
-    }
-}
-
-#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
-pub struct RustdocItem {
-    pub kind: RustdocItemKind,
-    /// The item path, up until the name of the item.
-    pub path: Vec<Arc<str>>,
-    /// The name of the item.
-    pub name: Arc<str>,
-}
-
-impl RustdocItem {
-    pub fn display(&self) -> String {
-        let mut path_segments = self.path.clone();
-        path_segments.push(self.name.clone());
-
-        path_segments.join("::")
-    }
-
-    pub fn url_path(&self) -> String {
-        let name = &self.name;
-        let mut path_components = self.path.clone();
-
-        match self.kind {
-            RustdocItemKind::Mod => {
-                path_components.push(name.clone());
-                path_components.push("index.html".into());
-            }
-            RustdocItemKind::Macro
-            | RustdocItemKind::Struct
-            | RustdocItemKind::Enum
-            | RustdocItemKind::Constant
-            | RustdocItemKind::Trait
-            | RustdocItemKind::Function
-            | RustdocItemKind::TypeAlias
-            | RustdocItemKind::AttributeMacro
-            | RustdocItemKind::DeriveMacro => {
-                path_components
-                    .push(format!("{kind}.{name}.html", kind = self.kind.class()).into());
-            }
-        }
-
-        path_components.join("/")
-    }
-}

crates/indexed_docs/src/providers/rustdoc/popular_crates.txt 🔗

@@ -1,252 +0,0 @@
-# A list of the most popular Rust crates.
-# Sourced from https://lib.rs/std.
-serde
-serde_json
-syn
-clap
-thiserror
-rand
-log
-tokio
-anyhow
-regex
-quote
-proc-macro2
-base64
-itertools
-chrono
-lazy_static
-once_cell
-libc
-reqwest
-futures
-bitflags
-tracing
-url
-bytes
-toml
-tempfile
-uuid
-indexmap
-env_logger
-num-traits
-async-trait
-sha2
-hex
-tracing-subscriber
-http
-parking_lot
-cfg-if
-futures-util
-cc
-hashbrown
-rayon
-hyper
-getrandom
-semver
-strum
-flate2
-tokio-util
-smallvec
-criterion
-paste
-heck
-rand_core
-nom
-rustls
-nix
-glob
-time
-byteorder
-strum_macros
-serde_yaml
-wasm-bindgen
-ahash
-either
-num_cpus
-rand_chacha
-prost
-percent-encoding
-pin-project-lite
-tokio-stream
-bincode
-walkdir
-bindgen
-axum
-windows-sys
-futures-core
-ring
-digest
-num-bigint
-rustls-pemfile
-serde_with
-crossbeam-channel
-tokio-rustls
-hmac
-fastrand
-dirs
-zeroize
-socket2
-pin-project
-tower
-derive_more
-memchr
-toml_edit
-static_assertions
-pretty_assertions
-js-sys
-convert_case
-unicode-width
-pkg-config
-itoa
-colored
-rustc-hash
-darling
-mime
-web-sys
-image
-bytemuck
-which
-sha1
-dashmap
-arrayvec
-fnv
-tonic
-humantime
-libloading
-winapi
-rustc_version
-http-body
-indoc
-num
-home
-serde_urlencoded
-http-body-util
-unicode-segmentation
-num-integer
-webpki-roots
-phf
-futures-channel
-indicatif
-petgraph
-ordered-float
-strsim
-zstd
-console
-encoding_rs
-wasm-bindgen-futures
-urlencoding
-subtle
-crc32fast
-slab
-rustix
-predicates
-spin
-hyper-rustls
-backtrace
-rustversion
-mio
-scopeguard
-proc-macro-error
-hyper-util
-ryu
-prost-types
-textwrap
-memmap2
-zip
-zerocopy
-generic-array
-tar
-pyo3
-async-stream
-quick-xml
-memoffset
-csv
-crossterm
-windows
-num_enum
-tokio-tungstenite
-crossbeam-utils
-async-channel
-lru
-aes
-futures-lite
-tracing-core
-prettyplease
-httparse
-serde_bytes
-tracing-log
-tower-service
-cargo_metadata
-pest
-mime_guess
-tower-http
-data-encoding
-native-tls
-prost-build
-proptest
-derivative
-serial_test
-libm
-half
-futures-io
-bitvec
-rustls-native-certs
-ureq
-object
-anstyle
-tonic-build
-form_urlencoded
-num-derive
-pest_derive
-schemars
-proc-macro-crate
-rstest
-futures-executor
-assert_cmd
-termcolor
-serde_repr
-ctrlc
-sha3
-clap_complete
-flume
-mockall
-ipnet
-aho-corasick
-atty
-signal-hook
-async-std
-filetime
-num-complex
-opentelemetry
-cmake
-arc-swap
-derive_builder
-async-recursion
-dyn-clone
-bumpalo
-fs_extra
-git2
-sysinfo
-shlex
-instant
-approx
-rmp-serde
-rand_distr
-rustls-pki-types
-maplit
-sqlx
-blake3
-hyper-tls
-dotenvy
-jsonwebtoken
-openssl-sys
-crossbeam
-camino
-winreg
-config
-rsa
-bit-vec
-chrono-tz
-async-lock
-bstr

crates/indexed_docs/src/providers/rustdoc/to_markdown.rs 🔗

@@ -1,618 +0,0 @@
-use std::cell::RefCell;
-use std::io::Read;
-use std::rc::Rc;
-
-use anyhow::Result;
-use html_to_markdown::markdown::{
-    HeadingHandler, ListHandler, ParagraphHandler, StyledTextHandler, TableHandler,
-};
-use html_to_markdown::{
-    HandleTag, HandlerOutcome, HtmlElement, MarkdownWriter, StartTagOutcome, TagHandler,
-    convert_html_to_markdown,
-};
-use indexmap::IndexSet;
-use strum::IntoEnumIterator;
-
-use crate::{RustdocItem, RustdocItemKind};
-
-/// Converts the provided rustdoc HTML to Markdown.
-pub fn convert_rustdoc_to_markdown(html: impl Read) -> Result<(String, Vec<RustdocItem>)> {
-    let item_collector = Rc::new(RefCell::new(RustdocItemCollector::new()));
-
-    let mut handlers: Vec<TagHandler> = vec![
-        Rc::new(RefCell::new(ParagraphHandler)),
-        Rc::new(RefCell::new(HeadingHandler)),
-        Rc::new(RefCell::new(ListHandler)),
-        Rc::new(RefCell::new(TableHandler::new())),
-        Rc::new(RefCell::new(StyledTextHandler)),
-        Rc::new(RefCell::new(RustdocChromeRemover)),
-        Rc::new(RefCell::new(RustdocHeadingHandler)),
-        Rc::new(RefCell::new(RustdocCodeHandler)),
-        Rc::new(RefCell::new(RustdocItemHandler)),
-        item_collector.clone(),
-    ];
-
-    let markdown = convert_html_to_markdown(html, &mut handlers)?;
-
-    let items = item_collector
-        .borrow()
-        .items
-        .iter()
-        .cloned()
-        .collect::<Vec<_>>();
-
-    Ok((markdown, items))
-}
-
-pub struct RustdocHeadingHandler;
-
-impl HandleTag for RustdocHeadingHandler {
-    fn should_handle(&self, _tag: &str) -> bool {
-        // We're only handling text, so we don't need to visit any tags.
-        false
-    }
-
-    fn handle_text(&mut self, text: &str, writer: &mut MarkdownWriter) -> HandlerOutcome {
-        if writer.is_inside("h1")
-            || writer.is_inside("h2")
-            || writer.is_inside("h3")
-            || writer.is_inside("h4")
-            || writer.is_inside("h5")
-            || writer.is_inside("h6")
-        {
-            let text = text
-                .trim_matches(|char| char == '\n' || char == '\r')
-                .replace('\n', " ");
-            writer.push_str(&text);
-
-            return HandlerOutcome::Handled;
-        }
-
-        HandlerOutcome::NoOp
-    }
-}
-
-pub struct RustdocCodeHandler;
-
-impl HandleTag for RustdocCodeHandler {
-    fn should_handle(&self, tag: &str) -> bool {
-        matches!(tag, "pre" | "code")
-    }
-
-    fn handle_tag_start(
-        &mut self,
-        tag: &HtmlElement,
-        writer: &mut MarkdownWriter,
-    ) -> StartTagOutcome {
-        match tag.tag() {
-            "code" => {
-                if !writer.is_inside("pre") {
-                    writer.push_str("`");
-                }
-            }
-            "pre" => {
-                let classes = tag.classes();
-                let is_rust = classes.iter().any(|class| class == "rust");
-                let language = is_rust
-                    .then_some("rs")
-                    .or_else(|| {
-                        classes.iter().find_map(|class| {
-                            if let Some((_, language)) = class.split_once("language-") {
-                                Some(language.trim())
-                            } else {
-                                None
-                            }
-                        })
-                    })
-                    .unwrap_or("");
-
-                writer.push_str(&format!("\n\n```{language}\n"));
-            }
-            _ => {}
-        }
-
-        StartTagOutcome::Continue
-    }
-
-    fn handle_tag_end(&mut self, tag: &HtmlElement, writer: &mut MarkdownWriter) {
-        match tag.tag() {
-            "code" => {
-                if !writer.is_inside("pre") {
-                    writer.push_str("`");
-                }
-            }
-            "pre" => writer.push_str("\n```\n"),
-            _ => {}
-        }
-    }
-
-    fn handle_text(&mut self, text: &str, writer: &mut MarkdownWriter) -> HandlerOutcome {
-        if writer.is_inside("pre") {
-            writer.push_str(text);
-            return HandlerOutcome::Handled;
-        }
-
-        HandlerOutcome::NoOp
-    }
-}
-
-const RUSTDOC_ITEM_NAME_CLASS: &str = "item-name";
-
-pub struct RustdocItemHandler;
-
-impl RustdocItemHandler {
-    /// Returns whether we're currently inside of an `.item-name` element, which
-    /// rustdoc uses to display Rust items in a list.
-    fn is_inside_item_name(writer: &MarkdownWriter) -> bool {
-        writer
-            .current_element_stack()
-            .iter()
-            .any(|element| element.has_class(RUSTDOC_ITEM_NAME_CLASS))
-    }
-}
-
-impl HandleTag for RustdocItemHandler {
-    fn should_handle(&self, tag: &str) -> bool {
-        matches!(tag, "div" | "span")
-    }
-
-    fn handle_tag_start(
-        &mut self,
-        tag: &HtmlElement,
-        writer: &mut MarkdownWriter,
-    ) -> StartTagOutcome {
-        match tag.tag() {
-            "div" | "span" => {
-                if Self::is_inside_item_name(writer) && tag.has_class("stab") {
-                    writer.push_str(" [");
-                }
-            }
-            _ => {}
-        }
-
-        StartTagOutcome::Continue
-    }
-
-    fn handle_tag_end(&mut self, tag: &HtmlElement, writer: &mut MarkdownWriter) {
-        match tag.tag() {
-            "div" | "span" => {
-                if tag.has_class(RUSTDOC_ITEM_NAME_CLASS) {
-                    writer.push_str(": ");
-                }
-
-                if Self::is_inside_item_name(writer) && tag.has_class("stab") {
-                    writer.push_str("]");
-                }
-            }
-            _ => {}
-        }
-    }
-
-    fn handle_text(&mut self, text: &str, writer: &mut MarkdownWriter) -> HandlerOutcome {
-        if Self::is_inside_item_name(writer)
-            && !writer.is_inside("span")
-            && !writer.is_inside("code")
-        {
-            writer.push_str(&format!("`{text}`"));
-            return HandlerOutcome::Handled;
-        }
-
-        HandlerOutcome::NoOp
-    }
-}
-
-pub struct RustdocChromeRemover;
-
-impl HandleTag for RustdocChromeRemover {
-    fn should_handle(&self, tag: &str) -> bool {
-        matches!(
-            tag,
-            "head" | "script" | "nav" | "summary" | "button" | "a" | "div" | "span"
-        )
-    }
-
-    fn handle_tag_start(
-        &mut self,
-        tag: &HtmlElement,
-        _writer: &mut MarkdownWriter,
-    ) -> StartTagOutcome {
-        match tag.tag() {
-            "head" | "script" | "nav" => return StartTagOutcome::Skip,
-            "summary" => {
-                if tag.has_class("hideme") {
-                    return StartTagOutcome::Skip;
-                }
-            }
-            "button" => {
-                if tag.attr("id").as_deref() == Some("copy-path") {
-                    return StartTagOutcome::Skip;
-                }
-            }
-            "a" => {
-                if tag.has_any_classes(&["anchor", "doc-anchor", "src"]) {
-                    return StartTagOutcome::Skip;
-                }
-            }
-            "div" | "span" => {
-                if tag.has_any_classes(&["nav-container", "sidebar-elems", "out-of-band"]) {
-                    return StartTagOutcome::Skip;
-                }
-            }
-
-            _ => {}
-        }
-
-        StartTagOutcome::Continue
-    }
-}
-
-pub struct RustdocItemCollector {
-    pub items: IndexSet<RustdocItem>,
-}
-
-impl RustdocItemCollector {
-    pub fn new() -> Self {
-        Self {
-            items: IndexSet::new(),
-        }
-    }
-
-    fn parse_item(tag: &HtmlElement) -> Option<RustdocItem> {
-        if tag.tag() != "a" {
-            return None;
-        }
-
-        let href = tag.attr("href")?;
-        if href.starts_with('#') || href.starts_with("https://") || href.starts_with("../") {
-            return None;
-        }
-
-        for kind in RustdocItemKind::iter() {
-            if tag.has_class(kind.class()) {
-                let mut parts = href.trim_end_matches("/index.html").split('/');
-
-                if let Some(last_component) = parts.next_back() {
-                    let last_component = match last_component.split_once('#') {
-                        Some((component, _fragment)) => component,
-                        None => last_component,
-                    };
-
-                    let name = last_component
-                        .trim_start_matches(&format!("{}.", kind.class()))
-                        .trim_end_matches(".html");
-
-                    return Some(RustdocItem {
-                        kind,
-                        name: name.into(),
-                        path: parts.map(Into::into).collect(),
-                    });
-                }
-            }
-        }
-
-        None
-    }
-}
-
-impl HandleTag for RustdocItemCollector {
-    fn should_handle(&self, tag: &str) -> bool {
-        tag == "a"
-    }
-
-    fn handle_tag_start(
-        &mut self,
-        tag: &HtmlElement,
-        writer: &mut MarkdownWriter,
-    ) -> StartTagOutcome {
-        if tag.tag() == "a" {
-            let is_reexport = writer.current_element_stack().iter().any(|element| {
-                if let Some(id) = element.attr("id") {
-                    id.starts_with("reexport.") || id.starts_with("method.")
-                } else {
-                    false
-                }
-            });
-
-            if !is_reexport {
-                if let Some(item) = Self::parse_item(tag) {
-                    self.items.insert(item);
-                }
-            }
-        }
-
-        StartTagOutcome::Continue
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use html_to_markdown::{TagHandler, convert_html_to_markdown};
-    use indoc::indoc;
-    use pretty_assertions::assert_eq;
-
-    use super::*;
-
-    fn rustdoc_handlers() -> Vec<TagHandler> {
-        vec![
-            Rc::new(RefCell::new(ParagraphHandler)),
-            Rc::new(RefCell::new(HeadingHandler)),
-            Rc::new(RefCell::new(ListHandler)),
-            Rc::new(RefCell::new(TableHandler::new())),
-            Rc::new(RefCell::new(StyledTextHandler)),
-            Rc::new(RefCell::new(RustdocChromeRemover)),
-            Rc::new(RefCell::new(RustdocHeadingHandler)),
-            Rc::new(RefCell::new(RustdocCodeHandler)),
-            Rc::new(RefCell::new(RustdocItemHandler)),
-        ]
-    }
-
-    #[test]
-    fn test_main_heading_buttons_get_removed() {
-        let html = indoc! {r##"
-            <div class="main-heading">
-                <h1>Crate <a class="mod" href="#">serde</a><button id="copy-path" title="Copy item path to clipboard">Copy item path</button></h1>
-                <span class="out-of-band">
-                    <a class="src" href="../src/serde/lib.rs.html#1-340">source</a> · <button id="toggle-all-docs" title="collapse all docs">[<span>−</span>]</button>
-                </span>
-            </div>
-        "##};
-        let expected = indoc! {"
-            # Crate serde
-        "}
-        .trim();
-
-        assert_eq!(
-            convert_html_to_markdown(html.as_bytes(), &mut rustdoc_handlers()).unwrap(),
-            expected
-        )
-    }
-
-    #[test]
-    fn test_single_paragraph() {
-        let html = indoc! {r#"
-            <p>In particular, the last point is what sets <code>axum</code> apart from other frameworks.
-            <code>axum</code> doesn’t have its own middleware system but instead uses
-            <a href="https://docs.rs/tower-service/0.3.2/x86_64-unknown-linux-gnu/tower_service/trait.Service.html" title="trait tower_service::Service"><code>tower::Service</code></a>. This means <code>axum</code> gets timeouts, tracing, compression,
-            authorization, and more, for free. It also enables you to share middleware with
-            applications written using <a href="http://crates.io/crates/hyper"><code>hyper</code></a> or <a href="http://crates.io/crates/tonic"><code>tonic</code></a>.</p>
-        "#};
-        let expected = indoc! {"
-            In particular, the last point is what sets `axum` apart from other frameworks. `axum` doesn’t have its own middleware system but instead uses `tower::Service`. This means `axum` gets timeouts, tracing, compression, authorization, and more, for free. It also enables you to share middleware with applications written using `hyper` or `tonic`.
-        "}
-        .trim();
-
-        assert_eq!(
-            convert_html_to_markdown(html.as_bytes(), &mut rustdoc_handlers()).unwrap(),
-            expected
-        )
-    }
-
-    #[test]
-    fn test_multiple_paragraphs() {
-        let html = indoc! {r##"
-            <h2 id="serde"><a class="doc-anchor" href="#serde">§</a>Serde</h2>
-            <p>Serde is a framework for <em><strong>ser</strong></em>ializing and <em><strong>de</strong></em>serializing Rust data
-            structures efficiently and generically.</p>
-            <p>The Serde ecosystem consists of data structures that know how to serialize
-            and deserialize themselves along with data formats that know how to
-            serialize and deserialize other things. Serde provides the layer by which
-            these two groups interact with each other, allowing any supported data
-            structure to be serialized and deserialized using any supported data format.</p>
-            <p>See the Serde website <a href="https://serde.rs/">https://serde.rs/</a> for additional documentation and
-            usage examples.</p>
-            <h3 id="design"><a class="doc-anchor" href="#design">§</a>Design</h3>
-            <p>Where many other languages rely on runtime reflection for serializing data,
-            Serde is instead built on Rust’s powerful trait system. A data structure
-            that knows how to serialize and deserialize itself is one that implements
-            Serde’s <code>Serialize</code> and <code>Deserialize</code> traits (or uses Serde’s derive
-            attribute to automatically generate implementations at compile time). This
-            avoids any overhead of reflection or runtime type information. In fact in
-            many situations the interaction between data structure and data format can
-            be completely optimized away by the Rust compiler, leaving Serde
-            serialization to perform the same speed as a handwritten serializer for the
-            specific selection of data structure and data format.</p>
-        "##};
-        let expected = indoc! {"
-            ## Serde
-
-            Serde is a framework for _**ser**_ializing and _**de**_serializing Rust data structures efficiently and generically.
-
-            The Serde ecosystem consists of data structures that know how to serialize and deserialize themselves along with data formats that know how to serialize and deserialize other things. Serde provides the layer by which these two groups interact with each other, allowing any supported data structure to be serialized and deserialized using any supported data format.
-
-            See the Serde website https://serde.rs/ for additional documentation and usage examples.
-
-            ### Design
-
-            Where many other languages rely on runtime reflection for serializing data, Serde is instead built on Rust’s powerful trait system. A data structure that knows how to serialize and deserialize itself is one that implements Serde’s `Serialize` and `Deserialize` traits (or uses Serde’s derive attribute to automatically generate implementations at compile time). This avoids any overhead of reflection or runtime type information. In fact in many situations the interaction between data structure and data format can be completely optimized away by the Rust compiler, leaving Serde serialization to perform the same speed as a handwritten serializer for the specific selection of data structure and data format.
-        "}
-        .trim();
-
-        assert_eq!(
-            convert_html_to_markdown(html.as_bytes(), &mut rustdoc_handlers()).unwrap(),
-            expected
-        )
-    }
-
-    #[test]
-    fn test_styled_text() {
-        let html = indoc! {r#"
-            <p>This text is <strong>bolded</strong>.</p>
-            <p>This text is <em>italicized</em>.</p>
-        "#};
-        let expected = indoc! {"
-            This text is **bolded**.
-
-            This text is _italicized_.
-        "}
-        .trim();
-
-        assert_eq!(
-            convert_html_to_markdown(html.as_bytes(), &mut rustdoc_handlers()).unwrap(),
-            expected
-        )
-    }
-
-    #[test]
-    fn test_rust_code_block() {
-        let html = indoc! {r#"
-            <pre class="rust rust-example-rendered"><code><span class="kw">use </span>axum::extract::{Path, Query, Json};
-            <span class="kw">use </span>std::collections::HashMap;
-
-            <span class="comment">// `Path` gives you the path parameters and deserializes them.
-            </span><span class="kw">async fn </span>path(Path(user_id): Path&lt;u32&gt;) {}
-
-            <span class="comment">// `Query` gives you the query parameters and deserializes them.
-            </span><span class="kw">async fn </span>query(Query(params): Query&lt;HashMap&lt;String, String&gt;&gt;) {}
-
-            <span class="comment">// Buffer the request body and deserialize it as JSON into a
-            // `serde_json::Value`. `Json` supports any type that implements
-            // `serde::Deserialize`.
-            </span><span class="kw">async fn </span>json(Json(payload): Json&lt;serde_json::Value&gt;) {}</code></pre>
-        "#};
-        let expected = indoc! {"
-            ```rs
-            use axum::extract::{Path, Query, Json};
-            use std::collections::HashMap;
-
-            // `Path` gives you the path parameters and deserializes them.
-            async fn path(Path(user_id): Path<u32>) {}
-
-            // `Query` gives you the query parameters and deserializes them.
-            async fn query(Query(params): Query<HashMap<String, String>>) {}
-
-            // Buffer the request body and deserialize it as JSON into a
-            // `serde_json::Value`. `Json` supports any type that implements
-            // `serde::Deserialize`.
-            async fn json(Json(payload): Json<serde_json::Value>) {}
-            ```
-        "}
-        .trim();
-
-        assert_eq!(
-            convert_html_to_markdown(html.as_bytes(), &mut rustdoc_handlers()).unwrap(),
-            expected
-        )
-    }
-
-    #[test]
-    fn test_toml_code_block() {
-        let html = indoc! {r##"
-            <h2 id="required-dependencies"><a class="doc-anchor" href="#required-dependencies">§</a>Required dependencies</h2>
-            <p>To use axum there are a few dependencies you have to pull in as well:</p>
-            <div class="example-wrap"><pre class="language-toml"><code>[dependencies]
-            axum = &quot;&lt;latest-version&gt;&quot;
-            tokio = { version = &quot;&lt;latest-version&gt;&quot;, features = [&quot;full&quot;] }
-            tower = &quot;&lt;latest-version&gt;&quot;
-            </code></pre></div>
-        "##};
-        let expected = indoc! {r#"
-            ## Required dependencies
-
-            To use axum there are a few dependencies you have to pull in as well:
-
-            ```toml
-            [dependencies]
-            axum = "<latest-version>"
-            tokio = { version = "<latest-version>", features = ["full"] }
-            tower = "<latest-version>"
-
-            ```
-        "#}
-        .trim();
-
-        assert_eq!(
-            convert_html_to_markdown(html.as_bytes(), &mut rustdoc_handlers()).unwrap(),
-            expected
-        )
-    }
-
-    #[test]
-    fn test_item_table() {
-        let html = indoc! {r##"
-            <h2 id="structs" class="section-header">Structs<a href="#structs" class="anchor">§</a></h2>
-            <ul class="item-table">
-            <li><div class="item-name"><a class="struct" href="struct.Error.html" title="struct axum::Error">Error</a></div><div class="desc docblock-short">Errors that can happen when using axum.</div></li>
-            <li><div class="item-name"><a class="struct" href="struct.Extension.html" title="struct axum::Extension">Extension</a></div><div class="desc docblock-short">Extractor and response for extensions.</div></li>
-            <li><div class="item-name"><a class="struct" href="struct.Form.html" title="struct axum::Form">Form</a><span class="stab portability" title="Available on crate feature `form` only"><code>form</code></span></div><div class="desc docblock-short">URL encoded extractor and response.</div></li>
-            <li><div class="item-name"><a class="struct" href="struct.Json.html" title="struct axum::Json">Json</a><span class="stab portability" title="Available on crate feature `json` only"><code>json</code></span></div><div class="desc docblock-short">JSON Extractor / Response.</div></li>
-            <li><div class="item-name"><a class="struct" href="struct.Router.html" title="struct axum::Router">Router</a></div><div class="desc docblock-short">The router type for composing handlers and services.</div></li></ul>
-            <h2 id="functions" class="section-header">Functions<a href="#functions" class="anchor">§</a></h2>
-            <ul class="item-table">
-            <li><div class="item-name"><a class="fn" href="fn.serve.html" title="fn axum::serve">serve</a><span class="stab portability" title="Available on crate feature `tokio` and (crate features `http1` or `http2`) only"><code>tokio</code> and (<code>http1</code> or <code>http2</code>)</span></div><div class="desc docblock-short">Serve the service with the supplied listener.</div></li>
-            </ul>
-        "##};
-        let expected = indoc! {r#"
-            ## Structs
-
-            - `Error`: Errors that can happen when using axum.
-            - `Extension`: Extractor and response for extensions.
-            - `Form` [`form`]: URL encoded extractor and response.
-            - `Json` [`json`]: JSON Extractor / Response.
-            - `Router`: The router type for composing handlers and services.
-
-            ## Functions
-
-            - `serve` [`tokio` and (`http1` or `http2`)]: Serve the service with the supplied listener.
-        "#}
-        .trim();
-
-        assert_eq!(
-            convert_html_to_markdown(html.as_bytes(), &mut rustdoc_handlers()).unwrap(),
-            expected
-        )
-    }
-
-    #[test]
-    fn test_table() {
-        let html = indoc! {r##"
-            <h2 id="feature-flags"><a class="doc-anchor" href="#feature-flags">§</a>Feature flags</h2>
-            <p>axum uses a set of <a href="https://doc.rust-lang.org/cargo/reference/features.html#the-features-section">feature flags</a> to reduce the amount of compiled and
-            optional dependencies.</p>
-            <p>The following optional features are available:</p>
-            <div><table><thead><tr><th>Name</th><th>Description</th><th>Default?</th></tr></thead><tbody>
-            <tr><td><code>http1</code></td><td>Enables hyper’s <code>http1</code> feature</td><td>Yes</td></tr>
-            <tr><td><code>http2</code></td><td>Enables hyper’s <code>http2</code> feature</td><td>No</td></tr>
-            <tr><td><code>json</code></td><td>Enables the <a href="struct.Json.html" title="struct axum::Json"><code>Json</code></a> type and some similar convenience functionality</td><td>Yes</td></tr>
-            <tr><td><code>macros</code></td><td>Enables optional utility macros</td><td>No</td></tr>
-            <tr><td><code>matched-path</code></td><td>Enables capturing of every request’s router path and the <a href="extract/struct.MatchedPath.html" title="struct axum::extract::MatchedPath"><code>MatchedPath</code></a> extractor</td><td>Yes</td></tr>
-            <tr><td><code>multipart</code></td><td>Enables parsing <code>multipart/form-data</code> requests with <a href="extract/struct.Multipart.html" title="struct axum::extract::Multipart"><code>Multipart</code></a></td><td>No</td></tr>
-            <tr><td><code>original-uri</code></td><td>Enables capturing of every request’s original URI and the <a href="extract/struct.OriginalUri.html" title="struct axum::extract::OriginalUri"><code>OriginalUri</code></a> extractor</td><td>Yes</td></tr>
-            <tr><td><code>tokio</code></td><td>Enables <code>tokio</code> as a dependency and <code>axum::serve</code>, <code>SSE</code> and <code>extract::connect_info</code> types.</td><td>Yes</td></tr>
-            <tr><td><code>tower-log</code></td><td>Enables <code>tower</code>’s <code>log</code> feature</td><td>Yes</td></tr>
-            <tr><td><code>tracing</code></td><td>Log rejections from built-in extractors</td><td>Yes</td></tr>
-            <tr><td><code>ws</code></td><td>Enables WebSockets support via <a href="extract/ws/index.html" title="mod axum::extract::ws"><code>extract::ws</code></a></td><td>No</td></tr>
-            <tr><td><code>form</code></td><td>Enables the <code>Form</code> extractor</td><td>Yes</td></tr>
-            <tr><td><code>query</code></td><td>Enables the <code>Query</code> extractor</td><td>Yes</td></tr>
-            </tbody></table>
-        "##};
-        let expected = indoc! {r#"
-            ## Feature flags
-
-            axum uses a set of feature flags to reduce the amount of compiled and optional dependencies.
-
-            The following optional features are available:
-
-            | Name | Description | Default? |
-            | --- | --- | --- |
-            | `http1` | Enables hyper’s `http1` feature | Yes |
-            | `http2` | Enables hyper’s `http2` feature | No |
-            | `json` | Enables the `Json` type and some similar convenience functionality | Yes |
-            | `macros` | Enables optional utility macros | No |
-            | `matched-path` | Enables capturing of every request’s router path and the `MatchedPath` extractor | Yes |
-            | `multipart` | Enables parsing `multipart/form-data` requests with `Multipart` | No |
-            | `original-uri` | Enables capturing of every request’s original URI and the `OriginalUri` extractor | Yes |
-            | `tokio` | Enables `tokio` as a dependency and `axum::serve`, `SSE` and `extract::connect_info` types. | Yes |
-            | `tower-log` | Enables `tower`’s `log` feature | Yes |
-            | `tracing` | Log rejections from built-in extractors | Yes |
-            | `ws` | Enables WebSockets support via `extract::ws` | No |
-            | `form` | Enables the `Form` extractor | Yes |
-            | `query` | Enables the `Query` extractor | Yes |
-        "#}
-        .trim();
-
-        assert_eq!(
-            convert_html_to_markdown(html.as_bytes(), &mut rustdoc_handlers()).unwrap(),
-            expected
-        )
-    }
-}

crates/indexed_docs/src/registry.rs 🔗

@@ -1,62 +0,0 @@
-use std::sync::Arc;
-
-use collections::HashMap;
-use gpui::{App, BackgroundExecutor, Global, ReadGlobal, UpdateGlobal};
-use parking_lot::RwLock;
-
-use crate::{IndexedDocsProvider, IndexedDocsStore, ProviderId};
-
-struct GlobalIndexedDocsRegistry(Arc<IndexedDocsRegistry>);
-
-impl Global for GlobalIndexedDocsRegistry {}
-
-pub struct IndexedDocsRegistry {
-    executor: BackgroundExecutor,
-    stores_by_provider: RwLock<HashMap<ProviderId, Arc<IndexedDocsStore>>>,
-}
-
-impl IndexedDocsRegistry {
-    pub fn global(cx: &App) -> Arc<Self> {
-        GlobalIndexedDocsRegistry::global(cx).0.clone()
-    }
-
-    pub(crate) fn init_global(cx: &mut App) {
-        GlobalIndexedDocsRegistry::set_global(
-            cx,
-            GlobalIndexedDocsRegistry(Arc::new(Self::new(cx.background_executor().clone()))),
-        );
-    }
-
-    pub fn new(executor: BackgroundExecutor) -> Self {
-        Self {
-            executor,
-            stores_by_provider: RwLock::new(HashMap::default()),
-        }
-    }
-
-    pub fn list_providers(&self) -> Vec<ProviderId> {
-        self.stores_by_provider
-            .read()
-            .keys()
-            .cloned()
-            .collect::<Vec<_>>()
-    }
-
-    pub fn register_provider(
-        &self,
-        provider: Box<dyn IndexedDocsProvider + Send + Sync + 'static>,
-    ) {
-        self.stores_by_provider.write().insert(
-            provider.id(),
-            Arc::new(IndexedDocsStore::new(provider, self.executor.clone())),
-        );
-    }
-
-    pub fn unregister_provider(&self, provider_id: &ProviderId) {
-        self.stores_by_provider.write().remove(provider_id);
-    }
-
-    pub fn get_provider_store(&self, provider_id: ProviderId) -> Option<Arc<IndexedDocsStore>> {
-        self.stores_by_provider.read().get(&provider_id).cloned()
-    }
-}

crates/indexed_docs/src/store.rs 🔗

@@ -1,346 +0,0 @@
-use std::path::PathBuf;
-use std::sync::Arc;
-use std::sync::atomic::AtomicBool;
-
-use anyhow::{Context as _, Result, anyhow};
-use async_trait::async_trait;
-use collections::HashMap;
-use derive_more::{Deref, Display};
-use futures::FutureExt;
-use futures::future::{self, BoxFuture, Shared};
-use fuzzy::StringMatchCandidate;
-use gpui::{App, BackgroundExecutor, Task};
-use heed::Database;
-use heed::types::SerdeBincode;
-use parking_lot::RwLock;
-use serde::{Deserialize, Serialize};
-use util::ResultExt;
-
-use crate::IndexedDocsRegistry;
-
-#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Deref, Display)]
-pub struct ProviderId(pub Arc<str>);
-
-/// The name of a package.
-#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Deref, Display)]
-pub struct PackageName(Arc<str>);
-
-impl From<&str> for PackageName {
-    fn from(value: &str) -> Self {
-        Self(value.into())
-    }
-}
-
-#[async_trait]
-pub trait IndexedDocsProvider {
-    /// Returns the ID of this provider.
-    fn id(&self) -> ProviderId;
-
-    /// Returns the path to the database for this provider.
-    fn database_path(&self) -> PathBuf;
-
-    /// Returns a list of packages as suggestions to be included in the search
-    /// results.
-    ///
-    /// This can be used to provide completions for known packages (e.g., from the
-    /// local project or a registry) before a package has been indexed.
-    async fn suggest_packages(&self) -> Result<Vec<PackageName>>;
-
-    /// Indexes the package with the given name.
-    async fn index(&self, package: PackageName, database: Arc<IndexedDocsDatabase>) -> Result<()>;
-}
-
-/// A store for indexed docs.
-pub struct IndexedDocsStore {
-    executor: BackgroundExecutor,
-    database_future:
-        Shared<BoxFuture<'static, Result<Arc<IndexedDocsDatabase>, Arc<anyhow::Error>>>>,
-    provider: Box<dyn IndexedDocsProvider + Send + Sync + 'static>,
-    indexing_tasks_by_package:
-        RwLock<HashMap<PackageName, Shared<Task<Result<(), Arc<anyhow::Error>>>>>>,
-    latest_errors_by_package: RwLock<HashMap<PackageName, Arc<str>>>,
-}
-
-impl IndexedDocsStore {
-    pub fn try_global(provider: ProviderId, cx: &App) -> Result<Arc<Self>> {
-        let registry = IndexedDocsRegistry::global(cx);
-        registry
-            .get_provider_store(provider.clone())
-            .with_context(|| format!("no indexed docs store found for {provider}"))
-    }
-
-    pub fn new(
-        provider: Box<dyn IndexedDocsProvider + Send + Sync + 'static>,
-        executor: BackgroundExecutor,
-    ) -> Self {
-        let database_future = executor
-            .spawn({
-                let executor = executor.clone();
-                let database_path = provider.database_path();
-                async move { IndexedDocsDatabase::new(database_path, executor) }
-            })
-            .then(|result| future::ready(result.map(Arc::new).map_err(Arc::new)))
-            .boxed()
-            .shared();
-
-        Self {
-            executor,
-            database_future,
-            provider,
-            indexing_tasks_by_package: RwLock::new(HashMap::default()),
-            latest_errors_by_package: RwLock::new(HashMap::default()),
-        }
-    }
-
-    pub fn latest_error_for_package(&self, package: &PackageName) -> Option<Arc<str>> {
-        self.latest_errors_by_package.read().get(package).cloned()
-    }
-
-    /// Returns whether the package with the given name is currently being indexed.
-    pub fn is_indexing(&self, package: &PackageName) -> bool {
-        self.indexing_tasks_by_package.read().contains_key(package)
-    }
-
-    pub async fn load(&self, key: String) -> Result<MarkdownDocs> {
-        self.database_future
-            .clone()
-            .await
-            .map_err(|err| anyhow!(err))?
-            .load(key)
-            .await
-    }
-
-    pub async fn load_many_by_prefix(&self, prefix: String) -> Result<Vec<(String, MarkdownDocs)>> {
-        self.database_future
-            .clone()
-            .await
-            .map_err(|err| anyhow!(err))?
-            .load_many_by_prefix(prefix)
-            .await
-    }
-
-    /// Returns whether any entries exist with the given prefix.
-    pub async fn any_with_prefix(&self, prefix: String) -> Result<bool> {
-        self.database_future
-            .clone()
-            .await
-            .map_err(|err| anyhow!(err))?
-            .any_with_prefix(prefix)
-            .await
-    }
-
-    pub fn suggest_packages(self: Arc<Self>) -> Task<Result<Vec<PackageName>>> {
-        let this = self.clone();
-        self.executor
-            .spawn(async move { this.provider.suggest_packages().await })
-    }
-
-    pub fn index(
-        self: Arc<Self>,
-        package: PackageName,
-    ) -> Shared<Task<Result<(), Arc<anyhow::Error>>>> {
-        if let Some(existing_task) = self.indexing_tasks_by_package.read().get(&package) {
-            return existing_task.clone();
-        }
-
-        let indexing_task = self
-            .executor
-            .spawn({
-                let this = self.clone();
-                let package = package.clone();
-                async move {
-                    let _finally = util::defer({
-                        let this = this.clone();
-                        let package = package.clone();
-                        move || {
-                            this.indexing_tasks_by_package.write().remove(&package);
-                        }
-                    });
-
-                    let index_task = {
-                        let package = package.clone();
-                        async {
-                            let database = this
-                                .database_future
-                                .clone()
-                                .await
-                                .map_err(|err| anyhow!(err))?;
-                            this.provider.index(package, database).await
-                        }
-                    };
-
-                    let result = index_task.await.map_err(Arc::new);
-                    match &result {
-                        Ok(_) => {
-                            this.latest_errors_by_package.write().remove(&package);
-                        }
-                        Err(err) => {
-                            this.latest_errors_by_package
-                                .write()
-                                .insert(package, err.to_string().into());
-                        }
-                    }
-
-                    result
-                }
-            })
-            .shared();
-
-        self.indexing_tasks_by_package
-            .write()
-            .insert(package, indexing_task.clone());
-
-        indexing_task
-    }
-
-    pub fn search(&self, query: String) -> Task<Vec<String>> {
-        let executor = self.executor.clone();
-        let database_future = self.database_future.clone();
-        self.executor.spawn(async move {
-            let Some(database) = database_future.await.map_err(|err| anyhow!(err)).log_err() else {
-                return Vec::new();
-            };
-
-            let Some(items) = database.keys().await.log_err() else {
-                return Vec::new();
-            };
-
-            let candidates = items
-                .iter()
-                .enumerate()
-                .map(|(ix, item_path)| StringMatchCandidate::new(ix, &item_path))
-                .collect::<Vec<_>>();
-
-            let matches = fuzzy::match_strings(
-                &candidates,
-                &query,
-                false,
-                true,
-                100,
-                &AtomicBool::default(),
-                executor,
-            )
-            .await;
-
-            matches
-                .into_iter()
-                .map(|mat| items[mat.candidate_id].clone())
-                .collect()
-        })
-    }
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Display, Serialize, Deserialize)]
-pub struct MarkdownDocs(pub String);
-
-pub struct IndexedDocsDatabase {
-    executor: BackgroundExecutor,
-    env: heed::Env,
-    entries: Database<SerdeBincode<String>, SerdeBincode<MarkdownDocs>>,
-}
-
-impl IndexedDocsDatabase {
-    pub fn new(path: PathBuf, executor: BackgroundExecutor) -> Result<Self> {
-        std::fs::create_dir_all(&path)?;
-
-        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(path)?
-        };
-
-        let mut txn = env.write_txn()?;
-        let entries = env.create_database(&mut txn, Some("rustdoc_entries"))?;
-        txn.commit()?;
-
-        Ok(Self {
-            executor,
-            env,
-            entries,
-        })
-    }
-
-    pub fn keys(&self) -> Task<Result<Vec<String>>> {
-        let env = self.env.clone();
-        let entries = self.entries;
-
-        self.executor.spawn(async move {
-            let txn = env.read_txn()?;
-            let mut iter = entries.iter(&txn)?;
-            let mut keys = Vec::new();
-            while let Some((key, _value)) = iter.next().transpose()? {
-                keys.push(key);
-            }
-
-            Ok(keys)
-        })
-    }
-
-    pub fn load(&self, key: String) -> Task<Result<MarkdownDocs>> {
-        let env = self.env.clone();
-        let entries = self.entries;
-
-        self.executor.spawn(async move {
-            let txn = env.read_txn()?;
-            entries
-                .get(&txn, &key)?
-                .with_context(|| format!("no docs found for {key}"))
-        })
-    }
-
-    pub fn load_many_by_prefix(&self, prefix: String) -> Task<Result<Vec<(String, MarkdownDocs)>>> {
-        let env = self.env.clone();
-        let entries = self.entries;
-
-        self.executor.spawn(async move {
-            let txn = env.read_txn()?;
-            let results = entries
-                .iter(&txn)?
-                .filter_map(|entry| {
-                    let (key, value) = entry.ok()?;
-                    if key.starts_with(&prefix) {
-                        Some((key, value))
-                    } else {
-                        None
-                    }
-                })
-                .collect::<Vec<_>>();
-
-            Ok(results)
-        })
-    }
-
-    /// Returns whether any entries exist with the given prefix.
-    pub fn any_with_prefix(&self, prefix: String) -> Task<Result<bool>> {
-        let env = self.env.clone();
-        let entries = self.entries;
-
-        self.executor.spawn(async move {
-            let txn = env.read_txn()?;
-            let any = entries
-                .iter(&txn)?
-                .any(|entry| entry.map_or(false, |(key, _value)| key.starts_with(&prefix)));
-            Ok(any)
-        })
-    }
-
-    pub fn insert(&self, key: String, docs: String) -> Task<Result<()>> {
-        let env = self.env.clone();
-        let entries = self.entries;
-
-        self.executor.spawn(async move {
-            let mut txn = env.write_txn()?;
-            entries.put(&mut txn, &key, &MarkdownDocs(docs))?;
-            txn.commit()?;
-            Ok(())
-        })
-    }
-}
-
-impl extension::KeyValueStoreDelegate for IndexedDocsDatabase {
-    fn insert(&self, key: String, docs: String) -> Task<Result<()>> {
-        IndexedDocsDatabase::insert(&self, key, docs)
-    }
-}

crates/inspector_ui/src/div_inspector.rs 🔗

@@ -93,8 +93,8 @@ impl DivInspector {
                     Ok((json_style_buffer, rust_style_buffer)) => {
                         this.update_in(cx, |this, window, cx| {
                             this.state = State::BuffersLoaded {
-                                json_style_buffer: json_style_buffer,
-                                rust_style_buffer: rust_style_buffer,
+                                json_style_buffer,
+                                rust_style_buffer,
                             };
 
                             // Initialize editors immediately instead of waiting for
@@ -200,8 +200,8 @@ impl DivInspector {
         cx.subscribe_in(&json_style_editor, window, {
             let id = id.clone();
             let rust_style_buffer = rust_style_buffer.clone();
-            move |this, editor, event: &EditorEvent, window, cx| match event {
-                EditorEvent::BufferEdited => {
+            move |this, editor, event: &EditorEvent, window, cx| {
+                if event == &EditorEvent::BufferEdited {
                     let style_json = editor.read(cx).text(cx);
                     match serde_json_lenient::from_str_lenient::<StyleRefinement>(&style_json) {
                         Ok(new_style) => {
@@ -243,7 +243,6 @@ impl DivInspector {
                         Err(err) => this.json_style_error = Some(err.to_string().into()),
                     }
                 }
-                _ => {}
             }
         })
         .detach();
@@ -251,11 +250,10 @@ impl DivInspector {
         cx.subscribe(&rust_style_editor, {
             let json_style_buffer = json_style_buffer.clone();
             let rust_style_buffer = rust_style_buffer.clone();
-            move |this, _editor, event: &EditorEvent, cx| match event {
-                EditorEvent::BufferEdited => {
+            move |this, _editor, event: &EditorEvent, cx| {
+                if let EditorEvent::BufferEdited = event {
                     this.update_json_style_from_rust(&json_style_buffer, &rust_style_buffer, cx);
                 }
-                _ => {}
             }
         })
         .detach();
@@ -271,23 +269,19 @@ impl DivInspector {
     }
 
     fn reset_style(&mut self, cx: &mut App) {
-        match &self.state {
-            State::Ready {
-                rust_style_buffer,
-                json_style_buffer,
-                ..
-            } => {
-                if let Err(err) = self.reset_style_editors(
-                    &rust_style_buffer.clone(),
-                    &json_style_buffer.clone(),
-                    cx,
-                ) {
-                    self.json_style_error = Some(format!("{err}").into());
-                } else {
-                    self.json_style_error = None;
-                }
+        if let State::Ready {
+            rust_style_buffer,
+            json_style_buffer,
+            ..
+        } = &self.state
+        {
+            if let Err(err) =
+                self.reset_style_editors(&rust_style_buffer.clone(), &json_style_buffer.clone(), cx)
+            {
+                self.json_style_error = Some(format!("{err}").into());
+            } else {
+                self.json_style_error = None;
             }
-            _ => {}
         }
     }
 
@@ -395,11 +389,11 @@ impl DivInspector {
             .zip(self.rust_completion_replace_range.as_ref())
         {
             let before_text = snapshot
-                .text_for_range(0..completion_range.start.to_offset(&snapshot))
+                .text_for_range(0..completion_range.start.to_offset(snapshot))
                 .collect::<String>();
             let after_text = snapshot
                 .text_for_range(
-                    completion_range.end.to_offset(&snapshot)
+                    completion_range.end.to_offset(snapshot)
                         ..snapshot.clip_offset(usize::MAX, Bias::Left),
                 )
                 .collect::<String>();
@@ -702,10 +696,10 @@ impl CompletionProvider for RustStyleCompletionProvider {
 }
 
 fn completion_replace_range(snapshot: &BufferSnapshot, anchor: &Anchor) -> Option<Range<Anchor>> {
-    let point = anchor.to_point(&snapshot);
-    let offset = point.to_offset(&snapshot);
-    let line_start = Point::new(point.row, 0).to_offset(&snapshot);
-    let line_end = Point::new(point.row, snapshot.line_len(point.row)).to_offset(&snapshot);
+    let point = anchor.to_point(snapshot);
+    let offset = point.to_offset(snapshot);
+    let line_start = Point::new(point.row, 0).to_offset(snapshot);
+    let line_end = Point::new(point.row, snapshot.line_len(point.row)).to_offset(snapshot);
     let mut lines = snapshot.text_for_range(line_start..line_end).lines();
     let line = lines.next()?;
 

crates/install_cli/src/install_cli.rs 🔗

@@ -105,7 +105,7 @@ pub fn install_cli(window: &mut Window, cx: &mut Context<Workspace>) {
                 cx,
             )
         })?;
-        register_zed_scheme(&cx).await.log_err();
+        register_zed_scheme(cx).await.log_err();
         Ok(())
     })
     .detach_and_prompt_err("Error installing zed cli", window, cx, |_, _, _| None);

crates/jj/src/jj_repository.rs 🔗

@@ -50,16 +50,13 @@ impl RealJujutsuRepository {
 
 impl JujutsuRepository for RealJujutsuRepository {
     fn list_bookmarks(&self) -> Vec<Bookmark> {
-        let bookmarks = self
-            .repository
+        self.repository
             .view()
             .bookmarks()
             .map(|(ref_name, _target)| Bookmark {
                 ref_name: ref_name.as_str().to_string().into(),
             })
-            .collect();
-
-        bookmarks
+            .collect()
     }
 }
 

crates/jj/src/jj_store.rs 🔗

@@ -16,7 +16,7 @@ pub struct JujutsuStore {
 
 impl JujutsuStore {
     pub fn init_global(cx: &mut App) {
-        let Some(repository) = RealJujutsuRepository::new(&Path::new(".")).ok() else {
+        let Some(repository) = RealJujutsuRepository::new(Path::new(".")).ok() else {
             return;
         };
 

crates/journal/src/journal.rs 🔗

@@ -123,7 +123,7 @@ pub fn new_journal_entry(workspace: &Workspace, window: &mut Window, cx: &mut Ap
     }
 
     let app_state = workspace.app_state().clone();
-    let view_snapshot = workspace.weak_handle().clone();
+    let view_snapshot = workspace.weak_handle();
 
     window
         .spawn(cx, async move |cx| {
@@ -170,23 +170,23 @@ pub fn new_journal_entry(workspace: &Workspace, window: &mut Window, cx: &mut Ap
                     .await
             };
 
-            if let Some(Some(Ok(item))) = opened.first() {
-                if let Some(editor) = item.downcast::<Editor>().map(|editor| editor.downgrade()) {
-                    editor.update_in(cx, |editor, window, cx| {
-                        let len = editor.buffer().read(cx).len(cx);
-                        editor.change_selections(
-                            SelectionEffects::scroll(Autoscroll::center()),
-                            window,
-                            cx,
-                            |s| s.select_ranges([len..len]),
-                        );
-                        if len > 0 {
-                            editor.insert("\n\n", window, cx);
-                        }
-                        editor.insert(&entry_heading, window, cx);
+            if let Some(Some(Ok(item))) = opened.first()
+                && let Some(editor) = item.downcast::<Editor>().map(|editor| editor.downgrade())
+            {
+                editor.update_in(cx, |editor, window, cx| {
+                    let len = editor.buffer().read(cx).len(cx);
+                    editor.change_selections(
+                        SelectionEffects::scroll(Autoscroll::center()),
+                        window,
+                        cx,
+                        |s| s.select_ranges([len..len]),
+                    );
+                    if len > 0 {
                         editor.insert("\n\n", window, cx);
-                    })?;
-                }
+                    }
+                    editor.insert(&entry_heading, window, cx);
+                    editor.insert("\n\n", window, cx);
+                })?;
             }
 
             anyhow::Ok(())
@@ -195,11 +195,9 @@ pub fn new_journal_entry(workspace: &Workspace, window: &mut Window, cx: &mut Ap
 }
 
 fn journal_dir(path: &str) -> Option<PathBuf> {
-    let expanded_journal_dir = shellexpand::full(path) //TODO handle this better
+    shellexpand::full(path) //TODO handle this better
         .ok()
-        .map(|dir| Path::new(&dir.to_string()).to_path_buf().join("journal"));
-
-    expanded_journal_dir
+        .map(|dir| Path::new(&dir.to_string()).to_path_buf().join("journal"))
 }
 
 fn heading_entry(now: NaiveTime, hour_format: &Option<HourFormat>) -> String {

crates/language/src/buffer.rs 🔗

@@ -716,7 +716,7 @@ impl EditPreview {
                     &self.applied_edits_snapshot,
                     &self.syntax_snapshot,
                     None,
-                    &syntax_theme,
+                    syntax_theme,
                 );
             }
 
@@ -727,7 +727,7 @@ impl EditPreview {
                     &current_snapshot.text,
                     &current_snapshot.syntax,
                     Some(deletion_highlight_style),
-                    &syntax_theme,
+                    syntax_theme,
                 );
             }
 
@@ -737,7 +737,7 @@ impl EditPreview {
                     &self.applied_edits_snapshot,
                     &self.syntax_snapshot,
                     Some(insertion_highlight_style),
-                    &syntax_theme,
+                    syntax_theme,
                 );
             }
 
@@ -749,7 +749,7 @@ impl EditPreview {
             &self.applied_edits_snapshot,
             &self.syntax_snapshot,
             None,
-            &syntax_theme,
+            syntax_theme,
         );
 
         highlighted_text.build()
@@ -974,8 +974,6 @@ impl Buffer {
                 TextBuffer::new_normalized(0, buffer_id, Default::default(), text).snapshot();
             let mut syntax = SyntaxMap::new(&text).snapshot();
             if let Some(language) = language.clone() {
-                let text = text.clone();
-                let language = language.clone();
                 let language_registry = language_registry.clone();
                 syntax.reparse(&text, language_registry, language);
             }
@@ -1020,9 +1018,6 @@ impl Buffer {
         let text = TextBuffer::new_normalized(0, buffer_id, Default::default(), text).snapshot();
         let mut syntax = SyntaxMap::new(&text).snapshot();
         if let Some(language) = language.clone() {
-            let text = text.clone();
-            let language = language.clone();
-            let language_registry = language_registry.clone();
             syntax.reparse(&text, language_registry, language);
         }
         BufferSnapshot {
@@ -1128,7 +1123,7 @@ impl Buffer {
         } else {
             ranges.as_slice()
         }
-        .into_iter()
+        .iter()
         .peekable();
 
         let mut edits = Vec::new();
@@ -1158,13 +1153,12 @@ impl Buffer {
             base_buffer.edit(edits, None, cx)
         });
 
-        if let Some(operation) = operation {
-            if let Some(BufferBranchState {
+        if let Some(operation) = operation
+            && let Some(BufferBranchState {
                 merged_operations, ..
             }) = &mut self.branch_state
-            {
-                merged_operations.push(operation);
-            }
+        {
+            merged_operations.push(operation);
         }
     }
 
@@ -1185,11 +1179,11 @@ impl Buffer {
         };
 
         let mut operation_to_undo = None;
-        if let Operation::Buffer(text::Operation::Edit(operation)) = &operation {
-            if let Ok(ix) = merged_operations.binary_search(&operation.timestamp) {
-                merged_operations.remove(ix);
-                operation_to_undo = Some(operation.timestamp);
-            }
+        if let Operation::Buffer(text::Operation::Edit(operation)) = &operation
+            && let Ok(ix) = merged_operations.binary_search(&operation.timestamp)
+        {
+            merged_operations.remove(ix);
+            operation_to_undo = Some(operation.timestamp);
         }
 
         self.apply_ops([operation.clone()], cx);
@@ -1396,7 +1390,8 @@ impl Buffer {
                     is_first = false;
                     return true;
                 }
-                let any_sub_ranges_contain_range = layer
+
+                layer
                     .included_sub_ranges
                     .map(|sub_ranges| {
                         sub_ranges.iter().any(|sub_range| {
@@ -1405,9 +1400,7 @@ impl Buffer {
                             !is_before_start && !is_after_end
                         })
                     })
-                    .unwrap_or(true);
-                let result = any_sub_ranges_contain_range;
-                return result;
+                    .unwrap_or(true)
             })
             .last()
             .map(|info| info.language.clone())
@@ -1424,10 +1417,10 @@ impl Buffer {
             .map(|info| info.language.clone())
             .collect();
 
-        if languages.is_empty() {
-            if let Some(buffer_language) = self.language() {
-                languages.push(buffer_language.clone());
-            }
+        if languages.is_empty()
+            && let Some(buffer_language) = self.language()
+        {
+            languages.push(buffer_language.clone());
         }
 
         languages
@@ -1521,12 +1514,12 @@ impl Buffer {
                     let new_syntax_map = parse_task.await;
                     this.update(cx, move |this, cx| {
                         let grammar_changed =
-                            this.language.as_ref().map_or(true, |current_language| {
+                            this.language.as_ref().is_none_or(|current_language| {
                                 !Arc::ptr_eq(&language, current_language)
                             });
                         let language_registry_changed = new_syntax_map
                             .contains_unknown_injections()
-                            && language_registry.map_or(false, |registry| {
+                            && language_registry.is_some_and(|registry| {
                                 registry.version() != new_syntax_map.language_registry_version()
                             });
                         let parse_again = language_registry_changed
@@ -1571,6 +1564,7 @@ impl Buffer {
             diagnostics: diagnostics.iter().cloned().collect(),
             lamport_timestamp,
         };
+
         self.apply_diagnostic_update(server_id, diagnostics, lamport_timestamp, cx);
         self.send_operation(op, true, cx);
     }
@@ -1719,8 +1713,7 @@ impl Buffer {
                                 })
                                 .with_delta(suggestion.delta, language_indent_size);
 
-                            if old_suggestions.get(&new_row).map_or(
-                                true,
+                            if old_suggestions.get(&new_row).is_none_or(
                                 |(old_indentation, was_within_error)| {
                                     suggested_indent != *old_indentation
                                         && (!suggestion.within_error || *was_within_error)
@@ -2014,7 +2007,7 @@ impl Buffer {
 
     fn was_changed(&mut self) {
         self.change_bits.retain(|change_bit| {
-            change_bit.upgrade().map_or(false, |bit| {
+            change_bit.upgrade().is_some_and(|bit| {
                 bit.replace(true);
                 true
             })
@@ -2191,7 +2184,7 @@ impl Buffer {
         if self
             .remote_selections
             .get(&self.text.replica_id())
-            .map_or(true, |set| !set.selections.is_empty())
+            .is_none_or(|set| !set.selections.is_empty())
         {
             self.set_active_selections(Arc::default(), false, Default::default(), cx);
         }
@@ -2208,7 +2201,7 @@ impl Buffer {
         self.remote_selections.insert(
             AGENT_REPLICA_ID,
             SelectionSet {
-                selections: selections.clone(),
+                selections,
                 lamport_timestamp,
                 line_mode,
                 cursor_shape,
@@ -2270,13 +2263,11 @@ impl Buffer {
             }
             let new_text = new_text.into();
             if !new_text.is_empty() || !range.is_empty() {
-                if let Some((prev_range, prev_text)) = edits.last_mut() {
-                    if prev_range.end >= range.start {
-                        prev_range.end = cmp::max(prev_range.end, range.end);
-                        *prev_text = format!("{prev_text}{new_text}").into();
-                    } else {
-                        edits.push((range, new_text));
-                    }
+                if let Some((prev_range, prev_text)) = edits.last_mut()
+                    && prev_range.end >= range.start
+                {
+                    prev_range.end = cmp::max(prev_range.end, range.end);
+                    *prev_text = format!("{prev_text}{new_text}").into();
                 } else {
                     edits.push((range, new_text));
                 }
@@ -2296,10 +2287,27 @@ impl Buffer {
 
         if let Some((before_edit, mode)) = autoindent_request {
             let mut delta = 0isize;
-            let entries = edits
+            let mut previous_setting = None;
+            let entries: Vec<_> = edits
                 .into_iter()
                 .enumerate()
                 .zip(&edit_operation.as_edit().unwrap().new_text)
+                .filter(|((_, (range, _)), _)| {
+                    let language = before_edit.language_at(range.start);
+                    let language_id = language.map(|l| l.id());
+                    if let Some((cached_language_id, auto_indent)) = previous_setting
+                        && cached_language_id == language_id
+                    {
+                        auto_indent
+                    } else {
+                        // The auto-indent setting is not present in editorconfigs, hence
+                        // we can avoid passing the file here.
+                        let auto_indent =
+                            language_settings(language.map(|l| l.name()), None, cx).auto_indent;
+                        previous_setting = Some((language_id, auto_indent));
+                        auto_indent
+                    }
+                })
                 .map(|((ix, (range, _)), new_text)| {
                     let new_text_length = new_text.len();
                     let old_start = range.start.to_point(&before_edit);
@@ -2373,12 +2381,14 @@ impl Buffer {
                 })
                 .collect();
 
-            self.autoindent_requests.push(Arc::new(AutoindentRequest {
-                before_edit,
-                entries,
-                is_block_mode: matches!(mode, AutoindentMode::Block { .. }),
-                ignore_empty_lines: false,
-            }));
+            if !entries.is_empty() {
+                self.autoindent_requests.push(Arc::new(AutoindentRequest {
+                    before_edit,
+                    entries,
+                    is_block_mode: matches!(mode, AutoindentMode::Block { .. }),
+                    ignore_empty_lines: false,
+                }));
+            }
         }
 
         self.end_transaction(cx);
@@ -2571,10 +2581,10 @@ impl Buffer {
                 line_mode,
                 cursor_shape,
             } => {
-                if let Some(set) = self.remote_selections.get(&lamport_timestamp.replica_id) {
-                    if set.lamport_timestamp > lamport_timestamp {
-                        return;
-                    }
+                if let Some(set) = self.remote_selections.get(&lamport_timestamp.replica_id)
+                    && set.lamport_timestamp > lamport_timestamp
+                {
+                    return;
                 }
 
                 self.remote_selections.insert(
@@ -2600,7 +2610,7 @@ impl Buffer {
                     self.completion_triggers = self
                         .completion_triggers_per_language_server
                         .values()
-                        .flat_map(|triggers| triggers.into_iter().cloned())
+                        .flat_map(|triggers| triggers.iter().cloned())
                         .collect();
                 } else {
                     self.completion_triggers_per_language_server
@@ -2760,7 +2770,7 @@ impl Buffer {
             self.completion_triggers = self
                 .completion_triggers_per_language_server
                 .values()
-                .flat_map(|triggers| triggers.into_iter().cloned())
+                .flat_map(|triggers| triggers.iter().cloned())
                 .collect();
         } else {
             self.completion_triggers_per_language_server
@@ -2822,7 +2832,7 @@ impl Buffer {
         let mut edits: Vec<(Range<usize>, String)> = Vec::new();
         let mut last_end = None;
         for _ in 0..old_range_count {
-            if last_end.map_or(false, |last_end| last_end >= self.len()) {
+            if last_end.is_some_and(|last_end| last_end >= self.len()) {
                 break;
             }
 
@@ -2991,9 +3001,9 @@ impl BufferSnapshot {
         }
 
         let mut error_ranges = Vec::<Range<Point>>::new();
-        let mut matches = self.syntax.matches(range.clone(), &self.text, |grammar| {
-            grammar.error_query.as_ref()
-        });
+        let mut matches = self
+            .syntax
+            .matches(range, &self.text, |grammar| grammar.error_query.as_ref());
         while let Some(mat) = matches.peek() {
             let node = mat.captures[0].node;
             let start = Point::from_ts_point(node.start_position());
@@ -3042,14 +3052,14 @@ impl BufferSnapshot {
                 if config
                     .decrease_indent_pattern
                     .as_ref()
-                    .map_or(false, |regex| regex.is_match(line))
+                    .is_some_and(|regex| regex.is_match(line))
                 {
                     indent_change_rows.push((row, Ordering::Less));
                 }
                 if config
                     .increase_indent_pattern
                     .as_ref()
-                    .map_or(false, |regex| regex.is_match(line))
+                    .is_some_and(|regex| regex.is_match(line))
                 {
                     indent_change_rows.push((row + 1, Ordering::Greater));
                 }
@@ -3065,7 +3075,7 @@ impl BufferSnapshot {
                     }
                 }
                 for rule in &config.decrease_indent_patterns {
-                    if rule.pattern.as_ref().map_or(false, |r| r.is_match(line)) {
+                    if rule.pattern.as_ref().is_some_and(|r| r.is_match(line)) {
                         let row_start_column = self.indent_size_for_line(row).len;
                         let basis_row = rule
                             .valid_after
@@ -3278,8 +3288,7 @@ impl BufferSnapshot {
         range: Range<D>,
     ) -> Option<SyntaxLayer<'_>> {
         let range = range.to_offset(self);
-        return self
-            .syntax
+        self.syntax
             .layers_for_range(range, &self.text, false)
             .max_by(|a, b| {
                 if a.depth != b.depth {
@@ -3289,7 +3298,7 @@ impl BufferSnapshot {
                 } else {
                     a.node().end_byte().cmp(&b.node().end_byte()).reverse()
                 }
-            });
+            })
     }
 
     /// Returns the main [`Language`].
@@ -3347,9 +3356,8 @@ impl BufferSnapshot {
                 }
             }
 
-            if let Some(range) = range {
-                if smallest_range_and_depth.as_ref().map_or(
-                    true,
+            if let Some(range) = range
+                && smallest_range_and_depth.as_ref().is_none_or(
                     |(smallest_range, smallest_range_depth)| {
                         if layer.depth > *smallest_range_depth {
                             true
@@ -3359,13 +3367,13 @@ impl BufferSnapshot {
                             false
                         }
                     },
-                ) {
-                    smallest_range_and_depth = Some((range, layer.depth));
-                    scope = Some(LanguageScope {
-                        language: layer.language.clone(),
-                        override_id: layer.override_id(offset, &self.text),
-                    });
-                }
+                )
+            {
+                smallest_range_and_depth = Some((range, layer.depth));
+                scope = Some(LanguageScope {
+                    language: layer.language.clone(),
+                    override_id: layer.override_id(offset, &self.text),
+                });
             }
         }
 
@@ -3481,17 +3489,17 @@ impl BufferSnapshot {
                 // If there is a candidate node on both sides of the (empty) range, then
                 // decide between the two by favoring a named node over an anonymous token.
                 // If both nodes are the same in that regard, favor the right one.
-                if let Some(right_node) = right_node {
-                    if right_node.is_named() || !left_node.is_named() {
-                        layer_result = right_node;
-                    }
+                if let Some(right_node) = right_node
+                    && (right_node.is_named() || !left_node.is_named())
+                {
+                    layer_result = right_node;
                 }
             }
 
-            if let Some(previous_result) = &result {
-                if previous_result.byte_range().len() < layer_result.byte_range().len() {
-                    continue;
-                }
+            if let Some(previous_result) = &result
+                && previous_result.byte_range().len() < layer_result.byte_range().len()
+            {
+                continue;
             }
             result = Some(layer_result);
         }
@@ -3526,7 +3534,7 @@ impl BufferSnapshot {
             }
         }
 
-        return Some(cursor.node());
+        Some(cursor.node())
     }
 
     /// Returns the outline for the buffer.
@@ -3555,7 +3563,7 @@ impl BufferSnapshot {
         )?;
         let mut prev_depth = None;
         items.retain(|item| {
-            let result = prev_depth.map_or(true, |prev_depth| item.depth > prev_depth);
+            let result = prev_depth.is_none_or(|prev_depth| item.depth > prev_depth);
             prev_depth = Some(item.depth);
             result
         });
@@ -4062,11 +4070,11 @@ impl BufferSnapshot {
         // Get the ranges of the innermost pair of brackets.
         let mut result: Option<(Range<usize>, Range<usize>)> = None;
 
-        for pair in self.enclosing_bracket_ranges(range.clone()) {
-            if let Some(range_filter) = range_filter {
-                if !range_filter(pair.open_range.clone(), pair.close_range.clone()) {
-                    continue;
-                }
+        for pair in self.enclosing_bracket_ranges(range) {
+            if let Some(range_filter) = range_filter
+                && !range_filter(pair.open_range.clone(), pair.close_range.clone())
+            {
+                continue;
             }
 
             let len = pair.close_range.end - pair.open_range.start;
@@ -4235,7 +4243,7 @@ impl BufferSnapshot {
                         .map(|(range, name)| {
                             (
                                 name.to_string(),
-                                self.text_for_range(range.clone()).collect::<String>(),
+                                self.text_for_range(range).collect::<String>(),
                             )
                         })
                         .collect();
@@ -4432,7 +4440,7 @@ impl BufferSnapshot {
 
     pub fn words_in_range(&self, query: WordsQuery) -> BTreeMap<String, Range<Anchor>> {
         let query_str = query.fuzzy_contents;
-        if query_str.map_or(false, |query| query.is_empty()) {
+        if query_str.is_some_and(|query| query.is_empty()) {
             return BTreeMap::default();
         }
 
@@ -4456,27 +4464,26 @@ impl BufferSnapshot {
                         current_word_start_ix = Some(ix);
                     }
 
-                    if let Some(query_chars) = &query_chars {
-                        if query_ix < query_len {
-                            if c.to_lowercase().eq(query_chars[query_ix].to_lowercase()) {
-                                query_ix += 1;
-                            }
-                        }
+                    if let Some(query_chars) = &query_chars
+                        && query_ix < query_len
+                        && c.to_lowercase().eq(query_chars[query_ix].to_lowercase())
+                    {
+                        query_ix += 1;
                     }
                     continue;
-                } else if let Some(word_start) = current_word_start_ix.take() {
-                    if query_ix == query_len {
-                        let word_range = self.anchor_before(word_start)..self.anchor_after(ix);
-                        let mut word_text = self.text_for_range(word_start..ix).peekable();
-                        let first_char = word_text
-                            .peek()
-                            .and_then(|first_chunk| first_chunk.chars().next());
-                        // Skip empty and "words" starting with digits as a heuristic to reduce useless completions
-                        if !query.skip_digits
-                            || first_char.map_or(true, |first_char| !first_char.is_digit(10))
-                        {
-                            words.insert(word_text.collect(), word_range);
-                        }
+                } else if let Some(word_start) = current_word_start_ix.take()
+                    && query_ix == query_len
+                {
+                    let word_range = self.anchor_before(word_start)..self.anchor_after(ix);
+                    let mut word_text = self.text_for_range(word_start..ix).peekable();
+                    let first_char = word_text
+                        .peek()
+                        .and_then(|first_chunk| first_chunk.chars().next());
+                    // Skip empty and "words" starting with digits as a heuristic to reduce useless completions
+                    if !query.skip_digits
+                        || first_char.is_none_or(|first_char| !first_char.is_digit(10))
+                    {
+                        words.insert(word_text.collect(), word_range);
                     }
                 }
                 query_ix = 0;
@@ -4589,17 +4596,17 @@ impl<'a> BufferChunks<'a> {
                 highlights
                     .stack
                     .retain(|(end_offset, _)| *end_offset > range.start);
-                if let Some(capture) = &highlights.next_capture {
-                    if range.start >= capture.node.start_byte() {
-                        let next_capture_end = capture.node.end_byte();
-                        if range.start < next_capture_end {
-                            highlights.stack.push((
-                                next_capture_end,
-                                highlights.highlight_maps[capture.grammar_index].get(capture.index),
-                            ));
-                        }
-                        highlights.next_capture.take();
+                if let Some(capture) = &highlights.next_capture
+                    && range.start >= capture.node.start_byte()
+                {
+                    let next_capture_end = capture.node.end_byte();
+                    if range.start < next_capture_end {
+                        highlights.stack.push((
+                            next_capture_end,
+                            highlights.highlight_maps[capture.grammar_index].get(capture.index),
+                        ));
                     }
+                    highlights.next_capture.take();
                 }
             } else if let Some(snapshot) = self.buffer_snapshot {
                 let (captures, highlight_maps) = snapshot.get_highlights(self.range.clone());
@@ -4624,33 +4631,33 @@ impl<'a> BufferChunks<'a> {
     }
 
     fn initialize_diagnostic_endpoints(&mut self) {
-        if let Some(diagnostics) = self.diagnostic_endpoints.as_mut() {
-            if let Some(buffer) = self.buffer_snapshot {
-                let mut diagnostic_endpoints = Vec::new();
-                for entry in buffer.diagnostics_in_range::<_, usize>(self.range.clone(), false) {
-                    diagnostic_endpoints.push(DiagnosticEndpoint {
-                        offset: entry.range.start,
-                        is_start: true,
-                        severity: entry.diagnostic.severity,
-                        is_unnecessary: entry.diagnostic.is_unnecessary,
-                        underline: entry.diagnostic.underline,
-                    });
-                    diagnostic_endpoints.push(DiagnosticEndpoint {
-                        offset: entry.range.end,
-                        is_start: false,
-                        severity: entry.diagnostic.severity,
-                        is_unnecessary: entry.diagnostic.is_unnecessary,
-                        underline: entry.diagnostic.underline,
-                    });
-                }
-                diagnostic_endpoints
-                    .sort_unstable_by_key(|endpoint| (endpoint.offset, !endpoint.is_start));
-                *diagnostics = diagnostic_endpoints.into_iter().peekable();
-                self.hint_depth = 0;
-                self.error_depth = 0;
-                self.warning_depth = 0;
-                self.information_depth = 0;
+        if let Some(diagnostics) = self.diagnostic_endpoints.as_mut()
+            && let Some(buffer) = self.buffer_snapshot
+        {
+            let mut diagnostic_endpoints = Vec::new();
+            for entry in buffer.diagnostics_in_range::<_, usize>(self.range.clone(), false) {
+                diagnostic_endpoints.push(DiagnosticEndpoint {
+                    offset: entry.range.start,
+                    is_start: true,
+                    severity: entry.diagnostic.severity,
+                    is_unnecessary: entry.diagnostic.is_unnecessary,
+                    underline: entry.diagnostic.underline,
+                });
+                diagnostic_endpoints.push(DiagnosticEndpoint {
+                    offset: entry.range.end,
+                    is_start: false,
+                    severity: entry.diagnostic.severity,
+                    is_unnecessary: entry.diagnostic.is_unnecessary,
+                    underline: entry.diagnostic.underline,
+                });
             }
+            diagnostic_endpoints
+                .sort_unstable_by_key(|endpoint| (endpoint.offset, !endpoint.is_start));
+            *diagnostics = diagnostic_endpoints.into_iter().peekable();
+            self.hint_depth = 0;
+            self.error_depth = 0;
+            self.warning_depth = 0;
+            self.information_depth = 0;
         }
     }
 
@@ -4761,11 +4768,11 @@ impl<'a> Iterator for BufferChunks<'a> {
                 .min(next_capture_start)
                 .min(next_diagnostic_endpoint);
             let mut highlight_id = None;
-            if let Some(highlights) = self.highlights.as_ref() {
-                if let Some((parent_capture_end, parent_highlight_id)) = highlights.stack.last() {
-                    chunk_end = chunk_end.min(*parent_capture_end);
-                    highlight_id = Some(*parent_highlight_id);
-                }
+            if let Some(highlights) = self.highlights.as_ref()
+                && let Some((parent_capture_end, parent_highlight_id)) = highlights.stack.last()
+            {
+                chunk_end = chunk_end.min(*parent_capture_end);
+                highlight_id = Some(*parent_highlight_id);
             }
 
             let slice =
@@ -4959,11 +4966,12 @@ pub(crate) fn contiguous_ranges(
     std::iter::from_fn(move || {
         loop {
             if let Some(value) = values.next() {
-                if let Some(range) = &mut current_range {
-                    if value == range.end && range.len() < max_len {
-                        range.end += 1;
-                        continue;
-                    }
+                if let Some(range) = &mut current_range
+                    && value == range.end
+                    && range.len() < max_len
+                {
+                    range.end += 1;
+                    continue;
                 }
 
                 let prev_range = current_range.clone();
@@ -5031,10 +5039,10 @@ impl CharClassifier {
             } else {
                 scope.word_characters()
             };
-            if let Some(characters) = characters {
-                if characters.contains(&c) {
-                    return CharKind::Word;
-                }
+            if let Some(characters) = characters
+                && characters.contains(&c)
+            {
+                return CharKind::Word;
             }
         }
 

crates/language/src/buffer_tests.rs 🔗

@@ -1744,7 +1744,7 @@ fn test_autoindent_block_mode(cx: &mut App) {
         buffer.edit(
             [(Point::new(2, 8)..Point::new(2, 8), inserted_text)],
             Some(AutoindentMode::Block {
-                original_indent_columns: original_indent_columns.clone(),
+                original_indent_columns,
             }),
             cx,
         );
@@ -1790,9 +1790,9 @@ fn test_autoindent_block_mode_with_newline(cx: &mut App) {
         "#
         .unindent();
         buffer.edit(
-            [(Point::new(2, 0)..Point::new(2, 0), inserted_text.clone())],
+            [(Point::new(2, 0)..Point::new(2, 0), inserted_text)],
             Some(AutoindentMode::Block {
-                original_indent_columns: original_indent_columns.clone(),
+                original_indent_columns,
             }),
             cx,
         );
@@ -1843,7 +1843,7 @@ fn test_autoindent_block_mode_without_original_indent_columns(cx: &mut App) {
         buffer.edit(
             [(Point::new(2, 0)..Point::new(2, 0), inserted_text)],
             Some(AutoindentMode::Block {
-                original_indent_columns: original_indent_columns.clone(),
+                original_indent_columns,
             }),
             cx,
         );
@@ -2030,7 +2030,7 @@ fn test_autoindent_with_injected_languages(cx: &mut App) {
 
     let language_registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
     language_registry.add(html_language.clone());
-    language_registry.add(javascript_language.clone());
+    language_registry.add(javascript_language);
 
     cx.new(|cx| {
         let (text, ranges) = marked_text_ranges(

crates/language/src/language.rs 🔗

@@ -44,6 +44,7 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
 use serde_json::Value;
 use settings::WorktreeId;
 use smol::future::FutureExt as _;
+use std::num::NonZeroU32;
 use std::{
     any::Any,
     ffi::OsStr,
@@ -59,7 +60,6 @@ use std::{
         atomic::{AtomicU64, AtomicUsize, Ordering::SeqCst},
     },
 };
-use std::{num::NonZeroU32, sync::OnceLock};
 use syntax_map::{QueryCursorHandle, SyntaxSnapshot};
 use task::RunnableTag;
 pub use task_context::{ContextLocation, ContextProvider, RunnableRange};
@@ -67,7 +67,9 @@ pub use text_diff::{
     DiffOptions, apply_diff_patch, line_diff, text_diff, text_diff_with_options, unified_diff,
 };
 use theme::SyntaxTheme;
-pub use toolchain::{LanguageToolchainStore, Toolchain, ToolchainList, ToolchainLister};
+pub use toolchain::{
+    LanguageToolchainStore, LocalLanguageToolchainStore, Toolchain, ToolchainList, ToolchainLister,
+};
 use tree_sitter::{self, Query, QueryCursor, WasmStore, wasmtime};
 use util::serde::default_true;
 
@@ -119,8 +121,8 @@ where
     func(cursor.deref_mut())
 }
 
-static NEXT_LANGUAGE_ID: LazyLock<AtomicUsize> = LazyLock::new(Default::default);
-static NEXT_GRAMMAR_ID: LazyLock<AtomicUsize> = LazyLock::new(Default::default);
+static NEXT_LANGUAGE_ID: AtomicUsize = AtomicUsize::new(0);
+static NEXT_GRAMMAR_ID: AtomicUsize = AtomicUsize::new(0);
 static WASM_ENGINE: LazyLock<wasmtime::Engine> = LazyLock::new(|| {
     wasmtime::Engine::new(&wasmtime::Config::new()).expect("Failed to create Wasmtime engine")
 });
@@ -165,7 +167,6 @@ pub struct CachedLspAdapter {
     pub adapter: Arc<dyn LspAdapter>,
     pub reinstall_attempt_count: AtomicU64,
     cached_binary: futures::lock::Mutex<Option<LanguageServerBinary>>,
-    manifest_name: OnceLock<Option<ManifestName>>,
 }
 
 impl Debug for CachedLspAdapter {
@@ -201,18 +202,17 @@ impl CachedLspAdapter {
             adapter,
             cached_binary: Default::default(),
             reinstall_attempt_count: AtomicU64::new(0),
-            manifest_name: Default::default(),
         })
     }
 
     pub fn name(&self) -> LanguageServerName {
-        self.adapter.name().clone()
+        self.adapter.name()
     }
 
     pub async fn get_language_server_command(
         self: Arc<Self>,
         delegate: Arc<dyn LspAdapterDelegate>,
-        toolchains: Arc<dyn LanguageToolchainStore>,
+        toolchains: Option<Toolchain>,
         binary_options: LanguageServerBinaryOptions,
         cx: &mut AsyncApp,
     ) -> Result<LanguageServerBinary> {
@@ -281,21 +281,6 @@ impl CachedLspAdapter {
             .cloned()
             .unwrap_or_else(|| language_name.lsp_id())
     }
-
-    pub fn manifest_name(&self) -> Option<ManifestName> {
-        self.manifest_name
-            .get_or_init(|| self.adapter.manifest_name())
-            .clone()
-    }
-}
-
-/// Determines what gets sent out as a workspace folders content
-#[derive(Clone, Copy, Debug, PartialEq)]
-pub enum WorkspaceFoldersContent {
-    /// Send out a single entry with the root of the workspace.
-    WorktreeRoot,
-    /// Send out a list of subproject roots.
-    SubprojectRoots,
 }
 
 /// [`LspAdapterDelegate`] allows [`LspAdapter]` implementations to interface with the application
@@ -327,7 +312,7 @@ pub trait LspAdapter: 'static + Send + Sync {
     fn get_language_server_command<'a>(
         self: Arc<Self>,
         delegate: Arc<dyn LspAdapterDelegate>,
-        toolchains: Arc<dyn LanguageToolchainStore>,
+        toolchains: Option<Toolchain>,
         binary_options: LanguageServerBinaryOptions,
         mut cached_binary: futures::lock::MutexGuard<'a, Option<LanguageServerBinary>>,
         cx: &'a mut AsyncApp,
@@ -344,9 +329,9 @@ pub trait LspAdapter: 'static + Send + Sync {
             // We only want to cache when we fall back to the global one,
             // because we don't want to download and overwrite our global one
             // for each worktree we might have open.
-            if binary_options.allow_path_lookup {
-                if let Some(binary) = self.check_if_user_installed(delegate.as_ref(), toolchains, cx).await {
-                    log::info!(
+            if binary_options.allow_path_lookup
+                && let Some(binary) = self.check_if_user_installed(delegate.as_ref(), toolchains, cx).await {
+                    log::debug!(
                         "found user-installed language server for {}. path: {:?}, arguments: {:?}",
                         self.name().0,
                         binary.path,
@@ -354,7 +339,6 @@ pub trait LspAdapter: 'static + Send + Sync {
                     );
                     return Ok(binary);
                 }
-            }
 
             anyhow::ensure!(binary_options.allow_binary_download, "downloading language servers disabled");
 
@@ -402,7 +386,7 @@ pub trait LspAdapter: 'static + Send + Sync {
     async fn check_if_user_installed(
         &self,
         _: &dyn LspAdapterDelegate,
-        _: Arc<dyn LanguageToolchainStore>,
+        _: Option<Toolchain>,
         _: &AsyncApp,
     ) -> Option<LanguageServerBinary> {
         None
@@ -535,7 +519,7 @@ pub trait LspAdapter: 'static + Send + Sync {
         self: Arc<Self>,
         _: &dyn Fs,
         _: &Arc<dyn LspAdapterDelegate>,
-        _: Arc<dyn LanguageToolchainStore>,
+        _: Option<Toolchain>,
         _cx: &mut AsyncApp,
     ) -> Result<Value> {
         Ok(serde_json::json!({}))
@@ -555,7 +539,6 @@ pub trait LspAdapter: 'static + Send + Sync {
         _target_language_server_id: LanguageServerName,
         _: &dyn Fs,
         _: &Arc<dyn LspAdapterDelegate>,
-        _: Arc<dyn LanguageToolchainStore>,
         _cx: &mut AsyncApp,
     ) -> Result<Option<Value>> {
         Ok(None)
@@ -587,17 +570,6 @@ pub trait LspAdapter: 'static + Send + Sync {
         Ok(original)
     }
 
-    /// Determines whether a language server supports workspace folders.
-    ///
-    /// And does not trip over itself in the process.
-    fn workspace_folders_content(&self) -> WorkspaceFoldersContent {
-        WorkspaceFoldersContent::SubprojectRoots
-    }
-
-    fn manifest_name(&self) -> Option<ManifestName> {
-        None
-    }
-
     /// Method only implemented by the default JSON language server adapter.
     /// Used to provide dynamic reloading of the JSON schemas used to
     /// provide autocompletion and diagnostics in Zed setting and keybind
@@ -629,7 +601,7 @@ async fn try_fetch_server_binary<L: LspAdapter + 'static + Send + Sync + ?Sized>
     }
 
     let name = adapter.name();
-    log::info!("fetching latest version of language server {:?}", name.0);
+    log::debug!("fetching latest version of language server {:?}", name.0);
     delegate.update_status(name.clone(), BinaryStatus::CheckingForUpdate);
 
     let latest_version = adapter
@@ -640,7 +612,7 @@ async fn try_fetch_server_binary<L: LspAdapter + 'static + Send + Sync + ?Sized>
         .check_if_version_installed(latest_version.as_ref(), &container_dir, delegate.as_ref())
         .await
     {
-        log::info!("language server {:?} is already installed", name.0);
+        log::debug!("language server {:?} is already installed", name.0);
         delegate.update_status(name.clone(), BinaryStatus::None);
         Ok(binary)
     } else {
@@ -991,11 +963,11 @@ where
 
 fn deserialize_regex_vec<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<Regex>, D::Error> {
     let sources = Vec::<String>::deserialize(d)?;
-    let mut regexes = Vec::new();
-    for source in sources {
-        regexes.push(regex::Regex::new(&source).map_err(de::Error::custom)?);
-    }
-    Ok(regexes)
+    sources
+        .into_iter()
+        .map(|source| regex::Regex::new(&source))
+        .collect::<Result<_, _>>()
+        .map_err(de::Error::custom)
 }
 
 fn regex_vec_json_schema(_: &mut SchemaGenerator) -> schemars::Schema {
@@ -1061,12 +1033,10 @@ impl<'de> Deserialize<'de> for BracketPairConfig {
         D: Deserializer<'de>,
     {
         let result = Vec::<BracketPairContent>::deserialize(deserializer)?;
-        let mut brackets = Vec::with_capacity(result.len());
-        let mut disabled_scopes_by_bracket_ix = Vec::with_capacity(result.len());
-        for entry in result {
-            brackets.push(entry.bracket_pair);
-            disabled_scopes_by_bracket_ix.push(entry.not_in);
-        }
+        let (brackets, disabled_scopes_by_bracket_ix) = result
+            .into_iter()
+            .map(|entry| (entry.bracket_pair, entry.not_in))
+            .unzip();
 
         Ok(BracketPairConfig {
             pairs: brackets,
@@ -1108,6 +1078,7 @@ pub struct Language {
     pub(crate) grammar: Option<Arc<Grammar>>,
     pub(crate) context_provider: Option<Arc<dyn ContextProvider>>,
     pub(crate) toolchain: Option<Arc<dyn ToolchainLister>>,
+    pub(crate) manifest_name: Option<ManifestName>,
 }
 
 #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
@@ -1318,6 +1289,7 @@ impl Language {
             }),
             context_provider: None,
             toolchain: None,
+            manifest_name: None,
         }
     }
 
@@ -1331,6 +1303,10 @@ impl Language {
         self
     }
 
+    pub fn with_manifest(mut self, name: Option<ManifestName>) -> Self {
+        self.manifest_name = name;
+        self
+    }
     pub fn with_queries(mut self, queries: LanguageQueries) -> Result<Self> {
         if let Some(query) = queries.highlights {
             self = self
@@ -1400,16 +1376,14 @@ impl Language {
         let grammar = self.grammar_mut().context("cannot mutate grammar")?;
 
         let query = Query::new(&grammar.ts_language, source)?;
-        let mut extra_captures = Vec::with_capacity(query.capture_names().len());
-
-        for name in query.capture_names().iter() {
-            let kind = if *name == "run" {
-                RunnableCapture::Run
-            } else {
-                RunnableCapture::Named(name.to_string().into())
-            };
-            extra_captures.push(kind);
-        }
+        let extra_captures: Vec<_> = query
+            .capture_names()
+            .iter()
+            .map(|&name| match name {
+                "run" => RunnableCapture::Run,
+                name => RunnableCapture::Named(name.to_string().into()),
+            })
+            .collect();
 
         grammar.runnable_config = Some(RunnableConfig {
             extra_captures,
@@ -1539,9 +1513,8 @@ impl Language {
             .map(|ix| {
                 let mut config = BracketsPatternConfig::default();
                 for setting in query.property_settings(ix) {
-                    match setting.key.as_ref() {
-                        "newline.only" => config.newline_only = true,
-                        _ => {}
+                    if setting.key.as_ref() == "newline.only" {
+                        config.newline_only = true
                     }
                 }
                 config
@@ -1764,6 +1737,9 @@ impl Language {
     pub fn name(&self) -> LanguageName {
         self.config.name.clone()
     }
+    pub fn manifest(&self) -> Option<&ManifestName> {
+        self.manifest_name.as_ref()
+    }
 
     pub fn code_fence_block_name(&self) -> Arc<str> {
         self.config
@@ -1798,10 +1774,10 @@ impl Language {
                 BufferChunks::new(text, range, Some((captures, highlight_maps)), false, None)
             {
                 let end_offset = offset + chunk.text.len();
-                if let Some(highlight_id) = chunk.syntax_highlight_id {
-                    if !highlight_id.is_default() {
-                        result.push((offset..end_offset, highlight_id));
-                    }
+                if let Some(highlight_id) = chunk.syntax_highlight_id
+                    && !highlight_id.is_default()
+                {
+                    result.push((offset..end_offset, highlight_id));
                 }
                 offset = end_offset;
             }
@@ -1818,11 +1794,11 @@ impl Language {
     }
 
     pub fn set_theme(&self, theme: &SyntaxTheme) {
-        if let Some(grammar) = self.grammar.as_ref() {
-            if let Some(highlights_query) = &grammar.highlights_query {
-                *grammar.highlight_map.lock() =
-                    HighlightMap::new(highlights_query.capture_names(), theme);
-            }
+        if let Some(grammar) = self.grammar.as_ref()
+            && let Some(highlights_query) = &grammar.highlights_query
+        {
+            *grammar.highlight_map.lock() =
+                HighlightMap::new(highlights_query.capture_names(), theme);
         }
     }
 
@@ -1852,7 +1828,7 @@ impl Language {
 
 impl LanguageScope {
     pub fn path_suffixes(&self) -> &[String] {
-        &self.language.path_suffixes()
+        self.language.path_suffixes()
     }
 
     pub fn language_name(&self) -> LanguageName {
@@ -1942,11 +1918,11 @@ impl LanguageScope {
             .enumerate()
             .map(move |(ix, bracket)| {
                 let mut is_enabled = true;
-                if let Some(next_disabled_ix) = disabled_ids.first() {
-                    if ix == *next_disabled_ix as usize {
-                        disabled_ids = &disabled_ids[1..];
-                        is_enabled = false;
-                    }
+                if let Some(next_disabled_ix) = disabled_ids.first()
+                    && ix == *next_disabled_ix as usize
+                {
+                    disabled_ids = &disabled_ids[1..];
+                    is_enabled = false;
                 }
                 (bracket, is_enabled)
             })
@@ -2209,7 +2185,7 @@ impl LspAdapter for FakeLspAdapter {
     async fn check_if_user_installed(
         &self,
         _: &dyn LspAdapterDelegate,
-        _: Arc<dyn LanguageToolchainStore>,
+        _: Option<Toolchain>,
         _: &AsyncApp,
     ) -> Option<LanguageServerBinary> {
         Some(self.language_server_binary.clone())
@@ -2218,7 +2194,7 @@ impl LspAdapter for FakeLspAdapter {
     fn get_language_server_command<'a>(
         self: Arc<Self>,
         _: Arc<dyn LspAdapterDelegate>,
-        _: Arc<dyn LanguageToolchainStore>,
+        _: Option<Toolchain>,
         _: LanguageServerBinaryOptions,
         _: futures::lock::MutexGuard<'a, Option<LanguageServerBinary>>,
         _: &'a mut AsyncApp,

crates/language/src/language_registry.rs 🔗

@@ -1,6 +1,6 @@
 use crate::{
     CachedLspAdapter, File, Language, LanguageConfig, LanguageId, LanguageMatcher,
-    LanguageServerName, LspAdapter, PLAIN_TEXT, ToolchainLister,
+    LanguageServerName, LspAdapter, ManifestName, PLAIN_TEXT, ToolchainLister,
     language_settings::{
         AllLanguageSettingsContent, LanguageSettingsContent, all_language_settings,
     },
@@ -49,7 +49,7 @@ impl LanguageName {
     pub fn from_proto(s: String) -> Self {
         Self(SharedString::from(s))
     }
-    pub fn to_proto(self) -> String {
+    pub fn to_proto(&self) -> String {
         self.0.to_string()
     }
     pub fn lsp_id(&self) -> String {
@@ -172,6 +172,7 @@ pub struct AvailableLanguage {
     hidden: bool,
     load: Arc<dyn Fn() -> Result<LoadedLanguage> + 'static + Send + Sync>,
     loaded: bool,
+    manifest_name: Option<ManifestName>,
 }
 
 impl AvailableLanguage {
@@ -259,6 +260,7 @@ pub struct LoadedLanguage {
     pub queries: LanguageQueries,
     pub context_provider: Option<Arc<dyn ContextProvider>>,
     pub toolchain_provider: Option<Arc<dyn ToolchainLister>>,
+    pub manifest_name: Option<ManifestName>,
 }
 
 impl LanguageRegistry {
@@ -349,12 +351,14 @@ impl LanguageRegistry {
             config.grammar.clone(),
             config.matcher.clone(),
             config.hidden,
+            None,
             Arc::new(move || {
                 Ok(LoadedLanguage {
                     config: config.clone(),
                     queries: Default::default(),
                     toolchain_provider: None,
                     context_provider: None,
+                    manifest_name: None,
                 })
             }),
         )
@@ -428,7 +432,7 @@ impl LanguageRegistry {
             let mut state = self.state.write();
             state
                 .lsp_adapters
-                .entry(language_name.clone())
+                .entry(language_name)
                 .or_default()
                 .push(adapter.clone());
             state.all_lsp_adapters.insert(adapter.name(), adapter);
@@ -450,7 +454,7 @@ impl LanguageRegistry {
         let cached_adapter = CachedLspAdapter::new(Arc::new(adapter));
         state
             .lsp_adapters
-            .entry(language_name.clone())
+            .entry(language_name)
             .or_default()
             .push(cached_adapter.clone());
         state
@@ -487,6 +491,7 @@ impl LanguageRegistry {
         grammar_name: Option<Arc<str>>,
         matcher: LanguageMatcher,
         hidden: bool,
+        manifest_name: Option<ManifestName>,
         load: Arc<dyn Fn() -> Result<LoadedLanguage> + 'static + Send + Sync>,
     ) {
         let state = &mut *self.state.write();
@@ -496,6 +501,7 @@ impl LanguageRegistry {
                 existing_language.grammar = grammar_name;
                 existing_language.matcher = matcher;
                 existing_language.load = load;
+                existing_language.manifest_name = manifest_name;
                 return;
             }
         }
@@ -508,6 +514,7 @@ impl LanguageRegistry {
             load,
             hidden,
             loaded: false,
+            manifest_name,
         });
         state.version += 1;
         state.reload_count += 1;
@@ -575,6 +582,7 @@ impl LanguageRegistry {
             grammar: language.config.grammar.clone(),
             matcher: language.config.matcher.clone(),
             hidden: language.config.hidden,
+            manifest_name: None,
             load: Arc::new(|| Err(anyhow!("already loaded"))),
             loaded: true,
         });
@@ -765,7 +773,7 @@ impl LanguageRegistry {
             };
 
             let content_matches = || {
-                config.first_line_pattern.as_ref().map_or(false, |pattern| {
+                config.first_line_pattern.as_ref().is_some_and(|pattern| {
                     content
                         .as_ref()
                         .is_some_and(|content| pattern.is_match(content))
@@ -914,10 +922,12 @@ impl LanguageRegistry {
                                 Language::new_with_id(id, loaded_language.config, grammar)
                                     .with_context_provider(loaded_language.context_provider)
                                     .with_toolchain_lister(loaded_language.toolchain_provider)
+                                    .with_manifest(loaded_language.manifest_name)
                                     .with_queries(loaded_language.queries)
                             } else {
                                 Ok(Language::new_with_id(id, loaded_language.config, None)
                                     .with_context_provider(loaded_language.context_provider)
+                                    .with_manifest(loaded_language.manifest_name)
                                     .with_toolchain_lister(loaded_language.toolchain_provider))
                             }
                         }
@@ -1092,7 +1102,7 @@ impl LanguageRegistry {
         use gpui::AppContext as _;
 
         let mut state = self.state.write();
-        let fake_entry = state.fake_server_entries.get_mut(&name)?;
+        let fake_entry = state.fake_server_entries.get_mut(name)?;
         let (server, mut fake_server) = lsp::FakeLanguageServer::new(
             server_id,
             binary,
@@ -1157,8 +1167,7 @@ impl LanguageRegistryState {
                 soft_wrap: language.config.soft_wrap,
                 auto_indent_on_paste: language.config.auto_indent_on_paste,
                 ..Default::default()
-            }
-            .clone(),
+            },
         );
         self.languages.push(language);
         self.version += 1;

crates/language/src/language_settings.rs 🔗

@@ -5,7 +5,7 @@ use anyhow::Result;
 use collections::{FxHashMap, HashMap, HashSet};
 use ec4rs::{
     Properties as EditorconfigProperties,
-    property::{FinalNewline, IndentSize, IndentStyle, TabWidth, TrimTrailingWs},
+    property::{FinalNewline, IndentSize, IndentStyle, MaxLineLen, TabWidth, TrimTrailingWs},
 };
 use globset::{Glob, GlobMatcher, GlobSet, GlobSetBuilder};
 use gpui::{App, Modifiers};
@@ -133,6 +133,8 @@ pub struct LanguageSettings {
     /// Whether to use additional LSP queries to format (and amend) the code after
     /// every "trigger" symbol input, defined by LSP server capabilities.
     pub use_on_type_format: bool,
+    /// Whether indentation should be adjusted based on the context whilst typing.
+    pub auto_indent: bool,
     /// Whether indentation of pasted content should be adjusted based on the context.
     pub auto_indent_on_paste: bool,
     /// Controls how the editor handles the autoclosed characters.
@@ -185,8 +187,8 @@ impl LanguageSettings {
         let rest = available_language_servers
             .iter()
             .filter(|&available_language_server| {
-                !disabled_language_servers.contains(&available_language_server)
-                    && !enabled_language_servers.contains(&available_language_server)
+                !disabled_language_servers.contains(available_language_server)
+                    && !enabled_language_servers.contains(available_language_server)
             })
             .cloned()
             .collect::<Vec<_>>();
@@ -197,7 +199,7 @@ impl LanguageSettings {
                 if language_server.0.as_ref() == Self::REST_OF_LANGUAGE_SERVERS {
                     rest.clone()
                 } else {
-                    vec![language_server.clone()]
+                    vec![language_server]
                 }
             })
             .collect::<Vec<_>>()
@@ -251,7 +253,7 @@ impl EditPredictionSettings {
         !self.disabled_globs.iter().any(|glob| {
             if glob.is_absolute {
                 file.as_local()
-                    .map_or(false, |local| glob.matcher.is_match(local.abs_path(cx)))
+                    .is_some_and(|local| glob.matcher.is_match(local.abs_path(cx)))
             } else {
                 glob.matcher.is_match(file.path())
             }
@@ -561,6 +563,10 @@ pub struct LanguageSettingsContent {
     ///
     /// Default: true
     pub linked_edits: Option<bool>,
+    /// Whether indentation should be adjusted based on the context whilst typing.
+    ///
+    /// Default: true
+    pub auto_indent: Option<bool>,
     /// Whether indentation of pasted content should be adjusted based on the context.
     ///
     /// Default: true
@@ -987,7 +993,7 @@ pub struct InlayHintSettings {
     /// Default: false
     #[serde(default)]
     pub enabled: bool,
-    /// Global switch to toggle inline values on and off.
+    /// Global switch to toggle inline values on and off when debugging.
     ///
     /// Default: true
     #[serde(default = "default_true")]
@@ -1125,6 +1131,10 @@ impl AllLanguageSettings {
 }
 
 fn merge_with_editorconfig(settings: &mut LanguageSettings, cfg: &EditorconfigProperties) {
+    let preferred_line_length = cfg.get::<MaxLineLen>().ok().and_then(|v| match v {
+        MaxLineLen::Value(u) => Some(u as u32),
+        MaxLineLen::Off => None,
+    });
     let tab_size = cfg.get::<IndentSize>().ok().and_then(|v| match v {
         IndentSize::Value(u) => NonZeroU32::new(u as u32),
         IndentSize::UseTabWidth => cfg.get::<TabWidth>().ok().and_then(|w| match w {
@@ -1152,6 +1162,7 @@ fn merge_with_editorconfig(settings: &mut LanguageSettings, cfg: &EditorconfigPr
             *target = value;
         }
     }
+    merge(&mut settings.preferred_line_length, preferred_line_length);
     merge(&mut settings.tab_size, tab_size);
     merge(&mut settings.hard_tabs, hard_tabs);
     merge(
@@ -1517,6 +1528,7 @@ fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent
     merge(&mut settings.use_autoclose, src.use_autoclose);
     merge(&mut settings.use_auto_surround, src.use_auto_surround);
     merge(&mut settings.use_on_type_format, src.use_on_type_format);
+    merge(&mut settings.auto_indent, src.auto_indent);
     merge(&mut settings.auto_indent_on_paste, src.auto_indent_on_paste);
     merge(
         &mut settings.always_treat_brackets_as_autoclosed,
@@ -1786,7 +1798,7 @@ mod tests {
         assert!(!settings.enabled_for_file(&dot_env_file, &cx));
 
         // Test tilde expansion
-        let home = shellexpand::tilde("~").into_owned().to_string();
+        let home = shellexpand::tilde("~").into_owned();
         let home_file = make_test_file(&[&home, "test.rs"]);
         let settings = build_settings(&["~/test.rs"]);
         assert!(!settings.enabled_for_file(&home_file, &cx));

crates/language/src/manifest.rs 🔗

@@ -12,6 +12,12 @@ impl Borrow<SharedString> for ManifestName {
     }
 }
 
+impl Borrow<str> for ManifestName {
+    fn borrow(&self) -> &str {
+        &self.0
+    }
+}
+
 impl From<SharedString> for ManifestName {
     fn from(value: SharedString) -> Self {
         Self(value)

crates/language/src/proto.rs 🔗

@@ -86,7 +86,7 @@ pub fn serialize_operation(operation: &crate::Operation) -> proto::Operation {
                 proto::operation::UpdateCompletionTriggers {
                     replica_id: lamport_timestamp.replica_id as u32,
                     lamport_timestamp: lamport_timestamp.value,
-                    triggers: triggers.iter().cloned().collect(),
+                    triggers: triggers.clone(),
                     language_server_id: server_id.to_proto(),
                 },
             ),
@@ -385,12 +385,10 @@ pub fn deserialize_undo_map_entry(
 
 /// Deserializes selections from the RPC representation.
 pub fn deserialize_selections(selections: Vec<proto::Selection>) -> Arc<[Selection<Anchor>]> {
-    Arc::from(
-        selections
-            .into_iter()
-            .filter_map(deserialize_selection)
-            .collect::<Vec<_>>(),
-    )
+    selections
+        .into_iter()
+        .filter_map(deserialize_selection)
+        .collect()
 }
 
 /// Deserializes a [`Selection`] from the RPC representation.

crates/language/src/syntax_map.rs 🔗

@@ -414,42 +414,42 @@ impl SyntaxSnapshot {
             .collect::<Vec<_>>();
         self.reparse_with_ranges(text, root_language.clone(), edit_ranges, registry.as_ref());
 
-        if let Some(registry) = registry {
-            if registry.version() != self.language_registry_version {
-                let mut resolved_injection_ranges = Vec::new();
-                let mut cursor = self
-                    .layers
-                    .filter::<_, ()>(text, |summary| summary.contains_unknown_injections);
-                cursor.next();
-                while let Some(layer) = cursor.item() {
-                    let SyntaxLayerContent::Pending { language_name } = &layer.content else {
-                        unreachable!()
-                    };
-                    if registry
-                        .language_for_name_or_extension(language_name)
-                        .now_or_never()
-                        .and_then(|language| language.ok())
-                        .is_some()
-                    {
-                        let range = layer.range.to_offset(text);
-                        log::trace!("reparse range {range:?} for language {language_name:?}");
-                        resolved_injection_ranges.push(range);
-                    }
-
-                    cursor.next();
-                }
-                drop(cursor);
-
-                if !resolved_injection_ranges.is_empty() {
-                    self.reparse_with_ranges(
-                        text,
-                        root_language,
-                        resolved_injection_ranges,
-                        Some(&registry),
-                    );
+        if let Some(registry) = registry
+            && registry.version() != self.language_registry_version
+        {
+            let mut resolved_injection_ranges = Vec::new();
+            let mut cursor = self
+                .layers
+                .filter::<_, ()>(text, |summary| summary.contains_unknown_injections);
+            cursor.next();
+            while let Some(layer) = cursor.item() {
+                let SyntaxLayerContent::Pending { language_name } = &layer.content else {
+                    unreachable!()
+                };
+                if registry
+                    .language_for_name_or_extension(language_name)
+                    .now_or_never()
+                    .and_then(|language| language.ok())
+                    .is_some()
+                {
+                    let range = layer.range.to_offset(text);
+                    log::trace!("reparse range {range:?} for language {language_name:?}");
+                    resolved_injection_ranges.push(range);
                 }
-                self.language_registry_version = registry.version();
+
+                cursor.next();
             }
+            drop(cursor);
+
+            if !resolved_injection_ranges.is_empty() {
+                self.reparse_with_ranges(
+                    text,
+                    root_language,
+                    resolved_injection_ranges,
+                    Some(&registry),
+                );
+            }
+            self.language_registry_version = registry.version();
         }
 
         self.update_count += 1;
@@ -832,7 +832,7 @@ impl SyntaxSnapshot {
         query: fn(&Grammar) -> Option<&Query>,
     ) -> SyntaxMapCaptures<'a> {
         SyntaxMapCaptures::new(
-            range.clone(),
+            range,
             text,
             [SyntaxLayer {
                 language,
@@ -1065,10 +1065,10 @@ impl<'a> SyntaxMapCaptures<'a> {
     pub fn set_byte_range(&mut self, range: Range<usize>) {
         for layer in &mut self.layers {
             layer.captures.set_byte_range(range.clone());
-            if let Some(capture) = &layer.next_capture {
-                if capture.node.end_byte() > range.start {
-                    continue;
-                }
+            if let Some(capture) = &layer.next_capture
+                && capture.node.end_byte() > range.start
+            {
+                continue;
             }
             layer.advance();
         }
@@ -1277,11 +1277,11 @@ fn join_ranges(
             (None, None) => break,
         };
 
-        if let Some(last) = result.last_mut() {
-            if range.start <= last.end {
-                last.end = last.end.max(range.end);
-                continue;
-            }
+        if let Some(last) = result.last_mut()
+            && range.start <= last.end
+        {
+            last.end = last.end.max(range.end);
+            continue;
         }
         result.push(range);
     }
@@ -1297,7 +1297,7 @@ fn parse_text(
 ) -> anyhow::Result<Tree> {
     with_parser(|parser| {
         let mut chunks = text.chunks_in_range(start_byte..text.len());
-        parser.set_included_ranges(&ranges)?;
+        parser.set_included_ranges(ranges)?;
         parser.set_language(&grammar.ts_language)?;
         parser
             .parse_with_options(
@@ -1330,14 +1330,13 @@ fn get_injections(
     // if there currently no matches for that injection.
     combined_injection_ranges.clear();
     for pattern in &config.patterns {
-        if let (Some(language_name), true) = (pattern.language.as_ref(), pattern.combined) {
-            if let Some(language) = language_registry
+        if let (Some(language_name), true) = (pattern.language.as_ref(), pattern.combined)
+            && let Some(language) = language_registry
                 .language_for_name_or_extension(language_name)
                 .now_or_never()
                 .and_then(|language| language.ok())
-            {
-                combined_injection_ranges.insert(language.id, (language, Vec::new()));
-            }
+        {
+            combined_injection_ranges.insert(language.id, (language, Vec::new()));
         }
     }
 
@@ -1357,10 +1356,11 @@ fn get_injections(
                 content_ranges.first().unwrap().start_byte..content_ranges.last().unwrap().end_byte;
 
             // Avoid duplicate matches if two changed ranges intersect the same injection.
-            if let Some((prev_pattern_ix, prev_range)) = &prev_match {
-                if mat.pattern_index == *prev_pattern_ix && content_range == *prev_range {
-                    continue;
-                }
+            if let Some((prev_pattern_ix, prev_range)) = &prev_match
+                && mat.pattern_index == *prev_pattern_ix
+                && content_range == *prev_range
+            {
+                continue;
             }
 
             prev_match = Some((mat.pattern_index, content_range.clone()));
@@ -1630,10 +1630,8 @@ impl<'a> SyntaxLayer<'a> {
                     if offset < range.start || offset > range.end {
                         continue;
                     }
-                } else {
-                    if offset <= range.start || offset >= range.end {
-                        continue;
-                    }
+                } else if offset <= range.start || offset >= range.end {
+                    continue;
                 }
 
                 if let Some((_, smallest_range)) = &smallest_match {

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

@@ -58,8 +58,7 @@ fn test_splice_included_ranges() {
     assert_eq!(change, 0..1);
 
     // does not create overlapping ranges
-    let (new_ranges, change) =
-        splice_included_ranges(ranges.clone(), &[0..18], &[ts_range(20..32)]);
+    let (new_ranges, change) = splice_included_ranges(ranges, &[0..18], &[ts_range(20..32)]);
     assert_eq!(
         new_ranges,
         &[ts_range(20..32), ts_range(50..60), ts_range(80..90)]
@@ -104,7 +103,7 @@ fn test_syntax_map_layers_for_range(cx: &mut App) {
     );
 
     let mut syntax_map = SyntaxMap::new(&buffer);
-    syntax_map.set_language_registry(registry.clone());
+    syntax_map.set_language_registry(registry);
     syntax_map.reparse(language.clone(), &buffer);
 
     assert_layers_for_range(
@@ -165,7 +164,7 @@ fn test_syntax_map_layers_for_range(cx: &mut App) {
     // Put the vec! macro back, adding back the syntactic layer.
     buffer.undo();
     syntax_map.interpolate(&buffer);
-    syntax_map.reparse(language.clone(), &buffer);
+    syntax_map.reparse(language, &buffer);
 
     assert_layers_for_range(
         &syntax_map,
@@ -252,8 +251,8 @@ fn test_dynamic_language_injection(cx: &mut App) {
     assert!(syntax_map.contains_unknown_injections());
 
     registry.add(Arc::new(html_lang()));
-    syntax_map.reparse(markdown.clone(), &buffer);
-    syntax_map.reparse(markdown_inline.clone(), &buffer);
+    syntax_map.reparse(markdown, &buffer);
+    syntax_map.reparse(markdown_inline, &buffer);
     assert_layers_for_range(
         &syntax_map,
         &buffer,
@@ -862,7 +861,7 @@ fn test_syntax_map_languages_loading_with_erb(cx: &mut App) {
     log::info!("editing");
     buffer.edit_via_marked_text(&text);
     syntax_map.interpolate(&buffer);
-    syntax_map.reparse(language.clone(), &buffer);
+    syntax_map.reparse(language, &buffer);
 
     assert_capture_ranges(
         &syntax_map,
@@ -986,7 +985,7 @@ fn test_random_edits(
     syntax_map.reparse(language.clone(), &buffer);
 
     let mut reference_syntax_map = SyntaxMap::new(&buffer);
-    reference_syntax_map.set_language_registry(registry.clone());
+    reference_syntax_map.set_language_registry(registry);
 
     log::info!("initial text:\n{}", buffer.text());
 

crates/language/src/text_diff.rs 🔗

@@ -88,11 +88,11 @@ pub fn text_diff_with_options(
                 let new_offset = new_byte_range.start;
                 hunk_input.clear();
                 hunk_input.update_before(tokenize(
-                    &old_text[old_byte_range.clone()],
+                    &old_text[old_byte_range],
                     options.language_scope.clone(),
                 ));
                 hunk_input.update_after(tokenize(
-                    &new_text[new_byte_range.clone()],
+                    &new_text[new_byte_range],
                     options.language_scope.clone(),
                 ));
                 diff_internal(&hunk_input, |old_byte_range, new_byte_range, _, _| {
@@ -103,7 +103,7 @@ pub fn text_diff_with_options(
                     let replacement_text = if new_byte_range.is_empty() {
                         empty.clone()
                     } else {
-                        new_text[new_byte_range.clone()].into()
+                        new_text[new_byte_range].into()
                     };
                     edits.push((old_byte_range, replacement_text));
                 });
@@ -111,9 +111,9 @@ pub fn text_diff_with_options(
                 let replacement_text = if new_byte_range.is_empty() {
                     empty.clone()
                 } else {
-                    new_text[new_byte_range.clone()].into()
+                    new_text[new_byte_range].into()
                 };
-                edits.push((old_byte_range.clone(), replacement_text));
+                edits.push((old_byte_range, replacement_text));
             }
         },
     );
@@ -154,19 +154,19 @@ fn diff_internal(
         input,
         |old_tokens: Range<u32>, new_tokens: Range<u32>| {
             old_offset += token_len(
-                &input,
+                input,
                 &input.before[old_token_ix as usize..old_tokens.start as usize],
             );
             new_offset += token_len(
-                &input,
+                input,
                 &input.after[new_token_ix as usize..new_tokens.start as usize],
             );
             let old_len = token_len(
-                &input,
+                input,
                 &input.before[old_tokens.start as usize..old_tokens.end as usize],
             );
             let new_len = token_len(
-                &input,
+                input,
                 &input.after[new_tokens.start as usize..new_tokens.end as usize],
             );
             let old_byte_range = old_offset..old_offset + old_len;
@@ -186,14 +186,14 @@ fn tokenize(text: &str, language_scope: Option<LanguageScope>) -> impl Iterator<
     let mut prev = None;
     let mut start_ix = 0;
     iter::from_fn(move || {
-        while let Some((ix, c)) = chars.next() {
+        for (ix, c) in chars.by_ref() {
             let mut token = None;
             let kind = classifier.kind(c);
-            if let Some((prev_char, prev_kind)) = prev {
-                if kind != prev_kind || (kind == CharKind::Punctuation && c != prev_char) {
-                    token = Some(&text[start_ix..ix]);
-                    start_ix = ix;
-                }
+            if let Some((prev_char, prev_kind)) = prev
+                && (kind != prev_kind || (kind == CharKind::Punctuation && c != prev_char))
+            {
+                token = Some(&text[start_ix..ix]);
+                start_ix = ix;
             }
             prev = Some((c, kind));
             if token.is_some() {

crates/language/src/toolchain.rs 🔗

@@ -17,7 +17,7 @@ use settings::WorktreeId;
 use crate::{LanguageName, ManifestName};
 
 /// Represents a single toolchain.
-#[derive(Clone, Debug)]
+#[derive(Clone, Debug, Eq)]
 pub struct Toolchain {
     /// User-facing label
     pub name: SharedString,
@@ -27,6 +27,14 @@ pub struct Toolchain {
     pub as_json: serde_json::Value,
 }
 
+impl std::hash::Hash for Toolchain {
+    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+        self.name.hash(state);
+        self.path.hash(state);
+        self.language_name.hash(state);
+    }
+}
+
 impl PartialEq for Toolchain {
     fn eq(&self, other: &Self) -> bool {
         // Do not use as_json for comparisons; it shouldn't impact equality, as it's not user-surfaced.
@@ -64,6 +72,29 @@ pub trait LanguageToolchainStore: Send + Sync + 'static {
     ) -> Option<Toolchain>;
 }
 
+pub trait LocalLanguageToolchainStore: Send + Sync + 'static {
+    fn active_toolchain(
+        self: Arc<Self>,
+        worktree_id: WorktreeId,
+        relative_path: &Arc<Path>,
+        language_name: LanguageName,
+        cx: &mut AsyncApp,
+    ) -> Option<Toolchain>;
+}
+
+#[async_trait(?Send )]
+impl<T: LocalLanguageToolchainStore> LanguageToolchainStore for T {
+    async fn active_toolchain(
+        self: Arc<Self>,
+        worktree_id: WorktreeId,
+        relative_path: Arc<Path>,
+        language_name: LanguageName,
+        cx: &mut AsyncApp,
+    ) -> Option<Toolchain> {
+        self.active_toolchain(worktree_id, &relative_path, language_name, cx)
+    }
+}
+
 type DefaultIndex = usize;
 #[derive(Default, Clone)]
 pub struct ToolchainList {

crates/language_extension/src/extension_lsp_adapter.rs 🔗

@@ -12,8 +12,8 @@ use fs::Fs;
 use futures::{Future, FutureExt, future::join_all};
 use gpui::{App, AppContext, AsyncApp, Task};
 use language::{
-    BinaryStatus, CodeLabel, HighlightId, Language, LanguageName, LanguageToolchainStore,
-    LspAdapter, LspAdapterDelegate,
+    BinaryStatus, CodeLabel, HighlightId, Language, LanguageName, LspAdapter, LspAdapterDelegate,
+    Toolchain,
 };
 use lsp::{
     CodeActionKind, LanguageServerBinary, LanguageServerBinaryOptions, LanguageServerName,
@@ -159,7 +159,7 @@ impl LspAdapter for ExtensionLspAdapter {
     fn get_language_server_command<'a>(
         self: Arc<Self>,
         delegate: Arc<dyn LspAdapterDelegate>,
-        _: Arc<dyn LanguageToolchainStore>,
+        _: Option<Toolchain>,
         _: LanguageServerBinaryOptions,
         _: futures::lock::MutexGuard<'a, Option<LanguageServerBinary>>,
         _: &'a mut AsyncApp,
@@ -288,7 +288,7 @@ impl LspAdapter for ExtensionLspAdapter {
         self: Arc<Self>,
         _: &dyn Fs,
         delegate: &Arc<dyn LspAdapterDelegate>,
-        _: Arc<dyn LanguageToolchainStore>,
+        _: Option<Toolchain>,
         _cx: &mut AsyncApp,
     ) -> Result<Value> {
         let delegate = Arc::new(WorktreeDelegateAdapter(delegate.clone())) as _;
@@ -336,7 +336,7 @@ impl LspAdapter for ExtensionLspAdapter {
         target_language_server_id: LanguageServerName,
         _: &dyn Fs,
         delegate: &Arc<dyn LspAdapterDelegate>,
-        _: Arc<dyn LanguageToolchainStore>,
+
         _cx: &mut AsyncApp,
     ) -> Result<Option<serde_json::Value>> {
         let delegate = Arc::new(WorktreeDelegateAdapter(delegate.clone())) as _;

crates/language_extension/src/language_extension.rs 🔗

@@ -52,7 +52,7 @@ impl ExtensionLanguageProxy for LanguageServerRegistryProxy {
         load: Arc<dyn Fn() -> Result<LoadedLanguage> + Send + Sync + 'static>,
     ) {
         self.language_registry
-            .register_language(language, grammar, matcher, hidden, load);
+            .register_language(language, grammar, matcher, hidden, None, load);
     }
 
     fn remove_languages(
@@ -61,6 +61,6 @@ impl ExtensionLanguageProxy for LanguageServerRegistryProxy {
         grammars_to_remove: &[Arc<str>],
     ) {
         self.language_registry
-            .remove_languages(&languages_to_remove, &grammars_to_remove);
+            .remove_languages(languages_to_remove, grammars_to_remove);
     }
 }

crates/language_model/src/fake_provider.rs 🔗

@@ -1,13 +1,14 @@
 use crate::{
-    AuthenticateError, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent,
-    LanguageModelId, LanguageModelName, LanguageModelProvider, LanguageModelProviderId,
-    LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest,
-    LanguageModelToolChoice,
+    AuthenticateError, ConfigurationViewTargetAgent, LanguageModel, LanguageModelCompletionError,
+    LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider,
+    LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
+    LanguageModelRequest, LanguageModelToolChoice,
 };
-use futures::{FutureExt, StreamExt, channel::mpsc, future::BoxFuture, stream::BoxStream};
+use futures::{FutureExt, channel::mpsc, future::BoxFuture, stream::BoxStream};
 use gpui::{AnyView, App, AsyncApp, Entity, Task, Window};
 use http_client::Result;
 use parking_lot::Mutex;
+use smol::stream::StreamExt;
 use std::sync::Arc;
 
 #[derive(Clone)]
@@ -62,7 +63,12 @@ impl LanguageModelProvider for FakeLanguageModelProvider {
         Task::ready(Ok(()))
     }
 
-    fn configuration_view(&self, _window: &mut Window, _: &mut App) -> AnyView {
+    fn configuration_view(
+        &self,
+        _target_agent: ConfigurationViewTargetAgent,
+        _window: &mut Window,
+        _: &mut App,
+    ) -> AnyView {
         unimplemented!()
     }
 
@@ -95,7 +101,9 @@ pub struct FakeLanguageModel {
     current_completion_txs: Mutex<
         Vec<(
             LanguageModelRequest,
-            mpsc::UnboundedSender<LanguageModelCompletionEvent>,
+            mpsc::UnboundedSender<
+                Result<LanguageModelCompletionEvent, LanguageModelCompletionError>,
+            >,
         )>,
     >,
 }
@@ -145,7 +153,21 @@ impl FakeLanguageModel {
             .find(|(req, _)| req == request)
             .map(|(_, tx)| tx)
             .unwrap();
-        tx.unbounded_send(event.into()).unwrap();
+        tx.unbounded_send(Ok(event.into())).unwrap();
+    }
+
+    pub fn send_completion_stream_error(
+        &self,
+        request: &LanguageModelRequest,
+        error: impl Into<LanguageModelCompletionError>,
+    ) {
+        let current_completion_txs = self.current_completion_txs.lock();
+        let tx = current_completion_txs
+            .iter()
+            .find(|(req, _)| req == request)
+            .map(|(_, tx)| tx)
+            .unwrap();
+        tx.unbounded_send(Err(error.into())).unwrap();
     }
 
     pub fn end_completion_stream(&self, request: &LanguageModelRequest) {
@@ -165,6 +187,13 @@ impl FakeLanguageModel {
         self.send_completion_stream_event(self.pending_completions().last().unwrap(), event);
     }
 
+    pub fn send_last_completion_stream_error(
+        &self,
+        error: impl Into<LanguageModelCompletionError>,
+    ) {
+        self.send_completion_stream_error(self.pending_completions().last().unwrap(), error);
+    }
+
     pub fn end_last_completion_stream(&self) {
         self.end_completion_stream(self.pending_completions().last().unwrap());
     }
@@ -224,7 +253,7 @@ impl LanguageModel for FakeLanguageModel {
     > {
         let (tx, rx) = mpsc::unbounded();
         self.current_completion_txs.lock().push((request, tx));
-        async move { Ok(rx.map(Ok).boxed()) }.boxed()
+        async move { Ok(rx.boxed()) }.boxed()
     }
 
     fn as_fake(&self) -> &Self {

crates/language_model/src/language_model.rs 🔗

@@ -54,7 +54,7 @@ pub const ZED_CLOUD_PROVIDER_NAME: LanguageModelProviderName =
 
 pub fn init(client: Arc<Client>, cx: &mut App) {
     init_settings(cx);
-    RefreshLlmTokenListener::register(client.clone(), cx);
+    RefreshLlmTokenListener::register(client, cx);
 }
 
 pub fn init_settings(cx: &mut App) {
@@ -300,7 +300,7 @@ impl From<AnthropicError> for LanguageModelCompletionError {
             },
             AnthropicError::ServerOverloaded { retry_after } => Self::ServerOverloaded {
                 provider,
-                retry_after: retry_after,
+                retry_after,
             },
             AnthropicError::ApiError(api_error) => api_error.into(),
         }
@@ -538,7 +538,7 @@ pub trait LanguageModel: Send + Sync {
             if let Some(first_event) = events.next().await {
                 match first_event {
                     Ok(LanguageModelCompletionEvent::StartMessage { message_id: id }) => {
-                        message_id = Some(id.clone());
+                        message_id = Some(id);
                     }
                     Ok(LanguageModelCompletionEvent::Text(text)) => {
                         first_item_text = Some(text);
@@ -634,7 +634,12 @@ pub trait LanguageModelProvider: 'static {
     }
     fn is_authenticated(&self, cx: &App) -> bool;
     fn authenticate(&self, cx: &mut App) -> Task<Result<(), AuthenticateError>>;
-    fn configuration_view(&self, window: &mut Window, cx: &mut App) -> AnyView;
+    fn configuration_view(
+        &self,
+        target_agent: ConfigurationViewTargetAgent,
+        window: &mut Window,
+        cx: &mut App,
+    ) -> AnyView;
     fn must_accept_terms(&self, _cx: &App) -> bool {
         false
     }
@@ -648,6 +653,13 @@ pub trait LanguageModelProvider: 'static {
     fn reset_credentials(&self, cx: &mut App) -> Task<Result<()>>;
 }
 
+#[derive(Default, Clone, Copy)]
+pub enum ConfigurationViewTargetAgent {
+    #[default]
+    ZedAgent,
+    Other(&'static str),
+}
+
 #[derive(PartialEq, Eq)]
 pub enum LanguageModelProviderTosView {
     /// When there are some past interactions in the Agent Panel.

crates/language_model/src/model/cloud_model.rs 🔗

@@ -42,6 +42,18 @@ impl fmt::Display for ModelRequestLimitReachedError {
     }
 }
 
+#[derive(Error, Debug)]
+pub struct ToolUseLimitReachedError;
+
+impl fmt::Display for ToolUseLimitReachedError {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        write!(
+            f,
+            "Consecutive tool use limit reached. Enable Burn Mode for unlimited tool use."
+        )
+    }
+}
+
 #[derive(Clone, Default)]
 pub struct LlmApiToken(Arc<RwLock<Option<String>>>);
 
@@ -70,7 +82,7 @@ impl LlmApiToken {
 
         let response = client.cloud_client().create_llm_token(system_id).await?;
         *lock = Some(response.token.0.clone());
-        Ok(response.token.0.clone())
+        Ok(response.token.0)
     }
 }
 

crates/language_model/src/registry.rs 🔗

@@ -21,7 +21,7 @@ impl Global for GlobalLanguageModelRegistry {}
 pub enum ConfigurationError {
     #[error("Configure at least one LLM provider to start using the panel.")]
     NoProvider,
-    #[error("LLM Provider is not configured or does not support the configured model.")]
+    #[error("LLM provider is not configured or does not support the configured model.")]
     ModelNotFound,
     #[error("{} LLM provider is not configured.", .0.name().0)]
     ProviderNotAuthenticated(Arc<dyn LanguageModelProvider>),
@@ -107,7 +107,7 @@ pub enum Event {
     InlineAssistantModelChanged,
     CommitMessageModelChanged,
     ThreadSummaryModelChanged,
-    ProviderStateChanged,
+    ProviderStateChanged(LanguageModelProviderId),
     AddedProvider(LanguageModelProviderId),
     RemovedProvider(LanguageModelProviderId),
 }
@@ -148,8 +148,11 @@ impl LanguageModelRegistry {
     ) {
         let id = provider.id();
 
-        let subscription = provider.subscribe(cx, |_, cx| {
-            cx.emit(Event::ProviderStateChanged);
+        let subscription = provider.subscribe(cx, {
+            let id = id.clone();
+            move |_, cx| {
+                cx.emit(Event::ProviderStateChanged(id.clone()));
+            }
         });
         if let Some(subscription) = subscription {
             subscription.detach();

crates/language_model/src/request.rs 🔗

@@ -220,42 +220,39 @@ impl<'de> Deserialize<'de> for LanguageModelToolResultContent {
 
             // Accept wrapped text format: { "type": "text", "text": "..." }
             if let (Some(type_value), Some(text_value)) =
-                (get_field(&obj, "type"), get_field(&obj, "text"))
+                (get_field(obj, "type"), get_field(obj, "text"))
+                && let Some(type_str) = type_value.as_str()
+                && type_str.to_lowercase() == "text"
+                && let Some(text) = text_value.as_str()
             {
-                if let Some(type_str) = type_value.as_str() {
-                    if type_str.to_lowercase() == "text" {
-                        if let Some(text) = text_value.as_str() {
-                            return Ok(Self::Text(Arc::from(text)));
-                        }
-                    }
-                }
+                return Ok(Self::Text(Arc::from(text)));
             }
 
             // Check for wrapped Text variant: { "text": "..." }
-            if let Some((_key, value)) = obj.iter().find(|(k, _)| k.to_lowercase() == "text") {
-                if obj.len() == 1 {
-                    // Only one field, and it's "text" (case-insensitive)
-                    if let Some(text) = value.as_str() {
-                        return Ok(Self::Text(Arc::from(text)));
-                    }
+            if let Some((_key, value)) = obj.iter().find(|(k, _)| k.to_lowercase() == "text")
+                && obj.len() == 1
+            {
+                // Only one field, and it's "text" (case-insensitive)
+                if let Some(text) = value.as_str() {
+                    return Ok(Self::Text(Arc::from(text)));
                 }
             }
 
             // Check for wrapped Image variant: { "image": { "source": "...", "size": ... } }
-            if let Some((_key, value)) = obj.iter().find(|(k, _)| k.to_lowercase() == "image") {
-                if obj.len() == 1 {
-                    // Only one field, and it's "image" (case-insensitive)
-                    // Try to parse the nested image object
-                    if let Some(image_obj) = value.as_object() {
-                        if let Some(image) = LanguageModelImage::from_json(image_obj) {
-                            return Ok(Self::Image(image));
-                        }
-                    }
+            if let Some((_key, value)) = obj.iter().find(|(k, _)| k.to_lowercase() == "image")
+                && obj.len() == 1
+            {
+                // Only one field, and it's "image" (case-insensitive)
+                // Try to parse the nested image object
+                if let Some(image_obj) = value.as_object()
+                    && let Some(image) = LanguageModelImage::from_json(image_obj)
+                {
+                    return Ok(Self::Image(image));
                 }
             }
 
             // Try as direct Image (object with "source" and "size" fields)
-            if let Some(image) = LanguageModelImage::from_json(&obj) {
+            if let Some(image) = LanguageModelImage::from_json(obj) {
                 return Ok(Self::Image(image));
             }
         }
@@ -272,7 +269,7 @@ impl<'de> Deserialize<'de> for LanguageModelToolResultContent {
 impl LanguageModelToolResultContent {
     pub fn to_str(&self) -> Option<&str> {
         match self {
-            Self::Text(text) => Some(&text),
+            Self::Text(text) => Some(text),
             Self::Image(_) => None,
         }
     }
@@ -297,6 +294,12 @@ impl From<String> for LanguageModelToolResultContent {
     }
 }
 
+impl From<LanguageModelImage> for LanguageModelToolResultContent {
+    fn from(image: LanguageModelImage) -> Self {
+        Self::Image(image)
+    }
+}
+
 #[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, Hash)]
 pub enum MessageContent {
     Text(String),

crates/language_model/src/role.rs 🔗

@@ -19,7 +19,7 @@ impl Role {
         }
     }
 
-    pub fn to_proto(&self) -> proto::LanguageModelRole {
+    pub fn to_proto(self) -> proto::LanguageModelRole {
         match self {
             Role::User => proto::LanguageModelRole::LanguageModelUser,
             Role::Assistant => proto::LanguageModelRole::LanguageModelAssistant,

crates/language_models/src/language_models.rs 🔗

@@ -104,7 +104,7 @@ fn register_language_model_providers(
     cx: &mut Context<LanguageModelRegistry>,
 ) {
     registry.register_provider(
-        CloudLanguageModelProvider::new(user_store.clone(), client.clone(), cx),
+        CloudLanguageModelProvider::new(user_store, client.clone(), cx),
         cx,
     );
 

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

@@ -15,11 +15,11 @@ use gpui::{
 };
 use http_client::HttpClient;
 use language_model::{
-    AuthenticateError, LanguageModel, LanguageModelCacheConfiguration,
-    LanguageModelCompletionError, LanguageModelId, LanguageModelName, LanguageModelProvider,
-    LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
-    LanguageModelRequest, LanguageModelToolChoice, LanguageModelToolResultContent, MessageContent,
-    RateLimiter, Role,
+    AuthenticateError, ConfigurationViewTargetAgent, LanguageModel,
+    LanguageModelCacheConfiguration, LanguageModelCompletionError, LanguageModelId,
+    LanguageModelName, LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName,
+    LanguageModelProviderState, LanguageModelRequest, LanguageModelToolChoice,
+    LanguageModelToolResultContent, MessageContent, RateLimiter, Role,
 };
 use language_model::{LanguageModelCompletionEvent, LanguageModelToolUse, StopReason};
 use schemars::JsonSchema;
@@ -114,7 +114,7 @@ impl State {
             .clone();
         cx.spawn(async move |this, cx| {
             credentials_provider
-                .delete_credentials(&api_url, &cx)
+                .delete_credentials(&api_url, cx)
                 .await
                 .ok();
             this.update(cx, |this, cx| {
@@ -133,7 +133,7 @@ impl State {
             .clone();
         cx.spawn(async move |this, cx| {
             credentials_provider
-                .write_credentials(&api_url, "Bearer", api_key.as_bytes(), &cx)
+                .write_credentials(&api_url, "Bearer", api_key.as_bytes(), cx)
                 .await
                 .ok();
 
@@ -153,29 +153,14 @@ impl State {
             return Task::ready(Ok(()));
         }
 
-        let credentials_provider = <dyn CredentialsProvider>::global(cx);
-        let api_url = AllLanguageModelSettings::get_global(cx)
-            .anthropic
-            .api_url
-            .clone();
+        let key = AnthropicLanguageModelProvider::api_key(cx);
 
         cx.spawn(async move |this, cx| {
-            let (api_key, from_env) = if let Ok(api_key) = std::env::var(ANTHROPIC_API_KEY_VAR) {
-                (api_key, true)
-            } else {
-                let (_, api_key) = credentials_provider
-                    .read_credentials(&api_url, &cx)
-                    .await?
-                    .ok_or(AuthenticateError::CredentialsNotFound)?;
-                (
-                    String::from_utf8(api_key).context("invalid {PROVIDER_NAME} API key")?,
-                    false,
-                )
-            };
+            let key = key.await?;
 
             this.update(cx, |this, cx| {
-                this.api_key = Some(api_key);
-                this.api_key_from_env = from_env;
+                this.api_key = Some(key.key);
+                this.api_key_from_env = key.from_env;
                 cx.notify();
             })?;
 
@@ -184,6 +169,11 @@ impl State {
     }
 }
 
+pub struct ApiKey {
+    pub key: String,
+    pub from_env: bool,
+}
+
 impl AnthropicLanguageModelProvider {
     pub fn new(http_client: Arc<dyn HttpClient>, cx: &mut App) -> Self {
         let state = cx.new(|cx| State {
@@ -206,6 +196,33 @@ impl AnthropicLanguageModelProvider {
             request_limiter: RateLimiter::new(4),
         })
     }
+
+    pub fn api_key(cx: &mut App) -> Task<Result<ApiKey>> {
+        let credentials_provider = <dyn CredentialsProvider>::global(cx);
+        let api_url = AllLanguageModelSettings::get_global(cx)
+            .anthropic
+            .api_url
+            .clone();
+
+        if let Ok(key) = std::env::var(ANTHROPIC_API_KEY_VAR) {
+            Task::ready(Ok(ApiKey {
+                key,
+                from_env: true,
+            }))
+        } else {
+            cx.spawn(async move |cx| {
+                let (_, api_key) = credentials_provider
+                    .read_credentials(&api_url, cx)
+                    .await?
+                    .ok_or(AuthenticateError::CredentialsNotFound)?;
+
+                Ok(ApiKey {
+                    key: String::from_utf8(api_key).context("invalid {PROVIDER_NAME} API key")?,
+                    from_env: false,
+                })
+            })
+        }
+    }
 }
 
 impl LanguageModelProviderState for AnthropicLanguageModelProvider {
@@ -299,8 +316,13 @@ impl LanguageModelProvider for AnthropicLanguageModelProvider {
         self.state.update(cx, |state, cx| state.authenticate(cx))
     }
 
-    fn configuration_view(&self, window: &mut Window, cx: &mut App) -> AnyView {
-        cx.new(|cx| ConfigurationView::new(self.state.clone(), window, cx))
+    fn configuration_view(
+        &self,
+        target_agent: ConfigurationViewTargetAgent,
+        window: &mut Window,
+        cx: &mut App,
+    ) -> AnyView {
+        cx.new(|cx| ConfigurationView::new(self.state.clone(), target_agent, window, cx))
             .into()
     }
 
@@ -532,7 +554,7 @@ pub fn into_anthropic(
                     .into_iter()
                     .filter_map(|content| match content {
                         MessageContent::Text(text) => {
-                            let text = if text.chars().last().map_or(false, |c| c.is_whitespace()) {
+                            let text = if text.chars().last().is_some_and(|c| c.is_whitespace()) {
                                 text.trim_end().to_string()
                             } else {
                                 text
@@ -611,11 +633,11 @@ pub fn into_anthropic(
                     Role::Assistant => anthropic::Role::Assistant,
                     Role::System => unreachable!("System role should never occur here"),
                 };
-                if let Some(last_message) = new_messages.last_mut() {
-                    if last_message.role == anthropic_role {
-                        last_message.content.extend(anthropic_message_content);
-                        continue;
-                    }
+                if let Some(last_message) = new_messages.last_mut()
+                    && last_message.role == anthropic_role
+                {
+                    last_message.content.extend(anthropic_message_content);
+                    continue;
                 }
 
                 // Mark the last segment of the message as cached
@@ -791,7 +813,7 @@ impl AnthropicEventMapper {
                             ))];
                         }
                     }
-                    return vec![];
+                    vec![]
                 }
             },
             Event::ContentBlockStop { index } => {
@@ -902,12 +924,18 @@ struct ConfigurationView {
     api_key_editor: Entity<Editor>,
     state: gpui::Entity<State>,
     load_credentials_task: Option<Task<()>>,
+    target_agent: ConfigurationViewTargetAgent,
 }
 
 impl ConfigurationView {
     const PLACEHOLDER_TEXT: &'static str = "sk-ant-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
 
-    fn new(state: gpui::Entity<State>, window: &mut Window, cx: &mut Context<Self>) -> Self {
+    fn new(
+        state: gpui::Entity<State>,
+        target_agent: ConfigurationViewTargetAgent,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) -> Self {
         cx.observe(&state, |_, _, cx| {
             cx.notify();
         })
@@ -939,6 +967,7 @@ impl ConfigurationView {
             }),
             state,
             load_credentials_task,
+            target_agent,
         }
     }
 
@@ -1012,7 +1041,10 @@ impl Render for ConfigurationView {
             v_flex()
                 .size_full()
                 .on_action(cx.listener(Self::save_api_key))
-                .child(Label::new("To use Zed's agent with Anthropic, you need to add an API key. Follow these steps:"))
+                .child(Label::new(format!("To use {}, you need to add an API key. Follow these steps:", match self.target_agent {
+                    ConfigurationViewTargetAgent::ZedAgent => "Zed's agent with Anthropic",
+                    ConfigurationViewTargetAgent::Other(agent) => agent,
+                })))
                 .child(
                     List::new()
                         .child(
@@ -1023,7 +1055,7 @@ impl Render for ConfigurationView {
                             )
                         )
                         .child(
-                            InstructionListItem::text_only("Paste your API key below and hit enter to start using the assistant")
+                            InstructionListItem::text_only("Paste your API key below and hit enter to start using the agent")
                         )
                 )
                 .child(

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

@@ -150,7 +150,7 @@ impl State {
         let credentials_provider = <dyn CredentialsProvider>::global(cx);
         cx.spawn(async move |this, cx| {
             credentials_provider
-                .delete_credentials(AMAZON_AWS_URL, &cx)
+                .delete_credentials(AMAZON_AWS_URL, cx)
                 .await
                 .log_err();
             this.update(cx, |this, cx| {
@@ -174,7 +174,7 @@ impl State {
                     AMAZON_AWS_URL,
                     "Bearer",
                     &serde_json::to_vec(&credentials)?,
-                    &cx,
+                    cx,
                 )
                 .await?;
             this.update(cx, |this, cx| {
@@ -206,7 +206,7 @@ impl State {
                     (credentials, true)
                 } else {
                     let (_, credentials) = credentials_provider
-                        .read_credentials(AMAZON_AWS_URL, &cx)
+                        .read_credentials(AMAZON_AWS_URL, cx)
                         .await?
                         .ok_or_else(|| AuthenticateError::CredentialsNotFound)?;
                     (
@@ -348,7 +348,12 @@ impl LanguageModelProvider for BedrockLanguageModelProvider {
         self.state.update(cx, |state, cx| state.authenticate(cx))
     }
 
-    fn configuration_view(&self, window: &mut Window, cx: &mut App) -> AnyView {
+    fn configuration_view(
+        &self,
+        _target_agent: language_model::ConfigurationViewTargetAgent,
+        window: &mut Window,
+        cx: &mut App,
+    ) -> AnyView {
         cx.new(|cx| ConfigurationView::new(self.state.clone(), window, cx))
             .into()
     }
@@ -407,10 +412,10 @@ impl BedrockModel {
                     .region(Region::new(region))
                     .timeout_config(TimeoutConfig::disabled());
 
-                if let Some(endpoint_url) = endpoint {
-                    if !endpoint_url.is_empty() {
-                        config_builder = config_builder.endpoint_url(endpoint_url);
-                    }
+                if let Some(endpoint_url) = endpoint
+                    && !endpoint_url.is_empty()
+                {
+                    config_builder = config_builder.endpoint_url(endpoint_url);
                 }
 
                 match auth_method {
@@ -460,7 +465,7 @@ impl BedrockModel {
         Result<BoxStream<'static, Result<BedrockStreamingResponse, BedrockError>>>,
     > {
         let Ok(runtime_client) = self
-            .get_or_init_client(&cx)
+            .get_or_init_client(cx)
             .cloned()
             .context("Bedrock client not initialized")
         else {
@@ -723,11 +728,11 @@ pub fn into_bedrock(
                     Role::Assistant => bedrock::BedrockRole::Assistant,
                     Role::System => unreachable!("System role should never occur here"),
                 };
-                if let Some(last_message) = new_messages.last_mut() {
-                    if last_message.role == bedrock_role {
-                        last_message.content.extend(bedrock_message_content);
-                        continue;
-                    }
+                if let Some(last_message) = new_messages.last_mut()
+                    && last_message.role == bedrock_role
+                {
+                    last_message.content.extend(bedrock_message_content);
+                    continue;
                 }
                 new_messages.push(
                     BedrockMessage::builder()
@@ -912,7 +917,7 @@ pub fn map_to_language_model_completion_events(
                             Some(ContentBlockDelta::ReasoningContent(thinking)) => match thinking {
                                 ReasoningContentBlockDelta::Text(thoughts) => {
                                     Some(Ok(LanguageModelCompletionEvent::Thinking {
-                                        text: thoughts.clone(),
+                                        text: thoughts,
                                         signature: None,
                                     }))
                                 }
@@ -963,7 +968,7 @@ pub fn map_to_language_model_completion_events(
                                         id: tool_use.id.into(),
                                         name: tool_use.name.into(),
                                         is_input_complete: true,
-                                        raw_input: tool_use.input_json.clone(),
+                                        raw_input: tool_use.input_json,
                                         input,
                                     },
                                 ))
@@ -1081,21 +1086,18 @@ impl ConfigurationView {
             .access_key_id_editor
             .read(cx)
             .text(cx)
-            .to_string()
             .trim()
             .to_string();
         let secret_access_key = self
             .secret_access_key_editor
             .read(cx)
             .text(cx)
-            .to_string()
             .trim()
             .to_string();
         let session_token = self
             .session_token_editor
             .read(cx)
             .text(cx)
-            .to_string()
             .trim()
             .to_string();
         let session_token = if session_token.is_empty() {
@@ -1103,13 +1105,7 @@ impl ConfigurationView {
         } else {
             Some(session_token)
         };
-        let region = self
-            .region_editor
-            .read(cx)
-            .text(cx)
-            .to_string()
-            .trim()
-            .to_string();
+        let region = self.region_editor.read(cx).text(cx).trim().to_string();
         let region = if region.is_empty() {
             "us-east-1".to_string()
         } else {

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

@@ -140,7 +140,7 @@ impl State {
         Self {
             client: client.clone(),
             llm_api_token: LlmApiToken::default(),
-            user_store: user_store.clone(),
+            user_store,
             status,
             accept_terms_of_service_task: None,
             models: Vec::new(),
@@ -193,7 +193,7 @@ impl State {
     fn authenticate(&self, cx: &mut Context<Self>) -> Task<Result<()>> {
         let client = self.client.clone();
         cx.spawn(async move |state, cx| {
-            client.sign_in_with_optional_connect(true, &cx).await?;
+            client.sign_in_with_optional_connect(true, cx).await?;
             state.update(cx, |_, cx| cx.notify())
         })
     }
@@ -270,7 +270,7 @@ impl State {
         if response.status().is_success() {
             let mut body = String::new();
             response.body_mut().read_to_string(&mut body).await?;
-            return Ok(serde_json::from_str(&body)?);
+            Ok(serde_json::from_str(&body)?)
         } else {
             let mut body = String::new();
             response.body_mut().read_to_string(&mut body).await?;
@@ -307,7 +307,7 @@ impl CloudLanguageModelProvider {
 
         Self {
             client,
-            state: state.clone(),
+            state,
             _maintain_client_status: maintain_client_status,
         }
     }
@@ -320,7 +320,7 @@ impl CloudLanguageModelProvider {
         Arc::new(CloudLanguageModel {
             id: LanguageModelId(SharedString::from(model.id.0.clone())),
             model,
-            llm_api_token: llm_api_token.clone(),
+            llm_api_token,
             client: self.client.clone(),
             request_limiter: RateLimiter::new(4),
         })
@@ -391,7 +391,12 @@ impl LanguageModelProvider for CloudLanguageModelProvider {
         Task::ready(Ok(()))
     }
 
-    fn configuration_view(&self, _: &mut Window, cx: &mut App) -> AnyView {
+    fn configuration_view(
+        &self,
+        _target_agent: language_model::ConfigurationViewTargetAgent,
+        _: &mut Window,
+        cx: &mut App,
+    ) -> AnyView {
         cx.new(|_| ConfigurationView::new(self.state.clone()))
             .into()
     }
@@ -437,7 +442,7 @@ fn render_accept_terms(
         .style(ButtonStyle::Subtle)
         .icon(IconName::ArrowUpRight)
         .icon_color(Color::Muted)
-        .icon_size(IconSize::XSmall)
+        .icon_size(IconSize::Small)
         .when(thread_empty_state, |this| this.label_size(LabelSize::Small))
         .on_click(move |_, _window, cx| cx.open_url("https://zed.dev/terms-of-service"));
 
@@ -592,15 +597,13 @@ impl CloudLanguageModel {
                     .headers()
                     .get(SUBSCRIPTION_LIMIT_RESOURCE_HEADER_NAME)
                     .and_then(|resource| resource.to_str().ok())
-                {
-                    if let Some(plan) = response
+                    && let Some(plan) = response
                         .headers()
                         .get(CURRENT_PLAN_HEADER_NAME)
                         .and_then(|plan| plan.to_str().ok())
                         .and_then(|plan| cloud_llm_client::Plan::from_str(plan).ok())
-                    {
-                        return Err(anyhow!(ModelRequestLimitReachedError { plan }));
-                    }
+                {
+                    return Err(anyhow!(ModelRequestLimitReachedError { plan }));
                 }
             } else if status == StatusCode::PAYMENT_REQUIRED {
                 return Err(anyhow!(PaymentRequiredError));
@@ -657,29 +660,29 @@ where
 
 impl From<ApiError> for LanguageModelCompletionError {
     fn from(error: ApiError) -> Self {
-        if let Ok(cloud_error) = serde_json::from_str::<CloudApiError>(&error.body) {
-            if cloud_error.code.starts_with("upstream_http_") {
-                let status = if let Some(status) = cloud_error.upstream_status {
-                    status
-                } else if cloud_error.code.ends_with("_error") {
-                    error.status
-                } else {
-                    // If there's a status code in the code string (e.g. "upstream_http_429")
-                    // then use that; otherwise, see if the JSON contains a status code.
-                    cloud_error
-                        .code
-                        .strip_prefix("upstream_http_")
-                        .and_then(|code_str| code_str.parse::<u16>().ok())
-                        .and_then(|code| StatusCode::from_u16(code).ok())
-                        .unwrap_or(error.status)
-                };
+        if let Ok(cloud_error) = serde_json::from_str::<CloudApiError>(&error.body)
+            && cloud_error.code.starts_with("upstream_http_")
+        {
+            let status = if let Some(status) = cloud_error.upstream_status {
+                status
+            } else if cloud_error.code.ends_with("_error") {
+                error.status
+            } else {
+                // If there's a status code in the code string (e.g. "upstream_http_429")
+                // then use that; otherwise, see if the JSON contains a status code.
+                cloud_error
+                    .code
+                    .strip_prefix("upstream_http_")
+                    .and_then(|code_str| code_str.parse::<u16>().ok())
+                    .and_then(|code| StatusCode::from_u16(code).ok())
+                    .unwrap_or(error.status)
+            };
 
-                return LanguageModelCompletionError::UpstreamProviderError {
-                    message: cloud_error.message,
-                    status,
-                    retry_after: cloud_error.retry_after.map(Duration::from_secs_f64),
-                };
-            }
+            return LanguageModelCompletionError::UpstreamProviderError {
+                message: cloud_error.message,
+                status,
+                retry_after: cloud_error.retry_after.map(Duration::from_secs_f64),
+            };
         }
 
         let retry_after = None;
@@ -941,6 +944,8 @@ impl LanguageModel for CloudLanguageModel {
                     request,
                     model.id(),
                     model.supports_parallel_tool_calls(),
+                    model.supports_prompt_cache_key(),
+                    None,
                     None,
                 );
                 let llm_api_token = self.llm_api_token.clone();

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

@@ -176,7 +176,12 @@ impl LanguageModelProvider for CopilotChatLanguageModelProvider {
         Task::ready(Err(err.into()))
     }
 
-    fn configuration_view(&self, _: &mut Window, cx: &mut App) -> AnyView {
+    fn configuration_view(
+        &self,
+        _target_agent: language_model::ConfigurationViewTargetAgent,
+        _: &mut Window,
+        cx: &mut App,
+    ) -> AnyView {
         let state = self.state.clone();
         cx.new(|cx| ConfigurationView::new(state, cx)).into()
     }

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

@@ -77,7 +77,7 @@ impl State {
             .clone();
         cx.spawn(async move |this, cx| {
             credentials_provider
-                .delete_credentials(&api_url, &cx)
+                .delete_credentials(&api_url, cx)
                 .await
                 .log_err();
             this.update(cx, |this, cx| {
@@ -96,7 +96,7 @@ impl State {
             .clone();
         cx.spawn(async move |this, cx| {
             credentials_provider
-                .write_credentials(&api_url, "Bearer", api_key.as_bytes(), &cx)
+                .write_credentials(&api_url, "Bearer", api_key.as_bytes(), cx)
                 .await?;
             this.update(cx, |this, cx| {
                 this.api_key = Some(api_key);
@@ -120,7 +120,7 @@ impl State {
                 (api_key, true)
             } else {
                 let (_, api_key) = credentials_provider
-                    .read_credentials(&api_url, &cx)
+                    .read_credentials(&api_url, cx)
                     .await?
                     .ok_or(AuthenticateError::CredentialsNotFound)?;
                 (
@@ -229,7 +229,12 @@ impl LanguageModelProvider for DeepSeekLanguageModelProvider {
         self.state.update(cx, |state, cx| state.authenticate(cx))
     }
 
-    fn configuration_view(&self, window: &mut Window, cx: &mut App) -> AnyView {
+    fn configuration_view(
+        &self,
+        _target_agent: language_model::ConfigurationViewTargetAgent,
+        window: &mut Window,
+        cx: &mut App,
+    ) -> AnyView {
         cx.new(|cx| ConfigurationView::new(self.state.clone(), window, cx))
             .into()
     }

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

@@ -12,9 +12,9 @@ use gpui::{
 };
 use http_client::HttpClient;
 use language_model::{
-    AuthenticateError, LanguageModelCompletionError, LanguageModelCompletionEvent,
-    LanguageModelToolChoice, LanguageModelToolSchemaFormat, LanguageModelToolUse,
-    LanguageModelToolUseId, MessageContent, StopReason,
+    AuthenticateError, ConfigurationViewTargetAgent, LanguageModelCompletionError,
+    LanguageModelCompletionEvent, LanguageModelToolChoice, LanguageModelToolSchemaFormat,
+    LanguageModelToolUse, LanguageModelToolUseId, MessageContent, StopReason,
 };
 use language_model::{
     LanguageModel, LanguageModelId, LanguageModelName, LanguageModelProvider,
@@ -37,6 +37,8 @@ use util::ResultExt;
 use crate::AllLanguageModelSettings;
 use crate::ui::InstructionListItem;
 
+use super::anthropic::ApiKey;
+
 const PROVIDER_ID: LanguageModelProviderId = language_model::GOOGLE_PROVIDER_ID;
 const PROVIDER_NAME: LanguageModelProviderName = language_model::GOOGLE_PROVIDER_NAME;
 
@@ -110,7 +112,7 @@ impl State {
             .clone();
         cx.spawn(async move |this, cx| {
             credentials_provider
-                .delete_credentials(&api_url, &cx)
+                .delete_credentials(&api_url, cx)
                 .await
                 .log_err();
             this.update(cx, |this, cx| {
@@ -129,7 +131,7 @@ impl State {
             .clone();
         cx.spawn(async move |this, cx| {
             credentials_provider
-                .write_credentials(&api_url, "Bearer", api_key.as_bytes(), &cx)
+                .write_credentials(&api_url, "Bearer", api_key.as_bytes(), cx)
                 .await?;
             this.update(cx, |this, cx| {
                 this.api_key = Some(api_key);
@@ -156,7 +158,7 @@ impl State {
                 (api_key, true)
             } else {
                 let (_, api_key) = credentials_provider
-                    .read_credentials(&api_url, &cx)
+                    .read_credentials(&api_url, cx)
                     .await?
                     .ok_or(AuthenticateError::CredentialsNotFound)?;
                 (
@@ -198,6 +200,33 @@ impl GoogleLanguageModelProvider {
             request_limiter: RateLimiter::new(4),
         })
     }
+
+    pub fn api_key(cx: &mut App) -> Task<Result<ApiKey>> {
+        let credentials_provider = <dyn CredentialsProvider>::global(cx);
+        let api_url = AllLanguageModelSettings::get_global(cx)
+            .google
+            .api_url
+            .clone();
+
+        if let Ok(key) = std::env::var(GEMINI_API_KEY_VAR) {
+            Task::ready(Ok(ApiKey {
+                key,
+                from_env: true,
+            }))
+        } else {
+            cx.spawn(async move |cx| {
+                let (_, api_key) = credentials_provider
+                    .read_credentials(&api_url, cx)
+                    .await?
+                    .ok_or(AuthenticateError::CredentialsNotFound)?;
+
+                Ok(ApiKey {
+                    key: String::from_utf8(api_key).context("invalid {PROVIDER_NAME} API key")?,
+                    from_env: false,
+                })
+            })
+        }
+    }
 }
 
 impl LanguageModelProviderState for GoogleLanguageModelProvider {
@@ -277,8 +306,13 @@ impl LanguageModelProvider for GoogleLanguageModelProvider {
         self.state.update(cx, |state, cx| state.authenticate(cx))
     }
 
-    fn configuration_view(&self, window: &mut Window, cx: &mut App) -> AnyView {
-        cx.new(|cx| ConfigurationView::new(self.state.clone(), window, cx))
+    fn configuration_view(
+        &self,
+        target_agent: language_model::ConfigurationViewTargetAgent,
+        window: &mut Window,
+        cx: &mut App,
+    ) -> AnyView {
+        cx.new(|cx| ConfigurationView::new(self.state.clone(), target_agent, window, cx))
             .into()
     }
 
@@ -382,7 +416,7 @@ impl LanguageModel for GoogleLanguageModel {
         cx: &App,
     ) -> BoxFuture<'static, Result<u64>> {
         let model_id = self.model.request_id().to_string();
-        let request = into_google(request, model_id.clone(), self.model.mode());
+        let request = into_google(request, model_id, self.model.mode());
         let http_client = self.http_client.clone();
         let api_key = self.state.read(cx).api_key.clone();
 
@@ -525,7 +559,7 @@ pub fn into_google(
     let system_instructions = if request
         .messages
         .first()
-        .map_or(false, |msg| matches!(msg.role, Role::System))
+        .is_some_and(|msg| matches!(msg.role, Role::System))
     {
         let message = request.messages.remove(0);
         Some(SystemInstruction {
@@ -572,7 +606,7 @@ pub fn into_google(
             top_k: None,
         }),
         safety_settings: None,
-        tools: (request.tools.len() > 0).then(|| {
+        tools: (!request.tools.is_empty()).then(|| {
             vec![google_ai::Tool {
                 function_declarations: request
                     .tools
@@ -771,11 +805,17 @@ fn convert_usage(usage: &UsageMetadata) -> language_model::TokenUsage {
 struct ConfigurationView {
     api_key_editor: Entity<Editor>,
     state: gpui::Entity<State>,
+    target_agent: language_model::ConfigurationViewTargetAgent,
     load_credentials_task: Option<Task<()>>,
 }
 
 impl ConfigurationView {
-    fn new(state: gpui::Entity<State>, window: &mut Window, cx: &mut Context<Self>) -> Self {
+    fn new(
+        state: gpui::Entity<State>,
+        target_agent: language_model::ConfigurationViewTargetAgent,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) -> Self {
         cx.observe(&state, |_, _, cx| {
             cx.notify();
         })
@@ -805,6 +845,7 @@ impl ConfigurationView {
                 editor.set_placeholder_text("AIzaSy...", cx);
                 editor
             }),
+            target_agent,
             state,
             load_credentials_task,
         }
@@ -880,7 +921,10 @@ impl Render for ConfigurationView {
             v_flex()
                 .size_full()
                 .on_action(cx.listener(Self::save_api_key))
-                .child(Label::new("To use Zed's agent with Google AI, you need to add an API key. Follow these steps:"))
+                .child(Label::new(format!("To use {}, you need to add an API key. Follow these steps:", match self.target_agent {
+                    ConfigurationViewTargetAgent::ZedAgent => "Zed's agent with Google AI",
+                    ConfigurationViewTargetAgent::Other(agent) => agent,
+                })))
                 .child(
                     List::new()
                         .child(InstructionListItem::new(

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

@@ -210,7 +210,7 @@ impl LanguageModelProvider for LmStudioLanguageModelProvider {
             .map(|model| {
                 Arc::new(LmStudioLanguageModel {
                     id: LanguageModelId::from(model.name.clone()),
-                    model: model.clone(),
+                    model,
                     http_client: self.http_client.clone(),
                     request_limiter: RateLimiter::new(4),
                 }) as Arc<dyn LanguageModel>
@@ -226,7 +226,12 @@ impl LanguageModelProvider for LmStudioLanguageModelProvider {
         self.state.update(cx, |state, cx| state.authenticate(cx))
     }
 
-    fn configuration_view(&self, _window: &mut Window, cx: &mut App) -> AnyView {
+    fn configuration_view(
+        &self,
+        _target_agent: language_model::ConfigurationViewTargetAgent,
+        _window: &mut Window,
+        cx: &mut App,
+    ) -> AnyView {
         let state = self.state.clone();
         cx.new(|cx| ConfigurationView::new(state, cx)).into()
     }
@@ -690,7 +695,7 @@ impl Render for ConfigurationView {
                                             Button::new("lmstudio-site", "LM Studio")
                                                 .style(ButtonStyle::Subtle)
                                                 .icon(IconName::ArrowUpRight)
-                                                .icon_size(IconSize::XSmall)
+                                                .icon_size(IconSize::Small)
                                                 .icon_color(Color::Muted)
                                                 .on_click(move |_, _window, cx| {
                                                     cx.open_url(LMSTUDIO_SITE)
@@ -705,7 +710,7 @@ impl Render for ConfigurationView {
                                             )
                                             .style(ButtonStyle::Subtle)
                                             .icon(IconName::ArrowUpRight)
-                                            .icon_size(IconSize::XSmall)
+                                            .icon_size(IconSize::Small)
                                             .icon_color(Color::Muted)
                                             .on_click(move |_, _window, cx| {
                                                 cx.open_url(LMSTUDIO_DOWNLOAD_URL)
@@ -718,7 +723,7 @@ impl Render for ConfigurationView {
                                     Button::new("view-models", "Model Catalog")
                                         .style(ButtonStyle::Subtle)
                                         .icon(IconName::ArrowUpRight)
-                                        .icon_size(IconSize::XSmall)
+                                        .icon_size(IconSize::Small)
                                         .icon_color(Color::Muted)
                                         .on_click(move |_, _window, cx| {
                                             cx.open_url(LMSTUDIO_CATALOG_URL)
@@ -744,7 +749,7 @@ impl Render for ConfigurationView {
                                     Button::new("retry_lmstudio_models", "Connect")
                                         .icon_position(IconPosition::Start)
                                         .icon_size(IconSize::XSmall)
-                                        .icon(IconName::PlayOutlined)
+                                        .icon(IconName::PlayFilled)
                                         .on_click(cx.listener(move |this, _, _window, cx| {
                                             this.retry_connection(cx)
                                         })),

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

@@ -47,6 +47,7 @@ pub struct AvailableModel {
     pub max_completion_tokens: Option<u64>,
     pub supports_tools: Option<bool>,
     pub supports_images: Option<bool>,
+    pub supports_thinking: Option<bool>,
 }
 
 pub struct MistralLanguageModelProvider {
@@ -75,7 +76,7 @@ impl State {
             .clone();
         cx.spawn(async move |this, cx| {
             credentials_provider
-                .delete_credentials(&api_url, &cx)
+                .delete_credentials(&api_url, cx)
                 .await
                 .log_err();
             this.update(cx, |this, cx| {
@@ -94,7 +95,7 @@ impl State {
             .clone();
         cx.spawn(async move |this, cx| {
             credentials_provider
-                .write_credentials(&api_url, "Bearer", api_key.as_bytes(), &cx)
+                .write_credentials(&api_url, "Bearer", api_key.as_bytes(), cx)
                 .await?;
             this.update(cx, |this, cx| {
                 this.api_key = Some(api_key);
@@ -118,7 +119,7 @@ impl State {
                 (api_key, true)
             } else {
                 let (_, api_key) = credentials_provider
-                    .read_credentials(&api_url, &cx)
+                    .read_credentials(&api_url, cx)
                     .await?
                     .ok_or(AuthenticateError::CredentialsNotFound)?;
                 (
@@ -215,6 +216,7 @@ impl LanguageModelProvider for MistralLanguageModelProvider {
                     max_completion_tokens: model.max_completion_tokens,
                     supports_tools: model.supports_tools,
                     supports_images: model.supports_images,
+                    supports_thinking: model.supports_thinking,
                 },
             );
         }
@@ -241,7 +243,12 @@ impl LanguageModelProvider for MistralLanguageModelProvider {
         self.state.update(cx, |state, cx| state.authenticate(cx))
     }
 
-    fn configuration_view(&self, window: &mut Window, cx: &mut App) -> AnyView {
+    fn configuration_view(
+        &self,
+        _target_agent: language_model::ConfigurationViewTargetAgent,
+        window: &mut Window,
+        cx: &mut App,
+    ) -> AnyView {
         cx.new(|cx| ConfigurationView::new(self.state.clone(), window, cx))
             .into()
     }
@@ -366,11 +373,7 @@ impl LanguageModel for MistralLanguageModel {
             LanguageModelCompletionError,
         >,
     > {
-        let request = into_mistral(
-            request,
-            self.model.id().to_string(),
-            self.max_output_tokens(),
-        );
+        let request = into_mistral(request, self.model.clone(), self.max_output_tokens());
         let stream = self.stream_completion(request, cx);
 
         async move {
@@ -384,7 +387,7 @@ impl LanguageModel for MistralLanguageModel {
 
 pub fn into_mistral(
     request: LanguageModelRequest,
-    model: String,
+    model: mistral::Model,
     max_output_tokens: Option<u64>,
 ) -> mistral::Request {
     let stream = true;
@@ -401,13 +404,20 @@ pub fn into_mistral(
                                 .push_part(mistral::MessagePart::Text { text: text.clone() });
                         }
                         MessageContent::Image(image_content) => {
-                            message_content.push_part(mistral::MessagePart::ImageUrl {
-                                image_url: image_content.to_base64_url(),
-                            });
+                            if model.supports_images() {
+                                message_content.push_part(mistral::MessagePart::ImageUrl {
+                                    image_url: image_content.to_base64_url(),
+                                });
+                            }
                         }
                         MessageContent::Thinking { text, .. } => {
-                            message_content
-                                .push_part(mistral::MessagePart::Text { text: text.clone() });
+                            if model.supports_thinking() {
+                                message_content.push_part(mistral::MessagePart::Thinking {
+                                    thinking: vec![mistral::ThinkingPart::Text {
+                                        text: text.clone(),
+                                    }],
+                                });
+                            }
                         }
                         MessageContent::RedactedThinking(_) => {}
                         MessageContent::ToolUse(_) => {
@@ -437,12 +447,28 @@ pub fn into_mistral(
             Role::Assistant => {
                 for content in &message.content {
                     match content {
-                        MessageContent::Text(text) | MessageContent::Thinking { text, .. } => {
+                        MessageContent::Text(text) => {
                             messages.push(mistral::RequestMessage::Assistant {
-                                content: Some(text.clone()),
+                                content: Some(mistral::MessageContent::Plain {
+                                    content: text.clone(),
+                                }),
                                 tool_calls: Vec::new(),
                             });
                         }
+                        MessageContent::Thinking { text, .. } => {
+                            if model.supports_thinking() {
+                                messages.push(mistral::RequestMessage::Assistant {
+                                    content: Some(mistral::MessageContent::Multipart {
+                                        content: vec![mistral::MessagePart::Thinking {
+                                            thinking: vec![mistral::ThinkingPart::Text {
+                                                text: text.clone(),
+                                            }],
+                                        }],
+                                    }),
+                                    tool_calls: Vec::new(),
+                                });
+                            }
+                        }
                         MessageContent::RedactedThinking(_) => {}
                         MessageContent::Image(_) => {}
                         MessageContent::ToolUse(tool_use) => {
@@ -477,11 +503,26 @@ pub fn into_mistral(
             Role::System => {
                 for content in &message.content {
                     match content {
-                        MessageContent::Text(text) | MessageContent::Thinking { text, .. } => {
+                        MessageContent::Text(text) => {
                             messages.push(mistral::RequestMessage::System {
-                                content: text.clone(),
+                                content: mistral::MessageContent::Plain {
+                                    content: text.clone(),
+                                },
                             });
                         }
+                        MessageContent::Thinking { text, .. } => {
+                            if model.supports_thinking() {
+                                messages.push(mistral::RequestMessage::System {
+                                    content: mistral::MessageContent::Multipart {
+                                        content: vec![mistral::MessagePart::Thinking {
+                                            thinking: vec![mistral::ThinkingPart::Text {
+                                                text: text.clone(),
+                                            }],
+                                        }],
+                                    },
+                                });
+                            }
+                        }
                         MessageContent::RedactedThinking(_) => {}
                         MessageContent::Image(_)
                         | MessageContent::ToolUse(_)
@@ -494,37 +535,8 @@ pub fn into_mistral(
         }
     }
 
-    // The Mistral API requires that tool messages be followed by assistant messages,
-    // not user messages. When we have a tool->user sequence in the conversation,
-    // we need to insert a placeholder assistant message to maintain proper conversation
-    // flow and prevent API errors. This is a Mistral-specific requirement that differs
-    // from other language model APIs.
-    let messages = {
-        let mut fixed_messages = Vec::with_capacity(messages.len());
-        let mut messages_iter = messages.into_iter().peekable();
-
-        while let Some(message) = messages_iter.next() {
-            let is_tool_message = matches!(message, mistral::RequestMessage::Tool { .. });
-            fixed_messages.push(message);
-
-            // Insert assistant message between tool and user messages
-            if is_tool_message {
-                if let Some(next_msg) = messages_iter.peek() {
-                    if matches!(next_msg, mistral::RequestMessage::User { .. }) {
-                        fixed_messages.push(mistral::RequestMessage::Assistant {
-                            content: Some(" ".to_string()),
-                            tool_calls: Vec::new(),
-                        });
-                    }
-                }
-            }
-        }
-
-        fixed_messages
-    };
-
     mistral::Request {
-        model,
+        model: model.id().to_string(),
         messages,
         stream,
         max_tokens: max_output_tokens,
@@ -595,8 +607,38 @@ impl MistralEventMapper {
         };
 
         let mut events = Vec::new();
-        if let Some(content) = choice.delta.content.clone() {
-            events.push(Ok(LanguageModelCompletionEvent::Text(content)));
+        if let Some(content) = choice.delta.content.as_ref() {
+            match content {
+                mistral::MessageContentDelta::Text(text) => {
+                    events.push(Ok(LanguageModelCompletionEvent::Text(text.clone())));
+                }
+                mistral::MessageContentDelta::Parts(parts) => {
+                    for part in parts {
+                        match part {
+                            mistral::MessagePart::Text { text } => {
+                                events.push(Ok(LanguageModelCompletionEvent::Text(text.clone())));
+                            }
+                            mistral::MessagePart::Thinking { thinking } => {
+                                for tp in thinking.iter().cloned() {
+                                    match tp {
+                                        mistral::ThinkingPart::Text { text } => {
+                                            events.push(Ok(
+                                                LanguageModelCompletionEvent::Thinking {
+                                                    text,
+                                                    signature: None,
+                                                },
+                                            ));
+                                        }
+                                    }
+                                }
+                            }
+                            mistral::MessagePart::ImageUrl { .. } => {
+                                // We currently don't emit a separate event for images in responses.
+                            }
+                        }
+                    }
+                }
+            }
         }
 
         if let Some(tool_calls) = choice.delta.tool_calls.as_ref() {
@@ -908,7 +950,7 @@ mod tests {
             thinking_allowed: true,
         };
 
-        let mistral_request = into_mistral(request, "mistral-small-latest".into(), None);
+        let mistral_request = into_mistral(request, mistral::Model::MistralSmallLatest, None);
 
         assert_eq!(mistral_request.model, "mistral-small-latest");
         assert_eq!(mistral_request.temperature, Some(0.5));
@@ -941,7 +983,7 @@ mod tests {
             thinking_allowed: true,
         };
 
-        let mistral_request = into_mistral(request, "pixtral-12b-latest".into(), None);
+        let mistral_request = into_mistral(request, mistral::Model::Pixtral12BLatest, None);
 
         assert_eq!(mistral_request.messages.len(), 1);
         assert!(matches!(

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

@@ -237,7 +237,7 @@ impl LanguageModelProvider for OllamaLanguageModelProvider {
             .map(|model| {
                 Arc::new(OllamaLanguageModel {
                     id: LanguageModelId::from(model.name.clone()),
-                    model: model.clone(),
+                    model,
                     http_client: self.http_client.clone(),
                     request_limiter: RateLimiter::new(4),
                 }) as Arc<dyn LanguageModel>
@@ -255,7 +255,12 @@ impl LanguageModelProvider for OllamaLanguageModelProvider {
         self.state.update(cx, |state, cx| state.authenticate(cx))
     }
 
-    fn configuration_view(&self, window: &mut Window, cx: &mut App) -> AnyView {
+    fn configuration_view(
+        &self,
+        _target_agent: language_model::ConfigurationViewTargetAgent,
+        window: &mut Window,
+        cx: &mut App,
+    ) -> AnyView {
         let state = self.state.clone();
         cx.new(|cx| ConfigurationView::new(state, window, cx))
             .into()
@@ -608,7 +613,7 @@ impl Render for ConfigurationView {
                                             Button::new("ollama-site", "Ollama")
                                                 .style(ButtonStyle::Subtle)
                                                 .icon(IconName::ArrowUpRight)
-                                                .icon_size(IconSize::XSmall)
+                                                .icon_size(IconSize::Small)
                                                 .icon_color(Color::Muted)
                                                 .on_click(move |_, _, cx| cx.open_url(OLLAMA_SITE))
                                                 .into_any_element(),
@@ -621,7 +626,7 @@ impl Render for ConfigurationView {
                                             )
                                             .style(ButtonStyle::Subtle)
                                             .icon(IconName::ArrowUpRight)
-                                            .icon_size(IconSize::XSmall)
+                                            .icon_size(IconSize::Small)
                                             .icon_color(Color::Muted)
                                             .on_click(move |_, _, cx| {
                                                 cx.open_url(OLLAMA_DOWNLOAD_URL)
@@ -634,7 +639,7 @@ impl Render for ConfigurationView {
                                     Button::new("view-models", "View All Models")
                                         .style(ButtonStyle::Subtle)
                                         .icon(IconName::ArrowUpRight)
-                                        .icon_size(IconSize::XSmall)
+                                        .icon_size(IconSize::Small)
                                         .icon_color(Color::Muted)
                                         .on_click(move |_, _, cx| cx.open_url(OLLAMA_LIBRARY_URL)),
                                 ),
@@ -658,7 +663,7 @@ impl Render for ConfigurationView {
                                     Button::new("retry_ollama_models", "Connect")
                                         .icon_position(IconPosition::Start)
                                         .icon_size(IconSize::XSmall)
-                                        .icon(IconName::PlayOutlined)
+                                        .icon(IconName::PlayFilled)
                                         .on_click(cx.listener(move |this, _, _, cx| {
                                             this.retry_connection(cx)
                                         })),

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

@@ -14,7 +14,7 @@ use language_model::{
     RateLimiter, Role, StopReason, TokenUsage,
 };
 use menu;
-use open_ai::{ImageUrl, Model, ResponseStreamEvent, stream_completion};
+use open_ai::{ImageUrl, Model, ReasoningEffort, ResponseStreamEvent, stream_completion};
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 use settings::{Settings, SettingsStore};
@@ -45,6 +45,7 @@ pub struct AvailableModel {
     pub max_tokens: u64,
     pub max_output_tokens: Option<u64>,
     pub max_completion_tokens: Option<u64>,
+    pub reasoning_effort: Option<ReasoningEffort>,
 }
 
 pub struct OpenAiLanguageModelProvider {
@@ -74,7 +75,7 @@ impl State {
             .clone();
         cx.spawn(async move |this, cx| {
             credentials_provider
-                .delete_credentials(&api_url, &cx)
+                .delete_credentials(&api_url, cx)
                 .await
                 .log_err();
             this.update(cx, |this, cx| {
@@ -93,7 +94,7 @@ impl State {
             .clone();
         cx.spawn(async move |this, cx| {
             credentials_provider
-                .write_credentials(&api_url, "Bearer", api_key.as_bytes(), &cx)
+                .write_credentials(&api_url, "Bearer", api_key.as_bytes(), cx)
                 .await
                 .log_err();
             this.update(cx, |this, cx| {
@@ -118,7 +119,7 @@ impl State {
                 (api_key, true)
             } else {
                 let (_, api_key) = credentials_provider
-                    .read_credentials(&api_url, &cx)
+                    .read_credentials(&api_url, cx)
                     .await?
                     .ok_or(AuthenticateError::CredentialsNotFound)?;
                 (
@@ -213,6 +214,7 @@ impl LanguageModelProvider for OpenAiLanguageModelProvider {
                     max_tokens: model.max_tokens,
                     max_output_tokens: model.max_output_tokens,
                     max_completion_tokens: model.max_completion_tokens,
+                    reasoning_effort: model.reasoning_effort.clone(),
                 },
             );
         }
@@ -231,7 +233,12 @@ impl LanguageModelProvider for OpenAiLanguageModelProvider {
         self.state.update(cx, |state, cx| state.authenticate(cx))
     }
 
-    fn configuration_view(&self, window: &mut Window, cx: &mut App) -> AnyView {
+    fn configuration_view(
+        &self,
+        _target_agent: language_model::ConfigurationViewTargetAgent,
+        window: &mut Window,
+        cx: &mut App,
+    ) -> AnyView {
         cx.new(|cx| ConfigurationView::new(self.state.clone(), window, cx))
             .into()
     }
@@ -301,7 +308,25 @@ impl LanguageModel for OpenAiLanguageModel {
     }
 
     fn supports_images(&self) -> bool {
-        false
+        use open_ai::Model;
+        match &self.model {
+            Model::FourOmni
+            | Model::FourOmniMini
+            | Model::FourPointOne
+            | Model::FourPointOneMini
+            | Model::FourPointOneNano
+            | Model::Five
+            | Model::FiveMini
+            | Model::FiveNano
+            | Model::O1
+            | Model::O3
+            | Model::O4Mini => true,
+            Model::ThreePointFiveTurbo
+            | Model::Four
+            | Model::FourTurbo
+            | Model::O3Mini
+            | Model::Custom { .. } => false,
+        }
     }
 
     fn supports_tool_choice(&self, choice: LanguageModelToolChoice) -> bool {
@@ -350,7 +375,9 @@ impl LanguageModel for OpenAiLanguageModel {
             request,
             self.model.id(),
             self.model.supports_parallel_tool_calls(),
+            self.model.supports_prompt_cache_key(),
             self.max_output_tokens(),
+            self.model.reasoning_effort(),
         );
         let completions = self.stream_completion(request, cx);
         async move {
@@ -365,7 +392,9 @@ pub fn into_open_ai(
     request: LanguageModelRequest,
     model_id: &str,
     supports_parallel_tool_calls: bool,
+    supports_prompt_cache_key: bool,
     max_output_tokens: Option<u64>,
+    reasoning_effort: Option<ReasoningEffort>,
 ) -> open_ai::Request {
     let stream = !model_id.starts_with("o1-");
 
@@ -375,7 +404,7 @@ pub fn into_open_ai(
             match content {
                 MessageContent::Text(text) | MessageContent::Thinking { text, .. } => {
                     add_message_content_part(
-                        open_ai::MessagePart::Text { text: text },
+                        open_ai::MessagePart::Text { text },
                         message.role,
                         &mut messages,
                     )
@@ -455,6 +484,11 @@ pub fn into_open_ai(
         } else {
             None
         },
+        prompt_cache_key: if supports_prompt_cache_key {
+            request.thread_id
+        } else {
+            None
+        },
         tools: request
             .tools
             .into_iter()
@@ -471,6 +505,7 @@ pub fn into_open_ai(
             LanguageModelToolChoice::Any => open_ai::ToolChoice::Required,
             LanguageModelToolChoice::None => open_ai::ToolChoice::None,
         }),
+        reasoning_effort,
     }
 }
 
@@ -869,7 +904,7 @@ impl Render for ConfigurationView {
             .child(
                 Button::new("docs", "Learn More")
                     .icon(IconName::ArrowUpRight)
-                    .icon_size(IconSize::XSmall)
+                    .icon_size(IconSize::Small)
                     .icon_color(Color::Muted)
                     .on_click(move |_, _window, cx| {
                         cx.open_url("https://zed.dev/docs/ai/llm-providers#openai-api-compatible")

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

@@ -38,6 +38,27 @@ pub struct AvailableModel {
     pub max_tokens: u64,
     pub max_output_tokens: Option<u64>,
     pub max_completion_tokens: Option<u64>,
+    #[serde(default)]
+    pub capabilities: ModelCapabilities,
+}
+
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
+pub struct ModelCapabilities {
+    pub tools: bool,
+    pub images: bool,
+    pub parallel_tool_calls: bool,
+    pub prompt_cache_key: bool,
+}
+
+impl Default for ModelCapabilities {
+    fn default() -> Self {
+        Self {
+            tools: true,
+            images: false,
+            parallel_tool_calls: false,
+            prompt_cache_key: false,
+        }
+    }
 }
 
 pub struct OpenAiCompatibleLanguageModelProvider {
@@ -66,7 +87,7 @@ impl State {
         let api_url = self.settings.api_url.clone();
         cx.spawn(async move |this, cx| {
             credentials_provider
-                .delete_credentials(&api_url, &cx)
+                .delete_credentials(&api_url, cx)
                 .await
                 .log_err();
             this.update(cx, |this, cx| {
@@ -82,7 +103,7 @@ impl State {
         let api_url = self.settings.api_url.clone();
         cx.spawn(async move |this, cx| {
             credentials_provider
-                .write_credentials(&api_url, "Bearer", api_key.as_bytes(), &cx)
+                .write_credentials(&api_url, "Bearer", api_key.as_bytes(), cx)
                 .await
                 .log_err();
             this.update(cx, |this, cx| {
@@ -105,7 +126,7 @@ impl State {
                 (api_key, true)
             } else {
                 let (_, api_key) = credentials_provider
-                    .read_credentials(&api_url, &cx)
+                    .read_credentials(&api_url, cx)
                     .await?
                     .ok_or(AuthenticateError::CredentialsNotFound)?;
                 (
@@ -222,7 +243,12 @@ impl LanguageModelProvider for OpenAiCompatibleLanguageModelProvider {
         self.state.update(cx, |state, cx| state.authenticate(cx))
     }
 
-    fn configuration_view(&self, window: &mut Window, cx: &mut App) -> AnyView {
+    fn configuration_view(
+        &self,
+        _target_agent: language_model::ConfigurationViewTargetAgent,
+        window: &mut Window,
+        cx: &mut App,
+    ) -> AnyView {
         cx.new(|cx| ConfigurationView::new(self.state.clone(), window, cx))
             .into()
     }
@@ -293,17 +319,17 @@ impl LanguageModel for OpenAiCompatibleLanguageModel {
     }
 
     fn supports_tools(&self) -> bool {
-        true
+        self.model.capabilities.tools
     }
 
     fn supports_images(&self) -> bool {
-        false
+        self.model.capabilities.images
     }
 
     fn supports_tool_choice(&self, choice: LanguageModelToolChoice) -> bool {
         match choice {
-            LanguageModelToolChoice::Auto => true,
-            LanguageModelToolChoice::Any => true,
+            LanguageModelToolChoice::Auto => self.model.capabilities.tools,
+            LanguageModelToolChoice::Any => self.model.capabilities.tools,
             LanguageModelToolChoice::None => true,
         }
     }
@@ -355,7 +381,14 @@ impl LanguageModel for OpenAiCompatibleLanguageModel {
             LanguageModelCompletionError,
         >,
     > {
-        let request = into_open_ai(request, &self.model.name, true, self.max_output_tokens());
+        let request = into_open_ai(
+            request,
+            &self.model.name,
+            self.model.capabilities.parallel_tool_calls,
+            self.model.capabilities.prompt_cache_key,
+            self.max_output_tokens(),
+            None,
+        );
         let completions = self.stream_completion(request, cx);
         async move {
             let mapper = OpenAiEventMapper::new();

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

@@ -112,7 +112,7 @@ impl State {
             .clone();
         cx.spawn(async move |this, cx| {
             credentials_provider
-                .delete_credentials(&api_url, &cx)
+                .delete_credentials(&api_url, cx)
                 .await
                 .log_err();
             this.update(cx, |this, cx| {
@@ -131,7 +131,7 @@ impl State {
             .clone();
         cx.spawn(async move |this, cx| {
             credentials_provider
-                .write_credentials(&api_url, "Bearer", api_key.as_bytes(), &cx)
+                .write_credentials(&api_url, "Bearer", api_key.as_bytes(), cx)
                 .await
                 .log_err();
             this.update(cx, |this, cx| {
@@ -157,7 +157,7 @@ impl State {
                 (api_key, true)
             } else {
                 let (_, api_key) = credentials_provider
-                    .read_credentials(&api_url, &cx)
+                    .read_credentials(&api_url, cx)
                     .await?
                     .ok_or(AuthenticateError::CredentialsNotFound)?;
                 (
@@ -306,7 +306,12 @@ impl LanguageModelProvider for OpenRouterLanguageModelProvider {
         self.state.update(cx, |state, cx| state.authenticate(cx))
     }
 
-    fn configuration_view(&self, window: &mut Window, cx: &mut App) -> AnyView {
+    fn configuration_view(
+        &self,
+        _target_agent: language_model::ConfigurationViewTargetAgent,
+        window: &mut Window,
+        cx: &mut App,
+    ) -> AnyView {
         cx.new(|cx| ConfigurationView::new(self.state.clone(), window, cx))
             .into()
     }

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

@@ -71,7 +71,7 @@ impl State {
         };
         cx.spawn(async move |this, cx| {
             credentials_provider
-                .delete_credentials(&api_url, &cx)
+                .delete_credentials(&api_url, cx)
                 .await
                 .log_err();
             this.update(cx, |this, cx| {
@@ -92,7 +92,7 @@ impl State {
         };
         cx.spawn(async move |this, cx| {
             credentials_provider
-                .write_credentials(&api_url, "Bearer", api_key.as_bytes(), &cx)
+                .write_credentials(&api_url, "Bearer", api_key.as_bytes(), cx)
                 .await
                 .log_err();
             this.update(cx, |this, cx| {
@@ -119,7 +119,7 @@ impl State {
                 (api_key, true)
             } else {
                 let (_, api_key) = credentials_provider
-                    .read_credentials(&api_url, &cx)
+                    .read_credentials(&api_url, cx)
                     .await?
                     .ok_or(AuthenticateError::CredentialsNotFound)?;
                 (
@@ -230,7 +230,12 @@ impl LanguageModelProvider for VercelLanguageModelProvider {
         self.state.update(cx, |state, cx| state.authenticate(cx))
     }
 
-    fn configuration_view(&self, window: &mut Window, cx: &mut App) -> AnyView {
+    fn configuration_view(
+        &self,
+        _target_agent: language_model::ConfigurationViewTargetAgent,
+        window: &mut Window,
+        cx: &mut App,
+    ) -> AnyView {
         cx.new(|cx| ConfigurationView::new(self.state.clone(), window, cx))
             .into()
     }
@@ -355,7 +360,9 @@ impl LanguageModel for VercelLanguageModel {
             request,
             self.model.id(),
             self.model.supports_parallel_tool_calls(),
+            self.model.supports_prompt_cache_key(),
             self.max_output_tokens(),
+            None,
         );
         let completions = self.stream_completion(request, cx);
         async move {

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

@@ -71,7 +71,7 @@ impl State {
         };
         cx.spawn(async move |this, cx| {
             credentials_provider
-                .delete_credentials(&api_url, &cx)
+                .delete_credentials(&api_url, cx)
                 .await
                 .log_err();
             this.update(cx, |this, cx| {
@@ -92,7 +92,7 @@ impl State {
         };
         cx.spawn(async move |this, cx| {
             credentials_provider
-                .write_credentials(&api_url, "Bearer", api_key.as_bytes(), &cx)
+                .write_credentials(&api_url, "Bearer", api_key.as_bytes(), cx)
                 .await
                 .log_err();
             this.update(cx, |this, cx| {
@@ -119,7 +119,7 @@ impl State {
                 (api_key, true)
             } else {
                 let (_, api_key) = credentials_provider
-                    .read_credentials(&api_url, &cx)
+                    .read_credentials(&api_url, cx)
                     .await?
                     .ok_or(AuthenticateError::CredentialsNotFound)?;
                 (
@@ -230,7 +230,12 @@ impl LanguageModelProvider for XAiLanguageModelProvider {
         self.state.update(cx, |state, cx| state.authenticate(cx))
     }
 
-    fn configuration_view(&self, window: &mut Window, cx: &mut App) -> AnyView {
+    fn configuration_view(
+        &self,
+        _target_agent: language_model::ConfigurationViewTargetAgent,
+        window: &mut Window,
+        cx: &mut App,
+    ) -> AnyView {
         cx.new(|cx| ConfigurationView::new(self.state.clone(), window, cx))
             .into()
     }
@@ -359,7 +364,9 @@ impl LanguageModel for XAiLanguageModel {
             request,
             self.model.id(),
             self.model.supports_parallel_tool_calls(),
+            self.model.supports_prompt_cache_key(),
             self.max_output_tokens(),
+            None,
         );
         let completions = self.stream_completion(request, cx);
         async move {

crates/language_models/src/ui/instruction_list_item.rs 🔗

@@ -37,7 +37,7 @@ impl IntoElement for InstructionListItem {
         let item_content = if let (Some(button_label), Some(button_link)) =
             (self.button_label, self.button_link)
         {
-            let link = button_link.clone();
+            let link = button_link;
             let unique_id = SharedString::from(format!("{}-button", self.label));
 
             h_flex()
@@ -47,7 +47,7 @@ impl IntoElement for InstructionListItem {
                     Button::new(unique_id, button_label)
                         .style(ButtonStyle::Subtle)
                         .icon(IconName::ArrowUpRight)
-                        .icon_size(IconSize::XSmall)
+                        .icon_size(IconSize::Small)
                         .icon_color(Color::Muted)
                         .on_click(move |_, _window, cx| cx.open_url(&link)),
                 )

crates/language_selector/src/active_buffer_language.rs 🔗

@@ -28,10 +28,10 @@ impl ActiveBufferLanguage {
         self.active_language = Some(None);
 
         let editor = editor.read(cx);
-        if let Some((_, buffer, _)) = editor.active_excerpt(cx) {
-            if let Some(language) = buffer.read(cx).language() {
-                self.active_language = Some(Some(language.name()));
-            }
+        if let Some((_, buffer, _)) = editor.active_excerpt(cx)
+            && let Some(language) = buffer.read(cx).language()
+        {
+            self.active_language = Some(Some(language.name()));
         }
 
         cx.notify();

crates/language_tools/src/key_context_view.rs 🔗

@@ -71,12 +71,10 @@ impl KeyContextView {
                         } else {
                             None
                         }
+                    } else if this.action_matches(&e.action, binding.action()) {
+                        Some(true)
                     } else {
-                        if this.action_matches(&e.action, binding.action()) {
-                            Some(true)
-                        } else {
-                            Some(false)
-                        }
+                        Some(false)
                     };
                     let predicate = if let Some(predicate) = binding.predicate() {
                         format!("{}", predicate)
@@ -98,9 +96,7 @@ impl KeyContextView {
             cx.notify();
         });
         let sub2 = cx.observe_pending_input(window, |this, window, cx| {
-            this.pending_keystrokes = window
-                .pending_input_keystrokes()
-                .map(|k| k.iter().cloned().collect());
+            this.pending_keystrokes = window.pending_input_keystrokes().map(|k| k.to_vec());
             if this.pending_keystrokes.is_some() {
                 this.last_keystrokes.take();
             }

crates/language_tools/src/lsp_log.rs 🔗

@@ -254,35 +254,35 @@ impl LogStore {
         let copilot_subscription = Copilot::global(cx).map(|copilot| {
             let copilot = &copilot;
             cx.subscribe(copilot, |this, copilot, edit_prediction_event, cx| {
-                if let copilot::Event::CopilotLanguageServerStarted = edit_prediction_event {
-                    if let Some(server) = copilot.read(cx).language_server() {
-                        let server_id = server.server_id();
-                        let weak_this = cx.weak_entity();
-                        this.copilot_log_subscription =
-                            Some(server.on_notification::<copilot::request::LogMessage, _>(
-                                move |params, cx| {
-                                    weak_this
-                                        .update(cx, |this, cx| {
-                                            this.add_language_server_log(
-                                                server_id,
-                                                MessageType::LOG,
-                                                &params.message,
-                                                cx,
-                                            );
-                                        })
-                                        .ok();
-                                },
-                            ));
-                        let name = LanguageServerName::new_static("copilot");
-                        this.add_language_server(
-                            LanguageServerKind::Global,
-                            server.server_id(),
-                            Some(name),
-                            None,
-                            Some(server.clone()),
-                            cx,
-                        );
-                    }
+                if let copilot::Event::CopilotLanguageServerStarted = edit_prediction_event
+                    && let Some(server) = copilot.read(cx).language_server()
+                {
+                    let server_id = server.server_id();
+                    let weak_this = cx.weak_entity();
+                    this.copilot_log_subscription =
+                        Some(server.on_notification::<copilot::request::LogMessage, _>(
+                            move |params, cx| {
+                                weak_this
+                                    .update(cx, |this, cx| {
+                                        this.add_language_server_log(
+                                            server_id,
+                                            MessageType::LOG,
+                                            &params.message,
+                                            cx,
+                                        );
+                                    })
+                                    .ok();
+                            },
+                        ));
+                    let name = LanguageServerName::new_static("copilot");
+                    this.add_language_server(
+                        LanguageServerKind::Global,
+                        server.server_id(),
+                        Some(name),
+                        None,
+                        Some(server.clone()),
+                        cx,
+                    );
                 }
             })
         });
@@ -406,10 +406,7 @@ impl LogStore {
             server_state.worktree_id = Some(worktree_id);
         }
 
-        if let Some(server) = server
-            .clone()
-            .filter(|_| server_state.io_logs_subscription.is_none())
-        {
+        if let Some(server) = server.filter(|_| server_state.io_logs_subscription.is_none()) {
             let io_tx = self.io_tx.clone();
             let server_id = server.server_id();
             server_state.io_logs_subscription = Some(server.on_io(move |io_kind, message| {
@@ -661,7 +658,7 @@ impl LogStore {
             IoKind::StdOut => true,
             IoKind::StdIn => false,
             IoKind::StdErr => {
-                self.add_language_server_log(language_server_id, MessageType::LOG, &message, cx);
+                self.add_language_server_log(language_server_id, MessageType::LOG, message, cx);
                 return Some(());
             }
         };
@@ -733,16 +730,14 @@ impl LspLogView {
                 let first_server_id_for_project =
                     store.read(cx).server_ids_for_project(&weak_project).next();
                 if let Some(current_lsp) = this.current_server_id {
-                    if !store.read(cx).language_servers.contains_key(&current_lsp) {
-                        if let Some(server_id) = first_server_id_for_project {
-                            match this.active_entry_kind {
-                                LogKind::Rpc => {
-                                    this.show_rpc_trace_for_server(server_id, window, cx)
-                                }
-                                LogKind::Trace => this.show_trace_for_server(server_id, window, cx),
-                                LogKind::Logs => this.show_logs_for_server(server_id, window, cx),
-                                LogKind::ServerInfo => this.show_server_info(server_id, window, cx),
-                            }
+                    if !store.read(cx).language_servers.contains_key(&current_lsp)
+                        && let Some(server_id) = first_server_id_for_project
+                    {
+                        match this.active_entry_kind {
+                            LogKind::Rpc => this.show_rpc_trace_for_server(server_id, window, cx),
+                            LogKind::Trace => this.show_trace_for_server(server_id, window, cx),
+                            LogKind::Logs => this.show_logs_for_server(server_id, window, cx),
+                            LogKind::ServerInfo => this.show_server_info(server_id, window, cx),
                         }
                     }
                 } else if let Some(server_id) = first_server_id_for_project {
@@ -776,21 +771,17 @@ impl LspLogView {
                                 ],
                                 cx,
                             );
-                            if text.len() > 1024 {
-                                if let Some((fold_offset, _)) =
+                            if text.len() > 1024
+                                && let Some((fold_offset, _)) =
                                     text.char_indices().dropping(1024).next()
-                                {
-                                    if fold_offset < text.len() {
-                                        editor.fold_ranges(
-                                            vec![
-                                                last_offset + fold_offset..last_offset + text.len(),
-                                            ],
-                                            false,
-                                            window,
-                                            cx,
-                                        );
-                                    }
-                                }
+                                && fold_offset < text.len()
+                            {
+                                editor.fold_ranges(
+                                    vec![last_offset + fold_offset..last_offset + text.len()],
+                                    false,
+                                    window,
+                                    cx,
+                                );
                             }
 
                             if newest_cursor_is_at_end {
@@ -936,7 +927,7 @@ impl LspLogView {
                         let state = log_store.language_servers.get(&server_id)?;
                         Some(LogMenuItem {
                             server_id,
-                            server_name: name.clone(),
+                            server_name: name,
                             server_kind: state.kind.clone(),
                             worktree_root_name: "supplementary".to_string(),
                             rpc_trace_enabled: state.rpc_state.is_some(),
@@ -1311,14 +1302,14 @@ impl ToolbarItemView for LspLogToolbarItemView {
         _: &mut Window,
         cx: &mut Context<Self>,
     ) -> workspace::ToolbarItemLocation {
-        if let Some(item) = active_pane_item {
-            if let Some(log_view) = item.downcast::<LspLogView>() {
-                self.log_view = Some(log_view.clone());
-                self._log_view_subscription = Some(cx.observe(&log_view, |_, _, cx| {
-                    cx.notify();
-                }));
-                return ToolbarItemLocation::PrimaryLeft;
-            }
+        if let Some(item) = active_pane_item
+            && let Some(log_view) = item.downcast::<LspLogView>()
+        {
+            self.log_view = Some(log_view.clone());
+            self._log_view_subscription = Some(cx.observe(&log_view, |_, _, cx| {
+                cx.notify();
+            }));
+            return ToolbarItemLocation::PrimaryLeft;
         }
         self.log_view = None;
         self._log_view_subscription = None;
@@ -1358,7 +1349,7 @@ impl Render for LspLogToolbarItemView {
             })
             .collect();
 
-        let log_toolbar_view = cx.entity().clone();
+        let log_toolbar_view = cx.entity();
 
         let lsp_menu = PopoverMenu::new("LspLogView")
             .anchor(Corner::TopLeft)
@@ -1533,7 +1524,7 @@ impl Render for LspLogToolbarItemView {
                                             .icon_color(Color::Muted),
                                         )
                                         .menu({
-                                            let log_view = log_view.clone();
+                                            let log_view = log_view;
 
                                             move |window, cx| {
                                                 let id = log_view.read(cx).current_server_id?;
@@ -1601,7 +1592,7 @@ impl Render for LspLogToolbarItemView {
                                             .icon_color(Color::Muted),
                                         )
                                         .menu({
-                                            let log_view = log_view.clone();
+                                            let log_view = log_view;
 
                                             move |window, cx| {
                                                 let id = log_view.read(cx).current_server_id?;

crates/language_tools/src/lsp_tool.rs 🔗

@@ -349,7 +349,6 @@ impl LanguageServerState {
                             .detach();
                         } else {
                             cx.propagate();
-                            return;
                         }
                     }
                 },
@@ -523,7 +522,6 @@ impl LspTool {
                 if ProjectSettings::get_global(cx).global_lsp_settings.button {
                     if lsp_tool.lsp_menu.is_none() {
                         lsp_tool.refresh_lsp_menu(true, window, cx);
-                        return;
                     }
                 } else if lsp_tool.lsp_menu.take().is_some() {
                     cx.notify();
@@ -1007,7 +1005,7 @@ impl Render for LspTool {
             (None, "All Servers Operational")
         };
 
-        let lsp_tool = cx.entity().clone();
+        let lsp_tool = cx.entity();
 
         div().child(
             PopoverMenu::new("lsp-tool")

crates/language_tools/src/syntax_tree_view.rs 🔗

@@ -103,12 +103,11 @@ impl SyntaxTreeView {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        if let Some(item) = active_item {
-            if item.item_id() != cx.entity_id() {
-                if let Some(editor) = item.act_as::<Editor>(cx) {
-                    self.set_editor(editor, window, cx);
-                }
-            }
+        if let Some(item) = active_item
+            && item.item_id() != cx.entity_id()
+            && let Some(editor) = item.act_as::<Editor>(cx)
+        {
+            self.set_editor(editor, window, cx);
         }
     }
 
@@ -157,7 +156,7 @@ impl SyntaxTreeView {
                 .buffer_snapshot
                 .range_to_buffer_ranges(selection_range)
                 .pop()?;
-            let buffer = multi_buffer.buffer(buffer.remote_id()).unwrap().clone();
+            let buffer = multi_buffer.buffer(buffer.remote_id()).unwrap();
             Some((buffer, range, excerpt_id))
         })?;
 
@@ -456,7 +455,7 @@ impl SyntaxTreeToolbarItemView {
         let active_layer = buffer_state.active_layer.clone()?;
         let active_buffer = buffer_state.buffer.read(cx).snapshot();
 
-        let view = cx.entity().clone();
+        let view = cx.entity();
         Some(
             PopoverMenu::new("Syntax Tree")
                 .trigger(Self::render_header(&active_layer))
@@ -537,12 +536,12 @@ impl ToolbarItemView for SyntaxTreeToolbarItemView {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) -> ToolbarItemLocation {
-        if let Some(item) = active_pane_item {
-            if let Some(view) = item.downcast::<SyntaxTreeView>() {
-                self.tree_view = Some(view.clone());
-                self.subscription = Some(cx.observe_in(&view, window, |_, _, _, cx| cx.notify()));
-                return ToolbarItemLocation::PrimaryLeft;
-            }
+        if let Some(item) = active_pane_item
+            && let Some(view) = item.downcast::<SyntaxTreeView>()
+        {
+            self.tree_view = Some(view.clone());
+            self.subscription = Some(cx.observe_in(&view, window, |_, _, _, cx| cx.notify()));
+            return ToolbarItemLocation::PrimaryLeft;
         }
         self.tree_view = None;
         self.subscription = None;

crates/languages/src/c.rs 🔗

@@ -22,13 +22,13 @@ impl CLspAdapter {
 #[async_trait(?Send)]
 impl super::LspAdapter for CLspAdapter {
     fn name(&self) -> LanguageServerName {
-        Self::SERVER_NAME.clone()
+        Self::SERVER_NAME
     }
 
     async fn check_if_user_installed(
         &self,
         delegate: &dyn LspAdapterDelegate,
-        _: Arc<dyn LanguageToolchainStore>,
+        _: Option<Toolchain>,
         _: &AsyncApp,
     ) -> Option<LanguageServerBinary> {
         let path = delegate.which(Self::SERVER_NAME.as_ref()).await?;
@@ -71,8 +71,11 @@ impl super::LspAdapter for CLspAdapter {
         container_dir: PathBuf,
         delegate: &dyn LspAdapterDelegate,
     ) -> Result<LanguageServerBinary> {
-        let GitHubLspBinaryVersion { name, url, digest } =
-            &*version.downcast::<GitHubLspBinaryVersion>().unwrap();
+        let GitHubLspBinaryVersion {
+            name,
+            url,
+            digest: expected_digest,
+        } = *version.downcast::<GitHubLspBinaryVersion>().unwrap();
         let version_dir = container_dir.join(format!("clangd_{name}"));
         let binary_path = version_dir.join("bin/clangd");
 
@@ -99,7 +102,9 @@ impl super::LspAdapter for CLspAdapter {
                         log::warn!("Unable to run {binary_path:?} asset, redownloading: {err}",)
                     })
             };
-            if let (Some(actual_digest), Some(expected_digest)) = (&metadata.digest, digest) {
+            if let (Some(actual_digest), Some(expected_digest)) =
+                (&metadata.digest, &expected_digest)
+            {
                 if actual_digest == expected_digest {
                     if validity_check().await.is_ok() {
                         return Ok(binary);
@@ -115,8 +120,8 @@ impl super::LspAdapter for CLspAdapter {
         }
         download_server_binary(
             delegate,
-            url,
-            digest.as_deref(),
+            &url,
+            expected_digest.as_deref(),
             &container_dir,
             AssetKind::Zip,
         )
@@ -125,7 +130,7 @@ impl super::LspAdapter for CLspAdapter {
         GithubBinaryMetadata::write_to_file(
             &GithubBinaryMetadata {
                 metadata_version: 1,
-                digest: digest.clone(),
+                digest: expected_digest,
             },
             &metadata_path,
         )
@@ -248,8 +253,7 @@ impl super::LspAdapter for CLspAdapter {
                     .grammar()
                     .and_then(|g| g.highlight_id_for_name(highlight_name?))
                 {
-                    let mut label =
-                        CodeLabel::plain(label.to_string(), completion.filter_text.as_deref());
+                    let mut label = CodeLabel::plain(label, completion.filter_text.as_deref());
                     label.runs.push((
                         0..label.text.rfind('(').unwrap_or(label.text.len()),
                         highlight_id,
@@ -259,10 +263,7 @@ impl super::LspAdapter for CLspAdapter {
             }
             _ => {}
         }
-        Some(CodeLabel::plain(
-            label.to_string(),
-            completion.filter_text.as_deref(),
-        ))
+        Some(CodeLabel::plain(label, completion.filter_text.as_deref()))
     }
 
     async fn label_for_symbol(

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

@@ -149,7 +149,9 @@
                 parameters: (parameter_list
                     "(" @context
                     ")" @context)))
-    ]
-    (type_qualifier)? @context) @item
+    ; Fields declarations may define multiple fields, and so @item is on the
+    ; declarator so they each get distinct ranges.
+    ] @item
+    (type_qualifier)? @context)
 
 (comment) @annotation

crates/languages/src/css.rs 🔗

@@ -2,9 +2,9 @@ use anyhow::{Context as _, Result};
 use async_trait::async_trait;
 use futures::StreamExt;
 use gpui::AsyncApp;
-use language::{LanguageToolchainStore, LspAdapter, LspAdapterDelegate};
+use language::{LspAdapter, LspAdapterDelegate, Toolchain};
 use lsp::{LanguageServerBinary, LanguageServerName};
-use node_runtime::NodeRuntime;
+use node_runtime::{NodeRuntime, VersionStrategy};
 use project::{Fs, lsp_store::language_server_settings};
 use serde_json::json;
 use smol::fs;
@@ -43,7 +43,7 @@ impl LspAdapter for CssLspAdapter {
     async fn check_if_user_installed(
         &self,
         delegate: &dyn LspAdapterDelegate,
-        _: Arc<dyn LanguageToolchainStore>,
+        _: Option<Toolchain>,
         _: &AsyncApp,
     ) -> Option<LanguageServerBinary> {
         let path = delegate
@@ -103,7 +103,12 @@ impl LspAdapter for CssLspAdapter {
 
         let should_install_language_server = self
             .node
-            .should_install_npm_package(Self::PACKAGE_NAME, &server_path, &container_dir, &version)
+            .should_install_npm_package(
+                Self::PACKAGE_NAME,
+                &server_path,
+                container_dir,
+                VersionStrategy::Latest(version),
+            )
             .await;
 
         if should_install_language_server {
@@ -139,7 +144,7 @@ impl LspAdapter for CssLspAdapter {
         self: Arc<Self>,
         _: &dyn Fs,
         delegate: &Arc<dyn LspAdapterDelegate>,
-        _: Arc<dyn LanguageToolchainStore>,
+        _: Option<Toolchain>,
         cx: &mut AsyncApp,
     ) -> Result<serde_json::Value> {
         let mut default_config = json!({

crates/languages/src/github_download.rs 🔗

@@ -18,9 +18,8 @@ impl GithubBinaryMetadata {
         let metadata_content = async_fs::read_to_string(metadata_path)
             .await
             .with_context(|| format!("reading metadata file at {metadata_path:?}"))?;
-        let metadata: GithubBinaryMetadata = serde_json::from_str(&metadata_content)
-            .with_context(|| format!("parsing metadata file at {metadata_path:?}"))?;
-        Ok(metadata)
+        serde_json::from_str(&metadata_content)
+            .with_context(|| format!("parsing metadata file at {metadata_path:?}"))
     }
 
     pub(crate) async fn write_to_file(&self, metadata_path: &Path) -> Result<()> {
@@ -62,6 +61,7 @@ pub(crate) async fn download_server_binary(
                     format!("saving archive contents into the temporary file for {url}",)
                 })?;
             let asset_sha_256 = format!("{:x}", writer.hasher.finalize());
+
             anyhow::ensure!(
                 asset_sha_256 == expected_sha_256,
                 "{url} asset got SHA-256 mismatch. Expected: {expected_sha_256}, Got: {asset_sha_256}",
@@ -96,7 +96,7 @@ async fn stream_response_archive(
         AssetKind::TarGz => extract_tar_gz(destination_path, url, response).await?,
         AssetKind::Gz => extract_gz(destination_path, url, response).await?,
         AssetKind::Zip => {
-            util::archive::extract_zip(&destination_path, response).await?;
+            util::archive::extract_zip(destination_path, response).await?;
         }
     };
     Ok(())
@@ -113,11 +113,11 @@ async fn stream_file_archive(
         AssetKind::Gz => extract_gz(destination_path, url, file_archive).await?,
         #[cfg(not(windows))]
         AssetKind::Zip => {
-            util::archive::extract_seekable_zip(&destination_path, file_archive).await?;
+            util::archive::extract_seekable_zip(destination_path, file_archive).await?;
         }
         #[cfg(windows)]
         AssetKind::Zip => {
-            util::archive::extract_zip(&destination_path, file_archive).await?;
+            util::archive::extract_zip(destination_path, file_archive).await?;
         }
     };
     Ok(())

crates/languages/src/go.rs 🔗

@@ -53,7 +53,7 @@ const BINARY: &str = if cfg!(target_os = "windows") {
 #[async_trait(?Send)]
 impl super::LspAdapter for GoLspAdapter {
     fn name(&self) -> LanguageServerName {
-        Self::SERVER_NAME.clone()
+        Self::SERVER_NAME
     }
 
     async fn fetch_latest_server_version(
@@ -75,7 +75,7 @@ impl super::LspAdapter for GoLspAdapter {
     async fn check_if_user_installed(
         &self,
         delegate: &dyn LspAdapterDelegate,
-        _: Arc<dyn LanguageToolchainStore>,
+        _: Option<Toolchain>,
         _: &AsyncApp,
     ) -> Option<LanguageServerBinary> {
         let path = delegate.which(Self::SERVER_NAME.as_ref()).await?;
@@ -131,19 +131,19 @@ impl super::LspAdapter for GoLspAdapter {
 
         if let Some(version) = *version {
             let binary_path = container_dir.join(format!("gopls_{version}_go_{go_version}"));
-            if let Ok(metadata) = fs::metadata(&binary_path).await {
-                if metadata.is_file() {
-                    remove_matching(&container_dir, |entry| {
-                        entry != binary_path && entry.file_name() != Some(OsStr::new("gobin"))
-                    })
-                    .await;
-
-                    return Ok(LanguageServerBinary {
-                        path: binary_path.to_path_buf(),
-                        arguments: server_binary_arguments(),
-                        env: None,
-                    });
-                }
+            if let Ok(metadata) = fs::metadata(&binary_path).await
+                && metadata.is_file()
+            {
+                remove_matching(&container_dir, |entry| {
+                    entry != binary_path && entry.file_name() != Some(OsStr::new("gobin"))
+                })
+                .await;
+
+                return Ok(LanguageServerBinary {
+                    path: binary_path.to_path_buf(),
+                    arguments: server_binary_arguments(),
+                    env: None,
+                });
             }
         } else if let Some(path) = this
             .cached_server_binary(container_dir.clone(), delegate)
@@ -452,7 +452,7 @@ async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServ
                 && entry
                     .file_name()
                     .to_str()
-                    .map_or(false, |name| name.starts_with("gopls_"))
+                    .is_some_and(|name| name.starts_with("gopls_"))
             {
                 last_binary_path = Some(entry.path());
             }
@@ -487,6 +487,8 @@ const GO_MODULE_ROOT_TASK_VARIABLE: VariableName =
     VariableName::Custom(Cow::Borrowed("GO_MODULE_ROOT"));
 const GO_SUBTEST_NAME_TASK_VARIABLE: VariableName =
     VariableName::Custom(Cow::Borrowed("GO_SUBTEST_NAME"));
+const GO_TABLE_TEST_CASE_NAME_TASK_VARIABLE: VariableName =
+    VariableName::Custom(Cow::Borrowed("GO_TABLE_TEST_CASE_NAME"));
 
 impl ContextProvider for GoContextProvider {
     fn build_context(
@@ -523,7 +525,7 @@ impl ContextProvider for GoContextProvider {
                     })
                     .unwrap_or_else(|| format!("{}", buffer_dir.to_string_lossy()));
 
-                (GO_PACKAGE_TASK_VARIABLE.clone(), package_name.to_string())
+                (GO_PACKAGE_TASK_VARIABLE.clone(), package_name)
             });
 
         let go_module_root_variable = local_abs_path
@@ -545,10 +547,19 @@ impl ContextProvider for GoContextProvider {
         let go_subtest_variable = extract_subtest_name(_subtest_name.unwrap_or(""))
             .map(|subtest_name| (GO_SUBTEST_NAME_TASK_VARIABLE.clone(), subtest_name));
 
+        let table_test_case_name = variables.get(&VariableName::Custom(Cow::Borrowed(
+            "_table_test_case_name",
+        )));
+
+        let go_table_test_case_variable = table_test_case_name
+            .and_then(extract_subtest_name)
+            .map(|case_name| (GO_TABLE_TEST_CASE_NAME_TASK_VARIABLE.clone(), case_name));
+
         Task::ready(Ok(TaskVariables::from_iter(
             [
                 go_package_variable,
                 go_subtest_variable,
+                go_table_test_case_variable,
                 go_module_root_variable,
             ]
             .into_iter()
@@ -570,6 +581,28 @@ impl ContextProvider for GoContextProvider {
         let module_cwd = Some(GO_MODULE_ROOT_TASK_VARIABLE.template_value());
 
         Task::ready(Some(TaskTemplates(vec![
+            TaskTemplate {
+                label: format!(
+                    "go test {} -v -run {}/{}",
+                    GO_PACKAGE_TASK_VARIABLE.template_value(),
+                    VariableName::Symbol.template_value(),
+                    GO_TABLE_TEST_CASE_NAME_TASK_VARIABLE.template_value(),
+                ),
+                command: "go".into(),
+                args: vec![
+                    "test".into(),
+                    "-v".into(),
+                    "-run".into(),
+                    format!(
+                        "\\^{}\\$/\\^{}\\$",
+                        VariableName::Symbol.template_value(),
+                        GO_TABLE_TEST_CASE_NAME_TASK_VARIABLE.template_value(),
+                    ),
+                ],
+                cwd: package_cwd.clone(),
+                tags: vec!["go-table-test-case".to_owned()],
+                ..TaskTemplate::default()
+            },
             TaskTemplate {
                 label: format!(
                     "go test {} -run {}",
@@ -669,7 +702,7 @@ impl ContextProvider for GoContextProvider {
                 label: format!("go generate {}", GO_PACKAGE_TASK_VARIABLE.template_value()),
                 command: "go".into(),
                 args: vec!["generate".into()],
-                cwd: package_cwd.clone(),
+                cwd: package_cwd,
                 tags: vec!["go-generate".to_owned()],
                 ..TaskTemplate::default()
             },
@@ -677,7 +710,7 @@ impl ContextProvider for GoContextProvider {
                 label: "go generate ./...".into(),
                 command: "go".into(),
                 args: vec!["generate".into(), "./...".into()],
-                cwd: module_cwd.clone(),
+                cwd: module_cwd,
                 ..TaskTemplate::default()
             },
         ])))
@@ -842,10 +875,21 @@ mod tests {
                 .collect()
         });
 
+        let tag_strings: Vec<String> = runnables
+            .iter()
+            .flat_map(|r| &r.runnable.tags)
+            .map(|tag| tag.0.to_string())
+            .collect();
+
+        assert!(
+            tag_strings.contains(&"go-test".to_string()),
+            "Should find go-test tag, found: {:?}",
+            tag_strings
+        );
         assert!(
-            runnables.len() == 2,
-            "Should find test function and subtest with double quotes, found: {}",
-            runnables.len()
+            tag_strings.contains(&"go-subtest".to_string()),
+            "Should find go-subtest tag, found: {:?}",
+            tag_strings
         );
 
         let buffer = cx.new(|cx| {
@@ -860,10 +904,299 @@ mod tests {
                 .collect()
         });
 
+        let tag_strings: Vec<String> = runnables
+            .iter()
+            .flat_map(|r| &r.runnable.tags)
+            .map(|tag| tag.0.to_string())
+            .collect();
+
+        assert!(
+            tag_strings.contains(&"go-test".to_string()),
+            "Should find go-test tag, found: {:?}",
+            tag_strings
+        );
+        assert!(
+            tag_strings.contains(&"go-subtest".to_string()),
+            "Should find go-subtest tag, found: {:?}",
+            tag_strings
+        );
+    }
+
+    #[gpui::test]
+    fn test_go_table_test_slice_detection(cx: &mut TestAppContext) {
+        let language = language("go", tree_sitter_go::LANGUAGE.into());
+
+        let table_test = r#"
+        package main
+
+        import "testing"
+
+        func TestExample(t *testing.T) {
+            _ = "some random string"
+
+            testCases := []struct{
+                name string
+                anotherStr string
+            }{
+                {
+                    name: "test case 1",
+                    anotherStr: "foo",
+                },
+                {
+                    name: "test case 2",
+                    anotherStr: "bar",
+                },
+            }
+
+            notATableTest := []struct{
+                name string
+            }{
+                {
+                    name: "some string",
+                },
+                {
+                    name: "some other string",
+                },
+            }
+
+            for _, tc := range testCases {
+                t.Run(tc.name, func(t *testing.T) {
+                    // test code here
+                })
+            }
+        }
+        "#;
+
+        let buffer =
+            cx.new(|cx| crate::Buffer::local(table_test, cx).with_language(language.clone(), cx));
+        cx.executor().run_until_parked();
+
+        let runnables: Vec<_> = buffer.update(cx, |buffer, _| {
+            let snapshot = buffer.snapshot();
+            snapshot.runnable_ranges(0..table_test.len()).collect()
+        });
+
+        let tag_strings: Vec<String> = runnables
+            .iter()
+            .flat_map(|r| &r.runnable.tags)
+            .map(|tag| tag.0.to_string())
+            .collect();
+
+        assert!(
+            tag_strings.contains(&"go-test".to_string()),
+            "Should find go-test tag, found: {:?}",
+            tag_strings
+        );
+        assert!(
+            tag_strings.contains(&"go-table-test-case".to_string()),
+            "Should find go-table-test-case tag, found: {:?}",
+            tag_strings
+        );
+
+        let go_test_count = tag_strings.iter().filter(|&tag| tag == "go-test").count();
+        let go_table_test_count = tag_strings
+            .iter()
+            .filter(|&tag| tag == "go-table-test-case")
+            .count();
+
+        assert!(
+            go_test_count == 1,
+            "Should find exactly 1 go-test, found: {}",
+            go_test_count
+        );
+        assert!(
+            go_table_test_count == 2,
+            "Should find exactly 2 go-table-test-case, found: {}",
+            go_table_test_count
+        );
+    }
+
+    #[gpui::test]
+    fn test_go_table_test_slice_ignored(cx: &mut TestAppContext) {
+        let language = language("go", tree_sitter_go::LANGUAGE.into());
+
+        let table_test = r#"
+        package main
+
+        func Example() {
+            _ = "some random string"
+
+            notATableTest := []struct{
+                name string
+            }{
+                {
+                    name: "some string",
+                },
+                {
+                    name: "some other string",
+                },
+            }
+        }
+        "#;
+
+        let buffer =
+            cx.new(|cx| crate::Buffer::local(table_test, cx).with_language(language.clone(), cx));
+        cx.executor().run_until_parked();
+
+        let runnables: Vec<_> = buffer.update(cx, |buffer, _| {
+            let snapshot = buffer.snapshot();
+            snapshot.runnable_ranges(0..table_test.len()).collect()
+        });
+
+        let tag_strings: Vec<String> = runnables
+            .iter()
+            .flat_map(|r| &r.runnable.tags)
+            .map(|tag| tag.0.to_string())
+            .collect();
+
+        assert!(
+            !tag_strings.contains(&"go-test".to_string()),
+            "Should find go-test tag, found: {:?}",
+            tag_strings
+        );
+        assert!(
+            !tag_strings.contains(&"go-table-test-case".to_string()),
+            "Should find go-table-test-case tag, found: {:?}",
+            tag_strings
+        );
+    }
+
+    #[gpui::test]
+    fn test_go_table_test_map_detection(cx: &mut TestAppContext) {
+        let language = language("go", tree_sitter_go::LANGUAGE.into());
+
+        let table_test = r#"
+        package main
+
+        import "testing"
+
+        func TestExample(t *testing.T) {
+            _ = "some random string"
+
+           	testCases := map[string]struct {
+          		someStr string
+          		fail    bool
+           	}{
+          		"test failure": {
+         			someStr: "foo",
+         			fail:    true,
+          		},
+          		"test success": {
+         			someStr: "bar",
+         			fail:    false,
+          		},
+           	}
+
+           	notATableTest := map[string]struct {
+          		someStr string
+           	}{
+          		"some string": {
+         			someStr: "foo",
+          		},
+          		"some other string": {
+         			someStr: "bar",
+          		},
+           	}
+
+            for name, tc := range testCases {
+                t.Run(name, func(t *testing.T) {
+                    // test code here
+                })
+            }
+        }
+        "#;
+
+        let buffer =
+            cx.new(|cx| crate::Buffer::local(table_test, cx).with_language(language.clone(), cx));
+        cx.executor().run_until_parked();
+
+        let runnables: Vec<_> = buffer.update(cx, |buffer, _| {
+            let snapshot = buffer.snapshot();
+            snapshot.runnable_ranges(0..table_test.len()).collect()
+        });
+
+        let tag_strings: Vec<String> = runnables
+            .iter()
+            .flat_map(|r| &r.runnable.tags)
+            .map(|tag| tag.0.to_string())
+            .collect();
+
+        assert!(
+            tag_strings.contains(&"go-test".to_string()),
+            "Should find go-test tag, found: {:?}",
+            tag_strings
+        );
+        assert!(
+            tag_strings.contains(&"go-table-test-case".to_string()),
+            "Should find go-table-test-case tag, found: {:?}",
+            tag_strings
+        );
+
+        let go_test_count = tag_strings.iter().filter(|&tag| tag == "go-test").count();
+        let go_table_test_count = tag_strings
+            .iter()
+            .filter(|&tag| tag == "go-table-test-case")
+            .count();
+
+        assert!(
+            go_test_count == 1,
+            "Should find exactly 1 go-test, found: {}",
+            go_test_count
+        );
+        assert!(
+            go_table_test_count == 2,
+            "Should find exactly 2 go-table-test-case, found: {}",
+            go_table_test_count
+        );
+    }
+
+    #[gpui::test]
+    fn test_go_table_test_map_ignored(cx: &mut TestAppContext) {
+        let language = language("go", tree_sitter_go::LANGUAGE.into());
+
+        let table_test = r#"
+        package main
+
+        func Example() {
+            _ = "some random string"
+
+           	notATableTest := map[string]struct {
+          		someStr string
+           	}{
+          		"some string": {
+         			someStr: "foo",
+          		},
+          		"some other string": {
+         			someStr: "bar",
+          		},
+           	}
+        }
+        "#;
+
+        let buffer =
+            cx.new(|cx| crate::Buffer::local(table_test, cx).with_language(language.clone(), cx));
+        cx.executor().run_until_parked();
+
+        let runnables: Vec<_> = buffer.update(cx, |buffer, _| {
+            let snapshot = buffer.snapshot();
+            snapshot.runnable_ranges(0..table_test.len()).collect()
+        });
+
+        let tag_strings: Vec<String> = runnables
+            .iter()
+            .flat_map(|r| &r.runnable.tags)
+            .map(|tag| tag.0.to_string())
+            .collect();
+
+        assert!(
+            !tag_strings.contains(&"go-test".to_string()),
+            "Should find go-test tag, found: {:?}",
+            tag_strings
+        );
         assert!(
-            runnables.len() == 2,
-            "Should find test function and subtest with backticks, found: {}",
-            runnables.len()
+            !tag_strings.contains(&"go-table-test-case".to_string()),
+            "Should find go-table-test-case tag, found: {:?}",
+            tag_strings
         );
     }
 

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

@@ -1,4 +1,5 @@
 (comment) @annotation
+
 (type_declaration
     "type" @context
     [
@@ -42,13 +43,13 @@
     (var_declaration
         "var" @context
         [
+            ; The declaration may define multiple variables, and so @item is on
+            ; the identifier so they get distinct ranges.
             (var_spec
-                name: (identifier) @name) @item
+                name: (identifier) @name @item)
             (var_spec_list
-                "("
                 (var_spec
-                    name: (identifier) @name) @item
-                ")"
+                    name: (identifier) @name @item)
             )
         ]
      )
@@ -60,5 +61,7 @@
       "(" @context
       ")" @context)) @item
 
+; Fields declarations may define multiple fields, and so @item is on the
+; declarator so they each get distinct ranges.
 (field_declaration
-    name: (_) @name) @item
+    name: (_) @name @item)

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

@@ -91,3 +91,103 @@
   ) @_
   (#set! tag go-main)
 )
+
+; Table test cases - slice and map
+(
+  (short_var_declaration
+    left: (expression_list (identifier) @_collection_var)
+    right: (expression_list
+      (composite_literal
+        type: [
+          (slice_type)
+          (map_type
+            key: (type_identifier) @_key_type
+            (#eq? @_key_type "string")
+          )
+        ]
+        body: (literal_value
+          [
+            (literal_element
+              (literal_value
+                (keyed_element
+                  (literal_element
+                    (identifier) @_field_name
+                  )
+                  (literal_element
+                    [
+                      (interpreted_string_literal) @run @_table_test_case_name
+                      (raw_string_literal) @run @_table_test_case_name
+                    ]
+                  )
+                )
+              )
+            )
+            (keyed_element
+              (literal_element
+                [
+                  (interpreted_string_literal) @run @_table_test_case_name
+                  (raw_string_literal) @run @_table_test_case_name
+                ]
+              )
+            )
+          ]
+        )
+      )
+    )
+  )
+  (for_statement
+    (range_clause
+      left: (expression_list
+        [
+          (
+            (identifier)
+            (identifier) @_loop_var
+          )
+          (identifier) @_loop_var
+        ]
+      )
+      right: (identifier) @_range_var
+      (#eq? @_range_var @_collection_var)
+    )
+    body: (block
+      (expression_statement
+        (call_expression
+          function: (selector_expression
+            operand: (identifier) @_t_var
+            field: (field_identifier) @_run_method
+            (#eq? @_run_method "Run")
+          )
+          arguments: (argument_list
+            .
+            [
+              (selector_expression
+                operand: (identifier) @_tc_var
+                (#eq? @_tc_var @_loop_var)
+                field: (field_identifier) @_field_check
+                (#eq? @_field_check @_field_name)
+              )
+              (identifier) @_arg_var
+              (#eq? @_arg_var @_loop_var)
+            ]
+            .
+            (func_literal
+              parameters: (parameter_list
+                (parameter_declaration
+                  type: (pointer_type
+                    (qualified_type
+                      package: (package_identifier) @_pkg
+                      name: (type_identifier) @_type
+                      (#eq? @_pkg "testing")
+                      (#eq? @_type "T")
+                    )
+                  )
+                )
+              )
+            )
+          )
+        )
+      )
+    )
+  ) @_
+  (#set! tag go-table-test-case)
+)

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

@@ -31,12 +31,16 @@
     (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)))
 
 (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))
 

crates/languages/src/json.rs 🔗

@@ -8,11 +8,11 @@ use futures::StreamExt;
 use gpui::{App, AsyncApp, Task};
 use http_client::github::{GitHubLspBinaryVersion, latest_github_release};
 use language::{
-    ContextProvider, LanguageName, LanguageRegistry, LanguageToolchainStore, LocalFile as _,
-    LspAdapter, LspAdapterDelegate,
+    ContextProvider, LanguageName, LanguageRegistry, LocalFile as _, LspAdapter,
+    LspAdapterDelegate, Toolchain,
 };
 use lsp::{LanguageServerBinary, LanguageServerName};
-use node_runtime::NodeRuntime;
+use node_runtime::{NodeRuntime, VersionStrategy};
 use project::{Fs, lsp_store::language_server_settings};
 use serde_json::{Value, json};
 use settings::{KeymapFile, SettingsJsonSchemaParams, SettingsStore};
@@ -234,7 +234,7 @@ impl JsonLspAdapter {
         schemas
             .as_array_mut()
             .unwrap()
-            .extend(cx.all_action_names().into_iter().map(|&name| {
+            .extend(cx.all_action_names().iter().map(|&name| {
                 project::lsp_store::json_language_server_ext::url_schema_for_action(name)
             }));
 
@@ -280,7 +280,7 @@ impl JsonLspAdapter {
             )
         })?;
         writer.replace(config.clone());
-        return Ok(config);
+        Ok(config)
     }
 }
 
@@ -303,7 +303,7 @@ impl LspAdapter for JsonLspAdapter {
     async fn check_if_user_installed(
         &self,
         delegate: &dyn LspAdapterDelegate,
-        _: Arc<dyn LanguageToolchainStore>,
+        _: Option<Toolchain>,
         _: &AsyncApp,
     ) -> Option<LanguageServerBinary> {
         let path = delegate
@@ -340,7 +340,12 @@ impl LspAdapter for JsonLspAdapter {
 
         let should_install_language_server = self
             .node
-            .should_install_npm_package(Self::PACKAGE_NAME, &server_path, &container_dir, &version)
+            .should_install_npm_package(
+                Self::PACKAGE_NAME,
+                &server_path,
+                container_dir,
+                VersionStrategy::Latest(version),
+            )
             .await;
 
         if should_install_language_server {
@@ -399,7 +404,7 @@ impl LspAdapter for JsonLspAdapter {
         self: Arc<Self>,
         _: &dyn Fs,
         delegate: &Arc<dyn LspAdapterDelegate>,
-        _: Arc<dyn LanguageToolchainStore>,
+        _: Option<Toolchain>,
         cx: &mut AsyncApp,
     ) -> Result<Value> {
         let mut config = self.get_or_init_workspace_config(cx).await?;
@@ -483,7 +488,7 @@ impl NodeVersionAdapter {
 #[async_trait(?Send)]
 impl LspAdapter for NodeVersionAdapter {
     fn name(&self) -> LanguageServerName {
-        Self::SERVER_NAME.clone()
+        Self::SERVER_NAME
     }
 
     async fn fetch_latest_server_version(
@@ -524,7 +529,7 @@ impl LspAdapter for NodeVersionAdapter {
     async fn check_if_user_installed(
         &self,
         delegate: &dyn LspAdapterDelegate,
-        _: Arc<dyn LanguageToolchainStore>,
+        _: Option<Toolchain>,
         _: &AsyncApp,
     ) -> Option<LanguageServerBinary> {
         let path = delegate.which(Self::SERVER_NAME.as_ref()).await?;

crates/languages/src/lib.rs 🔗

@@ -1,6 +1,6 @@
 use anyhow::Context as _;
 use feature_flags::{FeatureFlag, FeatureFlagAppExt as _};
-use gpui::{App, UpdateGlobal};
+use gpui::{App, SharedString, UpdateGlobal};
 use node_runtime::NodeRuntime;
 use python::PyprojectTomlManifestProvider;
 use rust::CargoManifestProvider;
@@ -104,7 +104,7 @@ pub fn init(languages: Arc<LanguageRegistry>, node: NodeRuntime, cx: &mut App) {
     let typescript_context = Arc::new(typescript::TypeScriptContextProvider::new());
     let typescript_lsp_adapter = Arc::new(typescript::TypeScriptLspAdapter::new(node.clone()));
     let vtsls_adapter = Arc::new(vtsls::VtslsLspAdapter::new(node.clone()));
-    let yaml_lsp_adapter = Arc::new(yaml::YamlLspAdapter::new(node.clone()));
+    let yaml_lsp_adapter = Arc::new(yaml::YamlLspAdapter::new(node));
 
     let built_in_languages = [
         LanguageInfo {
@@ -119,12 +119,12 @@ pub fn init(languages: Arc<LanguageRegistry>, node: NodeRuntime, cx: &mut App) {
         },
         LanguageInfo {
             name: "cpp",
-            adapters: vec![c_lsp_adapter.clone()],
+            adapters: vec![c_lsp_adapter],
             ..Default::default()
         },
         LanguageInfo {
             name: "css",
-            adapters: vec![css_lsp_adapter.clone()],
+            adapters: vec![css_lsp_adapter],
             ..Default::default()
         },
         LanguageInfo {
@@ -146,20 +146,20 @@ pub fn init(languages: Arc<LanguageRegistry>, node: NodeRuntime, cx: &mut App) {
         },
         LanguageInfo {
             name: "gowork",
-            adapters: vec![go_lsp_adapter.clone()],
-            context: Some(go_context_provider.clone()),
+            adapters: vec![go_lsp_adapter],
+            context: Some(go_context_provider),
             ..Default::default()
         },
         LanguageInfo {
             name: "json",
-            adapters: vec![json_lsp_adapter.clone(), node_version_lsp_adapter.clone()],
+            adapters: vec![json_lsp_adapter.clone(), node_version_lsp_adapter],
             context: Some(json_context_provider.clone()),
             ..Default::default()
         },
         LanguageInfo {
             name: "jsonc",
-            adapters: vec![json_lsp_adapter.clone()],
-            context: Some(json_context_provider.clone()),
+            adapters: vec![json_lsp_adapter],
+            context: Some(json_context_provider),
             ..Default::default()
         },
         LanguageInfo {
@@ -174,14 +174,16 @@ pub fn init(languages: Arc<LanguageRegistry>, node: NodeRuntime, cx: &mut App) {
         },
         LanguageInfo {
             name: "python",
-            adapters: vec![python_lsp_adapter.clone(), py_lsp_adapter.clone()],
+            adapters: vec![python_lsp_adapter, py_lsp_adapter],
             context: Some(python_context_provider),
             toolchain: Some(python_toolchain_provider),
+            manifest_name: Some(SharedString::new_static("pyproject.toml").into()),
         },
         LanguageInfo {
             name: "rust",
             adapters: vec![rust_lsp_adapter],
             context: Some(rust_context_provider),
+            manifest_name: Some(SharedString::new_static("Cargo.toml").into()),
             ..Default::default()
         },
         LanguageInfo {
@@ -199,7 +201,7 @@ pub fn init(languages: Arc<LanguageRegistry>, node: NodeRuntime, cx: &mut App) {
         LanguageInfo {
             name: "javascript",
             adapters: vec![typescript_lsp_adapter.clone(), vtsls_adapter.clone()],
-            context: Some(typescript_context.clone()),
+            context: Some(typescript_context),
             ..Default::default()
         },
         LanguageInfo {
@@ -234,6 +236,7 @@ pub fn init(languages: Arc<LanguageRegistry>, node: NodeRuntime, cx: &mut App) {
             registration.adapters,
             registration.context,
             registration.toolchain,
+            registration.manifest_name,
         );
     }
 
@@ -241,11 +244,8 @@ pub fn init(languages: Arc<LanguageRegistry>, node: NodeRuntime, cx: &mut App) {
     cx.observe_flag::<BasedPyrightFeatureFlag, _>({
         let languages = languages.clone();
         move |enabled, _| {
-            if enabled {
-                if let Some(adapter) = basedpyright_lsp_adapter.take() {
-                    languages
-                        .register_available_lsp_adapter(adapter.name(), move || adapter.clone());
-                }
+            if enabled && let Some(adapter) = basedpyright_lsp_adapter.take() {
+                languages.register_available_lsp_adapter(adapter.name(), move || adapter.clone());
             }
         }
     })
@@ -277,13 +277,13 @@ pub fn init(languages: Arc<LanguageRegistry>, node: NodeRuntime, cx: &mut App) {
         move || adapter.clone()
     });
     languages.register_available_lsp_adapter(LanguageServerName("vtsls".into()), {
-        let adapter = vtsls_adapter.clone();
+        let adapter = vtsls_adapter;
         move || adapter.clone()
     });
     languages.register_available_lsp_adapter(
         LanguageServerName("typescript-language-server".into()),
         {
-            let adapter = typescript_lsp_adapter.clone();
+            let adapter = typescript_lsp_adapter;
             move || adapter.clone()
         },
     );
@@ -340,7 +340,7 @@ pub fn init(languages: Arc<LanguageRegistry>, node: NodeRuntime, cx: &mut App) {
         Arc::from(PyprojectTomlManifestProvider),
     ];
     for provider in manifest_providers {
-        project::ManifestProviders::global(cx).register(provider);
+        project::ManifestProvidersStore::global(cx).register(provider);
     }
 }
 
@@ -350,6 +350,7 @@ struct LanguageInfo {
     adapters: Vec<Arc<dyn LspAdapter>>,
     context: Option<Arc<dyn ContextProvider>>,
     toolchain: Option<Arc<dyn ToolchainLister>>,
+    manifest_name: Option<ManifestName>,
 }
 
 fn register_language(
@@ -358,6 +359,7 @@ fn register_language(
     adapters: Vec<Arc<dyn LspAdapter>>,
     context: Option<Arc<dyn ContextProvider>>,
     toolchain: Option<Arc<dyn ToolchainLister>>,
+    manifest_name: Option<ManifestName>,
 ) {
     let config = load_config(name);
     for adapter in adapters {
@@ -368,12 +370,14 @@ fn register_language(
         config.grammar.clone(),
         config.matcher.clone(),
         config.hidden,
+        manifest_name.clone(),
         Arc::new(move || {
             Ok(LoadedLanguage {
                 config: config.clone(),
                 queries: load_queries(name),
                 context_provider: context.clone(),
                 toolchain_provider: toolchain.clone(),
+                manifest_name: manifest_name.clone(),
             })
         }),
     );

crates/languages/src/python.rs 🔗

@@ -4,16 +4,16 @@ use async_trait::async_trait;
 use collections::HashMap;
 use gpui::{App, Task};
 use gpui::{AsyncApp, SharedString};
+use language::Toolchain;
 use language::ToolchainList;
 use language::ToolchainLister;
 use language::language_settings::language_settings;
 use language::{ContextLocation, LanguageToolchainStore};
 use language::{ContextProvider, LspAdapter, LspAdapterDelegate};
 use language::{LanguageName, ManifestName, ManifestProvider, ManifestQuery};
-use language::{Toolchain, WorkspaceFoldersContent};
 use lsp::LanguageServerBinary;
 use lsp::LanguageServerName;
-use node_runtime::NodeRuntime;
+use node_runtime::{NodeRuntime, VersionStrategy};
 use pet_core::Configuration;
 use pet_core::os_environment::Environment;
 use pet_core::python_environment::PythonEnvironmentKind;
@@ -103,7 +103,7 @@ impl PythonLspAdapter {
 #[async_trait(?Send)]
 impl LspAdapter for PythonLspAdapter {
     fn name(&self) -> LanguageServerName {
-        Self::SERVER_NAME.clone()
+        Self::SERVER_NAME
     }
 
     async fn initialization_options(
@@ -127,7 +127,7 @@ impl LspAdapter for PythonLspAdapter {
     async fn check_if_user_installed(
         &self,
         delegate: &dyn LspAdapterDelegate,
-        _: Arc<dyn LanguageToolchainStore>,
+        _: Option<Toolchain>,
         _: &AsyncApp,
     ) -> Option<LanguageServerBinary> {
         if let Some(pyright_bin) = delegate.which("pyright-langserver".as_ref()).await {
@@ -204,8 +204,8 @@ impl LspAdapter for PythonLspAdapter {
             .should_install_npm_package(
                 Self::SERVER_NAME.as_ref(),
                 &server_path,
-                &container_dir,
-                &version,
+                container_dir,
+                VersionStrategy::Latest(version),
             )
             .await;
 
@@ -319,17 +319,9 @@ impl LspAdapter for PythonLspAdapter {
         self: Arc<Self>,
         _: &dyn Fs,
         adapter: &Arc<dyn LspAdapterDelegate>,
-        toolchains: Arc<dyn LanguageToolchainStore>,
+        toolchain: Option<Toolchain>,
         cx: &mut AsyncApp,
     ) -> Result<Value> {
-        let toolchain = toolchains
-            .active_toolchain(
-                adapter.worktree_id(),
-                Arc::from("".as_ref()),
-                LanguageName::new("Python"),
-                cx,
-            )
-            .await;
         cx.update(move |cx| {
             let mut user_settings =
                 language_server_settings(adapter.as_ref(), &Self::SERVER_NAME, cx)
@@ -346,31 +338,31 @@ impl LspAdapter for PythonLspAdapter {
                 let interpreter_path = toolchain.path.to_string();
 
                 // Detect if this is a virtual environment
-                if let Some(interpreter_dir) = Path::new(&interpreter_path).parent() {
-                    if let Some(venv_dir) = interpreter_dir.parent() {
-                        // Check if this looks like a virtual environment
-                        if venv_dir.join("pyvenv.cfg").exists()
-                            || venv_dir.join("bin/activate").exists()
-                            || venv_dir.join("Scripts/activate.bat").exists()
-                        {
-                            // Set venvPath and venv at the root level
-                            // This matches the format of a pyrightconfig.json file
-                            if let Some(parent) = venv_dir.parent() {
-                                // Use relative path if the venv is inside the workspace
-                                let venv_path = if parent == adapter.worktree_root_path() {
-                                    ".".to_string()
-                                } else {
-                                    parent.to_string_lossy().into_owned()
-                                };
-                                object.insert("venvPath".to_string(), Value::String(venv_path));
-                            }
+                if let Some(interpreter_dir) = Path::new(&interpreter_path).parent()
+                    && let Some(venv_dir) = interpreter_dir.parent()
+                {
+                    // Check if this looks like a virtual environment
+                    if venv_dir.join("pyvenv.cfg").exists()
+                        || venv_dir.join("bin/activate").exists()
+                        || venv_dir.join("Scripts/activate.bat").exists()
+                    {
+                        // Set venvPath and venv at the root level
+                        // This matches the format of a pyrightconfig.json file
+                        if let Some(parent) = venv_dir.parent() {
+                            // Use relative path if the venv is inside the workspace
+                            let venv_path = if parent == adapter.worktree_root_path() {
+                                ".".to_string()
+                            } else {
+                                parent.to_string_lossy().into_owned()
+                            };
+                            object.insert("venvPath".to_string(), Value::String(venv_path));
+                        }
 
-                            if let Some(venv_name) = venv_dir.file_name() {
-                                object.insert(
-                                    "venv".to_owned(),
-                                    Value::String(venv_name.to_string_lossy().into_owned()),
-                                );
-                            }
+                        if let Some(venv_name) = venv_dir.file_name() {
+                            object.insert(
+                                "venv".to_owned(),
+                                Value::String(venv_name.to_string_lossy().into_owned()),
+                            );
                         }
                     }
                 }
@@ -397,12 +389,6 @@ impl LspAdapter for PythonLspAdapter {
             user_settings
         })
     }
-    fn manifest_name(&self) -> Option<ManifestName> {
-        Some(SharedString::new_static("pyproject.toml").into())
-    }
-    fn workspace_folders_content(&self) -> WorkspaceFoldersContent {
-        WorkspaceFoldersContent::WorktreeRoot
-    }
 }
 
 async fn get_cached_server_binary(
@@ -725,7 +711,7 @@ impl Default for PythonToolchainProvider {
     }
 }
 
-static ENV_PRIORITY_LIST: &'static [PythonEnvironmentKind] = &[
+static ENV_PRIORITY_LIST: &[PythonEnvironmentKind] = &[
     // Prioritize non-Conda environments.
     PythonEnvironmentKind::Poetry,
     PythonEnvironmentKind::Pipenv,
@@ -842,7 +828,7 @@ impl ToolchainLister for PythonToolchainProvider {
                         .get_env_var("CONDA_PREFIX".to_string())
                         .map(|conda_prefix| {
                             let is_match = |exe: &Option<PathBuf>| {
-                                exe.as_ref().map_or(false, |e| e.starts_with(&conda_prefix))
+                                exe.as_ref().is_some_and(|e| e.starts_with(&conda_prefix))
                             };
                             match (is_match(&lhs.executable), is_match(&rhs.executable)) {
                                 (true, false) => Ordering::Less,
@@ -1040,14 +1026,14 @@ const BINARY_DIR: &str = if cfg!(target_os = "windows") {
 #[async_trait(?Send)]
 impl LspAdapter for PyLspAdapter {
     fn name(&self) -> LanguageServerName {
-        Self::SERVER_NAME.clone()
+        Self::SERVER_NAME
     }
 
     async fn check_if_user_installed(
         &self,
         delegate: &dyn LspAdapterDelegate,
-        toolchains: Arc<dyn LanguageToolchainStore>,
-        cx: &AsyncApp,
+        toolchain: Option<Toolchain>,
+        _: &AsyncApp,
     ) -> Option<LanguageServerBinary> {
         if let Some(pylsp_bin) = delegate.which(Self::SERVER_NAME.as_ref()).await {
             let env = delegate.shell_env().await;
@@ -1057,14 +1043,7 @@ impl LspAdapter for PyLspAdapter {
                 arguments: vec![],
             })
         } else {
-            let venv = toolchains
-                .active_toolchain(
-                    delegate.worktree_id(),
-                    Arc::from("".as_ref()),
-                    LanguageName::new("Python"),
-                    &mut cx.clone(),
-                )
-                .await?;
+            let venv = toolchain?;
             let pylsp_path = Path::new(venv.path.as_ref()).parent()?.join("pylsp");
             pylsp_path.exists().then(|| LanguageServerBinary {
                 path: venv.path.to_string().into(),
@@ -1211,17 +1190,9 @@ impl LspAdapter for PyLspAdapter {
         self: Arc<Self>,
         _: &dyn Fs,
         adapter: &Arc<dyn LspAdapterDelegate>,
-        toolchains: Arc<dyn LanguageToolchainStore>,
+        toolchain: Option<Toolchain>,
         cx: &mut AsyncApp,
     ) -> Result<Value> {
-        let toolchain = toolchains
-            .active_toolchain(
-                adapter.worktree_id(),
-                Arc::from("".as_ref()),
-                LanguageName::new("Python"),
-                cx,
-            )
-            .await;
         cx.update(move |cx| {
             let mut user_settings =
                 language_server_settings(adapter.as_ref(), &Self::SERVER_NAME, cx)
@@ -1282,12 +1253,6 @@ impl LspAdapter for PyLspAdapter {
             user_settings
         })
     }
-    fn manifest_name(&self) -> Option<ManifestName> {
-        Some(SharedString::new_static("pyproject.toml").into())
-    }
-    fn workspace_folders_content(&self) -> WorkspaceFoldersContent {
-        WorkspaceFoldersContent::WorktreeRoot
-    }
 }
 
 pub(crate) struct BasedPyrightLspAdapter {
@@ -1353,7 +1318,7 @@ impl BasedPyrightLspAdapter {
 #[async_trait(?Send)]
 impl LspAdapter for BasedPyrightLspAdapter {
     fn name(&self) -> LanguageServerName {
-        Self::SERVER_NAME.clone()
+        Self::SERVER_NAME
     }
 
     async fn initialization_options(
@@ -1377,8 +1342,8 @@ impl LspAdapter for BasedPyrightLspAdapter {
     async fn check_if_user_installed(
         &self,
         delegate: &dyn LspAdapterDelegate,
-        toolchains: Arc<dyn LanguageToolchainStore>,
-        cx: &AsyncApp,
+        toolchain: Option<Toolchain>,
+        _: &AsyncApp,
     ) -> Option<LanguageServerBinary> {
         if let Some(bin) = delegate.which(Self::BINARY_NAME.as_ref()).await {
             let env = delegate.shell_env().await;
@@ -1388,15 +1353,7 @@ impl LspAdapter for BasedPyrightLspAdapter {
                 arguments: vec!["--stdio".into()],
             })
         } else {
-            let venv = toolchains
-                .active_toolchain(
-                    delegate.worktree_id(),
-                    Arc::from("".as_ref()),
-                    LanguageName::new("Python"),
-                    &mut cx.clone(),
-                )
-                .await?;
-            let path = Path::new(venv.path.as_ref())
+            let path = Path::new(toolchain?.path.as_ref())
                 .parent()?
                 .join(Self::BINARY_NAME);
             path.exists().then(|| LanguageServerBinary {
@@ -1543,17 +1500,9 @@ impl LspAdapter for BasedPyrightLspAdapter {
         self: Arc<Self>,
         _: &dyn Fs,
         adapter: &Arc<dyn LspAdapterDelegate>,
-        toolchains: Arc<dyn LanguageToolchainStore>,
+        toolchain: Option<Toolchain>,
         cx: &mut AsyncApp,
     ) -> Result<Value> {
-        let toolchain = toolchains
-            .active_toolchain(
-                adapter.worktree_id(),
-                Arc::from("".as_ref()),
-                LanguageName::new("Python"),
-                cx,
-            )
-            .await;
         cx.update(move |cx| {
             let mut user_settings =
                 language_server_settings(adapter.as_ref(), &Self::SERVER_NAME, cx)
@@ -1570,31 +1519,31 @@ impl LspAdapter for BasedPyrightLspAdapter {
                 let interpreter_path = toolchain.path.to_string();
 
                 // Detect if this is a virtual environment
-                if let Some(interpreter_dir) = Path::new(&interpreter_path).parent() {
-                    if let Some(venv_dir) = interpreter_dir.parent() {
-                        // Check if this looks like a virtual environment
-                        if venv_dir.join("pyvenv.cfg").exists()
-                            || venv_dir.join("bin/activate").exists()
-                            || venv_dir.join("Scripts/activate.bat").exists()
-                        {
-                            // Set venvPath and venv at the root level
-                            // This matches the format of a pyrightconfig.json file
-                            if let Some(parent) = venv_dir.parent() {
-                                // Use relative path if the venv is inside the workspace
-                                let venv_path = if parent == adapter.worktree_root_path() {
-                                    ".".to_string()
-                                } else {
-                                    parent.to_string_lossy().into_owned()
-                                };
-                                object.insert("venvPath".to_string(), Value::String(venv_path));
-                            }
+                if let Some(interpreter_dir) = Path::new(&interpreter_path).parent()
+                    && let Some(venv_dir) = interpreter_dir.parent()
+                {
+                    // Check if this looks like a virtual environment
+                    if venv_dir.join("pyvenv.cfg").exists()
+                        || venv_dir.join("bin/activate").exists()
+                        || venv_dir.join("Scripts/activate.bat").exists()
+                    {
+                        // Set venvPath and venv at the root level
+                        // This matches the format of a pyrightconfig.json file
+                        if let Some(parent) = venv_dir.parent() {
+                            // Use relative path if the venv is inside the workspace
+                            let venv_path = if parent == adapter.worktree_root_path() {
+                                ".".to_string()
+                            } else {
+                                parent.to_string_lossy().into_owned()
+                            };
+                            object.insert("venvPath".to_string(), Value::String(venv_path));
+                        }
 
-                            if let Some(venv_name) = venv_dir.file_name() {
-                                object.insert(
-                                    "venv".to_owned(),
-                                    Value::String(venv_name.to_string_lossy().into_owned()),
-                                );
-                            }
+                        if let Some(venv_name) = venv_dir.file_name() {
+                            object.insert(
+                                "venv".to_owned(),
+                                Value::String(venv_name.to_string_lossy().into_owned()),
+                            );
                         }
                     }
                 }
@@ -1621,14 +1570,6 @@ impl LspAdapter for BasedPyrightLspAdapter {
             user_settings
         })
     }
-
-    fn manifest_name(&self) -> Option<ManifestName> {
-        Some(SharedString::new_static("pyproject.toml").into())
-    }
-
-    fn workspace_folders_content(&self) -> WorkspaceFoldersContent {
-        WorkspaceFoldersContent::WorktreeRoot
-    }
 }
 
 #[cfg(test)]

crates/languages/src/rust.rs 🔗

@@ -23,7 +23,7 @@ use std::{
     sync::{Arc, LazyLock},
 };
 use task::{TaskTemplate, TaskTemplates, TaskVariables, VariableName};
-use util::fs::make_file_executable;
+use util::fs::{make_file_executable, remove_matching};
 use util::merge_json_value_into;
 use util::{ResultExt, maybe};
 
@@ -106,17 +106,13 @@ impl ManifestProvider for CargoManifestProvider {
 #[async_trait(?Send)]
 impl LspAdapter for RustLspAdapter {
     fn name(&self) -> LanguageServerName {
-        SERVER_NAME.clone()
-    }
-
-    fn manifest_name(&self) -> Option<ManifestName> {
-        Some(SharedString::new_static("Cargo.toml").into())
+        SERVER_NAME
     }
 
     async fn check_if_user_installed(
         &self,
         delegate: &dyn LspAdapterDelegate,
-        _: Arc<dyn LanguageToolchainStore>,
+        _: Option<Toolchain>,
         _: &AsyncApp,
     ) -> Option<LanguageServerBinary> {
         let path = delegate.which("rust-analyzer".as_ref()).await?;
@@ -162,13 +158,13 @@ impl LspAdapter for RustLspAdapter {
         let asset_name = Self::build_asset_name();
         let asset = release
             .assets
-            .iter()
+            .into_iter()
             .find(|asset| asset.name == asset_name)
             .with_context(|| format!("no asset found matching `{asset_name:?}`"))?;
         Ok(Box::new(GitHubLspBinaryVersion {
             name: release.tag_name,
-            url: asset.browser_download_url.clone(),
-            digest: asset.digest.clone(),
+            url: asset.browser_download_url,
+            digest: asset.digest,
         }))
     }
 
@@ -178,11 +174,11 @@ impl LspAdapter for RustLspAdapter {
         container_dir: PathBuf,
         delegate: &dyn LspAdapterDelegate,
     ) -> Result<LanguageServerBinary> {
-        let GitHubLspBinaryVersion { name, url, digest } =
-            &*version.downcast::<GitHubLspBinaryVersion>().unwrap();
-        let expected_digest = digest
-            .as_ref()
-            .and_then(|digest| digest.strip_prefix("sha256:"));
+        let GitHubLspBinaryVersion {
+            name,
+            url,
+            digest: expected_digest,
+        } = *version.downcast::<GitHubLspBinaryVersion>().unwrap();
         let destination_path = container_dir.join(format!("rust-analyzer-{name}"));
         let server_path = match Self::GITHUB_ASSET_KIND {
             AssetKind::TarGz | AssetKind::Gz => destination_path.clone(), // Tar and gzip extract in place.
@@ -213,7 +209,7 @@ impl LspAdapter for RustLspAdapter {
                     })
             };
             if let (Some(actual_digest), Some(expected_digest)) =
-                (&metadata.digest, expected_digest)
+                (&metadata.digest, &expected_digest)
             {
                 if actual_digest == expected_digest {
                     if validity_check().await.is_ok() {
@@ -229,20 +225,20 @@ impl LspAdapter for RustLspAdapter {
             }
         }
 
-        _ = fs::remove_dir_all(&destination_path).await;
         download_server_binary(
             delegate,
-            url,
-            expected_digest,
+            &url,
+            expected_digest.as_deref(),
             &destination_path,
             Self::GITHUB_ASSET_KIND,
         )
         .await?;
         make_file_executable(&server_path).await?;
+        remove_matching(&container_dir, |path| path != destination_path).await;
         GithubBinaryMetadata::write_to_file(
             &GithubBinaryMetadata {
                 metadata_version: 1,
-                digest: expected_digest.map(ToString::to_string),
+                digest: expected_digest,
             },
             &metadata_path,
         )
@@ -407,7 +403,7 @@ impl LspAdapter for RustLspAdapter {
                 } else if completion
                     .detail
                     .as_ref()
-                    .map_or(false, |detail| detail.starts_with("macro_rules! "))
+                    .is_some_and(|detail| detail.starts_with("macro_rules! "))
                 {
                     let text = completion.label.clone();
                     let len = text.len();
@@ -500,7 +496,7 @@ impl LspAdapter for RustLspAdapter {
         let enable_lsp_tasks = ProjectSettings::get_global(cx)
             .lsp
             .get(&SERVER_NAME)
-            .map_or(false, |s| s.enable_lsp_tasks);
+            .is_some_and(|s| s.enable_lsp_tasks);
         if enable_lsp_tasks {
             let experimental = json!({
                 "runnables": {
@@ -585,7 +581,7 @@ impl ContextProvider for RustContextProvider {
 
         if let (Some(path), Some(stem)) = (&local_abs_path, task_variables.get(&VariableName::Stem))
         {
-            let fragment = test_fragment(&variables, &path, stem);
+            let fragment = test_fragment(&variables, path, stem);
             variables.insert(RUST_TEST_FRAGMENT_TASK_VARIABLE, fragment);
         };
         if let Some(test_name) =
@@ -602,16 +598,14 @@ impl ContextProvider for RustContextProvider {
             if let Some(path) = local_abs_path
                 .as_deref()
                 .and_then(|local_abs_path| local_abs_path.parent())
-            {
-                if let Some(package_name) =
+                && let Some(package_name) =
                     human_readable_package_name(path, project_env.as_ref()).await
-                {
-                    variables.insert(RUST_PACKAGE_TASK_VARIABLE.clone(), package_name);
-                }
+            {
+                variables.insert(RUST_PACKAGE_TASK_VARIABLE.clone(), package_name);
             }
             if let Some(path) = local_abs_path.as_ref()
                 && let Some((target, manifest_path)) =
-                    target_info_from_abs_path(&path, project_env.as_ref()).await
+                    target_info_from_abs_path(path, project_env.as_ref()).await
             {
                 if let Some(target) = target {
                     variables.extend(TaskVariables::from_iter([
@@ -665,7 +659,7 @@ impl ContextProvider for RustContextProvider {
             .variables
             .get(CUSTOM_TARGET_DIR)
             .cloned();
-        let run_task_args = if let Some(package_to_run) = package_to_run.clone() {
+        let run_task_args = if let Some(package_to_run) = package_to_run {
             vec!["run".into(), "-p".into(), package_to_run]
         } else {
             vec!["run".into()]
@@ -1023,8 +1017,14 @@ async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServ
             last = Some(path);
         }
 
+        let path = last.context("no cached binary")?;
+        let path = match RustLspAdapter::GITHUB_ASSET_KIND {
+            AssetKind::TarGz | AssetKind::Gz => path, // Tar and gzip extract in place.
+            AssetKind::Zip => path.join("rust-analyzer.exe"), // zip contains a .exe
+        };
+
         anyhow::Ok(LanguageServerBinary {
-            path: last.context("no cached binary")?,
+            path,
             env: None,
             arguments: Default::default(),
         })
@@ -1568,7 +1568,7 @@ mod tests {
             let found = test_fragment(
                 &TaskVariables::from_iter(variables.into_iter().map(|(k, v)| (k, v.to_owned()))),
                 path,
-                &path.file_stem().unwrap().to_str().unwrap(),
+                path.file_stem().unwrap().to_str().unwrap(),
             );
             assert_eq!(expected, found);
         }

crates/languages/src/tailwind.rs 🔗

@@ -3,9 +3,9 @@ use async_trait::async_trait;
 use collections::HashMap;
 use futures::StreamExt;
 use gpui::AsyncApp;
-use language::{LanguageName, LanguageToolchainStore, LspAdapter, LspAdapterDelegate};
+use language::{LanguageName, LspAdapter, LspAdapterDelegate, Toolchain};
 use lsp::{LanguageServerBinary, LanguageServerName};
-use node_runtime::NodeRuntime;
+use node_runtime::{NodeRuntime, VersionStrategy};
 use project::{Fs, lsp_store::language_server_settings};
 use serde_json::{Value, json};
 use smol::fs;
@@ -44,13 +44,13 @@ impl TailwindLspAdapter {
 #[async_trait(?Send)]
 impl LspAdapter for TailwindLspAdapter {
     fn name(&self) -> LanguageServerName {
-        Self::SERVER_NAME.clone()
+        Self::SERVER_NAME
     }
 
     async fn check_if_user_installed(
         &self,
         delegate: &dyn LspAdapterDelegate,
-        _: Arc<dyn LanguageToolchainStore>,
+        _: Option<Toolchain>,
         _: &AsyncApp,
     ) -> Option<LanguageServerBinary> {
         let path = delegate.which(Self::SERVER_NAME.as_ref()).await?;
@@ -108,7 +108,12 @@ impl LspAdapter for TailwindLspAdapter {
 
         let should_install_language_server = self
             .node
-            .should_install_npm_package(Self::PACKAGE_NAME, &server_path, &container_dir, &version)
+            .should_install_npm_package(
+                Self::PACKAGE_NAME,
+                &server_path,
+                container_dir,
+                VersionStrategy::Latest(version),
+            )
             .await;
 
         if should_install_language_server {
@@ -150,7 +155,7 @@ impl LspAdapter for TailwindLspAdapter {
         self: Arc<Self>,
         _: &dyn Fs,
         delegate: &Arc<dyn LspAdapterDelegate>,
-        _: Arc<dyn LanguageToolchainStore>,
+        _: Option<Toolchain>,
         cx: &mut AsyncApp,
     ) -> Result<Value> {
         let mut tailwind_user_settings = cx.update(|cx| {

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

@@ -34,12 +34,16 @@
 (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))
 
 (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))
 

crates/languages/src/typescript.rs 🔗

@@ -7,10 +7,10 @@ use gpui::{App, AppContext, AsyncApp, Task};
 use http_client::github::{AssetKind, GitHubLspBinaryVersion, build_asset_url};
 use language::{
     ContextLocation, ContextProvider, File, LanguageName, LanguageToolchainStore, LspAdapter,
-    LspAdapterDelegate,
+    LspAdapterDelegate, Toolchain,
 };
 use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerName};
-use node_runtime::NodeRuntime;
+use node_runtime::{NodeRuntime, VersionStrategy};
 use project::{Fs, lsp_store::language_server_settings};
 use serde_json::{Value, json};
 use smol::{fs, lock::RwLock, stream::StreamExt};
@@ -341,10 +341,10 @@ async fn detect_package_manager(
     fs: Arc<dyn Fs>,
     package_json_data: Option<PackageJsonData>,
 ) -> &'static str {
-    if let Some(package_json_data) = package_json_data {
-        if let Some(package_manager) = package_json_data.package_manager {
-            return package_manager;
-        }
+    if let Some(package_json_data) = package_json_data
+        && let Some(package_manager) = package_json_data.package_manager
+    {
+        return package_manager;
     }
     if fs.is_file(&worktree_root.join("pnpm-lock.yaml")).await {
         return "pnpm";
@@ -557,7 +557,7 @@ struct TypeScriptVersions {
 #[async_trait(?Send)]
 impl LspAdapter for TypeScriptLspAdapter {
     fn name(&self) -> LanguageServerName {
-        Self::SERVER_NAME.clone()
+        Self::SERVER_NAME
     }
 
     async fn fetch_latest_server_version(
@@ -587,8 +587,8 @@ impl LspAdapter for TypeScriptLspAdapter {
             .should_install_npm_package(
                 Self::PACKAGE_NAME,
                 &server_path,
-                &container_dir,
-                version.typescript_version.as_str(),
+                container_dir,
+                VersionStrategy::Latest(version.typescript_version.as_str()),
             )
             .await;
 
@@ -722,7 +722,7 @@ impl LspAdapter for TypeScriptLspAdapter {
         self: Arc<Self>,
         _: &dyn Fs,
         delegate: &Arc<dyn LspAdapterDelegate>,
-        _: Arc<dyn LanguageToolchainStore>,
+        _: Option<Toolchain>,
         cx: &mut AsyncApp,
     ) -> Result<Value> {
         let override_options = cx.update(|cx| {
@@ -822,7 +822,7 @@ impl LspAdapter for EsLintLspAdapter {
         self: Arc<Self>,
         _: &dyn Fs,
         delegate: &Arc<dyn LspAdapterDelegate>,
-        _: Arc<dyn LanguageToolchainStore>,
+        _: Option<Toolchain>,
         cx: &mut AsyncApp,
     ) -> Result<Value> {
         let workspace_root = delegate.worktree_root_path();
@@ -879,7 +879,7 @@ impl LspAdapter for EsLintLspAdapter {
     }
 
     fn name(&self) -> LanguageServerName {
-        Self::SERVER_NAME.clone()
+        Self::SERVER_NAME
     }
 
     async fn fetch_latest_server_version(
@@ -910,7 +910,7 @@ impl LspAdapter for EsLintLspAdapter {
         let server_path = destination_path.join(Self::SERVER_PATH);
 
         if fs::metadata(&server_path).await.is_err() {
-            remove_matching(&container_dir, |entry| entry != destination_path).await;
+            remove_matching(&container_dir, |_| true).await;
 
             download_server_binary(
                 delegate,

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

@@ -34,12 +34,16 @@
 (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))
 
 (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))
 

crates/languages/src/vtsls.rs 🔗

@@ -2,9 +2,9 @@ use anyhow::Result;
 use async_trait::async_trait;
 use collections::HashMap;
 use gpui::AsyncApp;
-use language::{LanguageName, LanguageToolchainStore, LspAdapter, LspAdapterDelegate};
+use language::{LanguageName, LspAdapter, LspAdapterDelegate, Toolchain};
 use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerName};
-use node_runtime::NodeRuntime;
+use node_runtime::{NodeRuntime, VersionStrategy};
 use project::{Fs, lsp_store::language_server_settings};
 use serde_json::Value;
 use std::{
@@ -67,7 +67,7 @@ const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("vtsls");
 #[async_trait(?Send)]
 impl LspAdapter for VtslsLspAdapter {
     fn name(&self) -> LanguageServerName {
-        SERVER_NAME.clone()
+        SERVER_NAME
     }
 
     async fn fetch_latest_server_version(
@@ -86,7 +86,7 @@ impl LspAdapter for VtslsLspAdapter {
     async fn check_if_user_installed(
         &self,
         delegate: &dyn LspAdapterDelegate,
-        _: Arc<dyn LanguageToolchainStore>,
+        _: Option<Toolchain>,
         _: &AsyncApp,
     ) -> Option<LanguageServerBinary> {
         let env = delegate.shell_env().await;
@@ -115,7 +115,7 @@ impl LspAdapter for VtslsLspAdapter {
                 Self::PACKAGE_NAME,
                 &server_path,
                 &container_dir,
-                &latest_version.server_version,
+                VersionStrategy::Latest(&latest_version.server_version),
             )
             .await
         {
@@ -128,7 +128,7 @@ impl LspAdapter for VtslsLspAdapter {
                 Self::TYPESCRIPT_PACKAGE_NAME,
                 &container_dir.join(Self::TYPESCRIPT_TSDK_PATH),
                 &container_dir,
-                &latest_version.typescript_version,
+                VersionStrategy::Latest(&latest_version.typescript_version),
             )
             .await
         {
@@ -211,7 +211,7 @@ impl LspAdapter for VtslsLspAdapter {
         self: Arc<Self>,
         fs: &dyn Fs,
         delegate: &Arc<dyn LspAdapterDelegate>,
-        _: Arc<dyn LanguageToolchainStore>,
+        _: Option<Toolchain>,
         cx: &mut AsyncApp,
     ) -> Result<Value> {
         let tsdk_path = Self::tsdk_path(fs, delegate).await;

crates/languages/src/yaml.rs 🔗

@@ -2,11 +2,9 @@ use anyhow::{Context as _, Result};
 use async_trait::async_trait;
 use futures::StreamExt;
 use gpui::AsyncApp;
-use language::{
-    LanguageToolchainStore, LspAdapter, LspAdapterDelegate, language_settings::AllLanguageSettings,
-};
+use language::{LspAdapter, LspAdapterDelegate, Toolchain, language_settings::AllLanguageSettings};
 use lsp::{LanguageServerBinary, LanguageServerName};
-use node_runtime::NodeRuntime;
+use node_runtime::{NodeRuntime, VersionStrategy};
 use project::{Fs, lsp_store::language_server_settings};
 use serde_json::Value;
 use settings::{Settings, SettingsLocation};
@@ -40,7 +38,7 @@ impl YamlLspAdapter {
 #[async_trait(?Send)]
 impl LspAdapter for YamlLspAdapter {
     fn name(&self) -> LanguageServerName {
-        Self::SERVER_NAME.clone()
+        Self::SERVER_NAME
     }
 
     async fn fetch_latest_server_version(
@@ -57,7 +55,7 @@ impl LspAdapter for YamlLspAdapter {
     async fn check_if_user_installed(
         &self,
         delegate: &dyn LspAdapterDelegate,
-        _: Arc<dyn LanguageToolchainStore>,
+        _: Option<Toolchain>,
         _: &AsyncApp,
     ) -> Option<LanguageServerBinary> {
         let path = delegate.which(Self::SERVER_NAME.as_ref()).await?;
@@ -104,7 +102,12 @@ impl LspAdapter for YamlLspAdapter {
 
         let should_install_language_server = self
             .node
-            .should_install_npm_package(Self::PACKAGE_NAME, &server_path, &container_dir, &version)
+            .should_install_npm_package(
+                Self::PACKAGE_NAME,
+                &server_path,
+                container_dir,
+                VersionStrategy::Latest(version),
+            )
             .await;
 
         if should_install_language_server {
@@ -130,7 +133,7 @@ impl LspAdapter for YamlLspAdapter {
         self: Arc<Self>,
         _: &dyn Fs,
         delegate: &Arc<dyn LspAdapterDelegate>,
-        _: Arc<dyn LanguageToolchainStore>,
+        _: Option<Toolchain>,
         cx: &mut AsyncApp,
     ) -> Result<Value> {
         let location = SettingsLocation {

crates/livekit_client/Cargo.toml 🔗

@@ -25,6 +25,7 @@ async-trait.workspace = true
 collections.workspace = true
 cpal.workspace = true
 futures.workspace = true
+audio.workspace = true
 gpui = { workspace = true, features = ["screen-capture", "x11", "wayland", "windows-manifest"] }
 gpui_tokio.workspace = true
 http_client_tls.workspace = true
@@ -35,10 +36,13 @@ nanoid.workspace = true
 parking_lot.workspace = true
 postage.workspace = true
 smallvec.workspace = true
+settings.workspace = true
 tokio-tungstenite.workspace = true
 util.workspace = true
 workspace-hack.workspace = true
 
+rodio = { workspace = true, features = ["wav_output"] }
+
 [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" }
 livekit = { rev = "5f04705ac3f356350ae31534ffbc476abc9ea83d", git = "https://github.com/zed-industries/livekit-rust-sdks", features = [

crates/livekit_client/examples/test_app.rs 🔗

@@ -159,14 +159,14 @@ impl LivekitWindow {
                 if output
                     .audio_output_stream
                     .as_ref()
-                    .map_or(false, |(track, _)| track.sid() == unpublish_sid)
+                    .is_some_and(|(track, _)| track.sid() == unpublish_sid)
                 {
                     output.audio_output_stream.take();
                 }
                 if output
                     .screen_share_output_view
                     .as_ref()
-                    .map_or(false, |(track, _)| track.sid() == unpublish_sid)
+                    .is_some_and(|(track, _)| track.sid() == unpublish_sid)
                 {
                     output.screen_share_output_view.take();
                 }
@@ -183,7 +183,7 @@ impl LivekitWindow {
                 match track {
                     livekit_client::RemoteTrack::Audio(track) => {
                         output.audio_output_stream = Some((
-                            publication.clone(),
+                            publication,
                             room.play_remote_audio_track(&track, cx).unwrap(),
                         ));
                     }

crates/livekit_client/src/lib.rs 🔗

@@ -1,7 +1,13 @@
+use anyhow::Context as _;
 use collections::HashMap;
 
 mod remote_video_track_view;
+use cpal::traits::HostTrait as _;
 pub use remote_video_track_view::{RemoteVideoTrackView, RemoteVideoTrackViewEvent};
+use rodio::DeviceTrait as _;
+
+mod record;
+pub use record::CaptureInput;
 
 #[cfg(not(any(
     test,
@@ -18,6 +24,11 @@ mod livekit_client;
 )))]
 pub use livekit_client::*;
 
+// If you need proper LSP in livekit_client you've got to comment
+// - the cfg blocks above
+// - the mods: mock_client & test and their conditional blocks
+// - the pub use mock_client::* and their conditional blocks
+
 #[cfg(any(
     test,
     feature = "test-support",
@@ -168,3 +179,59 @@ pub enum RoomEvent {
     Reconnecting,
     Reconnected,
 }
+
+pub(crate) fn default_device(
+    input: bool,
+) -> anyhow::Result<(cpal::Device, cpal::SupportedStreamConfig)> {
+    let device;
+    let config;
+    if input {
+        device = cpal::default_host()
+            .default_input_device()
+            .context("no audio input device available")?;
+        config = device
+            .default_input_config()
+            .context("failed to get default input config")?;
+    } else {
+        device = cpal::default_host()
+            .default_output_device()
+            .context("no audio output device available")?;
+        config = device
+            .default_output_config()
+            .context("failed to get default output config")?;
+    }
+    Ok((device, config))
+}
+
+pub(crate) fn get_sample_data(
+    sample_format: cpal::SampleFormat,
+    data: &cpal::Data,
+) -> anyhow::Result<Vec<i16>> {
+    match sample_format {
+        cpal::SampleFormat::I8 => Ok(convert_sample_data::<i8, i16>(data)),
+        cpal::SampleFormat::I16 => Ok(data.as_slice::<i16>().unwrap().to_vec()),
+        cpal::SampleFormat::I24 => Ok(convert_sample_data::<cpal::I24, i16>(data)),
+        cpal::SampleFormat::I32 => Ok(convert_sample_data::<i32, i16>(data)),
+        cpal::SampleFormat::I64 => Ok(convert_sample_data::<i64, i16>(data)),
+        cpal::SampleFormat::U8 => Ok(convert_sample_data::<u8, i16>(data)),
+        cpal::SampleFormat::U16 => Ok(convert_sample_data::<u16, i16>(data)),
+        cpal::SampleFormat::U32 => Ok(convert_sample_data::<u32, i16>(data)),
+        cpal::SampleFormat::U64 => Ok(convert_sample_data::<u64, i16>(data)),
+        cpal::SampleFormat::F32 => Ok(convert_sample_data::<f32, i16>(data)),
+        cpal::SampleFormat::F64 => Ok(convert_sample_data::<f64, i16>(data)),
+        _ => anyhow::bail!("Unsupported sample format"),
+    }
+}
+
+pub(crate) fn convert_sample_data<
+    TSource: cpal::SizedSample,
+    TDest: cpal::SizedSample + cpal::FromSample<TSource>,
+>(
+    data: &cpal::Data,
+) -> Vec<TDest> {
+    data.as_slice::<TSource>()
+        .unwrap()
+        .iter()
+        .map(|e| e.to_sample::<TDest>())
+        .collect()
+}

crates/livekit_client/src/livekit_client.rs 🔗

@@ -1,11 +1,14 @@
 use std::sync::Arc;
 
 use anyhow::{Context as _, Result};
+use audio::AudioSettings;
 use collections::HashMap;
 use futures::{SinkExt, channel::mpsc};
 use gpui::{App, AsyncApp, ScreenCaptureSource, ScreenCaptureStream, Task};
 use gpui_tokio::Tokio;
+use log::info;
 use playback::capture_local_video_track;
+use settings::Settings;
 
 mod playback;
 
@@ -123,9 +126,14 @@ impl Room {
     pub fn play_remote_audio_track(
         &self,
         track: &RemoteAudioTrack,
-        _cx: &App,
+        cx: &mut App,
     ) -> Result<playback::AudioStream> {
-        Ok(self.playback.play_remote_audio_track(&track.0))
+        if AudioSettings::get_global(cx).rodio_audio {
+            info!("Using experimental.rodio_audio audio pipeline");
+            playback::play_remote_audio_track(&track.0, cx)
+        } else {
+            Ok(self.playback.play_remote_audio_track(&track.0))
+        }
     }
 }
 

crates/livekit_client/src/livekit_client/playback.rs 🔗

@@ -1,7 +1,6 @@
 use anyhow::{Context as _, Result};
 
-use cpal::traits::{DeviceTrait, HostTrait, StreamTrait as _};
-use cpal::{Data, FromSample, I24, SampleFormat, SizedSample};
+use cpal::traits::{DeviceTrait, StreamTrait as _};
 use futures::channel::mpsc::UnboundedSender;
 use futures::{Stream, StreamExt as _};
 use gpui::{
@@ -19,13 +18,16 @@ use livekit::webrtc::{
     video_stream::native::NativeVideoStream,
 };
 use parking_lot::Mutex;
+use rodio::Source;
 use std::cell::RefCell;
 use std::sync::Weak;
-use std::sync::atomic::{self, AtomicI32};
+use std::sync::atomic::{AtomicBool, AtomicI32, Ordering};
 use std::time::Duration;
 use std::{borrow::Cow, collections::VecDeque, sync::Arc, thread};
 use util::{ResultExt as _, maybe};
 
+mod source;
+
 pub(crate) struct AudioStack {
     executor: BackgroundExecutor,
     apm: Arc<Mutex<apm::AudioProcessingModule>>,
@@ -41,6 +43,29 @@ pub(crate) struct AudioStack {
 const SAMPLE_RATE: u32 = 48000;
 const NUM_CHANNELS: u32 = 2;
 
+pub(crate) fn play_remote_audio_track(
+    track: &livekit::track::RemoteAudioTrack,
+    cx: &mut gpui::App,
+) -> Result<AudioStream> {
+    let stop_handle = Arc::new(AtomicBool::new(false));
+    let stop_handle_clone = stop_handle.clone();
+    let stream = source::LiveKitStream::new(cx.background_executor(), track)
+        .stoppable()
+        .periodic_access(Duration::from_millis(50), move |s| {
+            if stop_handle.load(Ordering::Relaxed) {
+                s.stop();
+            }
+        });
+    audio::Audio::play_source(stream, cx).context("Could not play audio")?;
+
+    let on_drop = util::defer(move || {
+        stop_handle_clone.store(true, Ordering::Relaxed);
+    });
+    Ok(AudioStream::Output {
+        _drop: Box::new(on_drop),
+    })
+}
+
 impl AudioStack {
     pub(crate) fn new(executor: BackgroundExecutor) -> Self {
         let apm = Arc::new(Mutex::new(apm::AudioProcessingModule::new(
@@ -62,7 +87,7 @@ impl AudioStack {
     ) -> AudioStream {
         let output_task = self.start_output();
 
-        let next_ssrc = self.next_ssrc.fetch_add(1, atomic::Ordering::Relaxed);
+        let next_ssrc = self.next_ssrc.fetch_add(1, Ordering::Relaxed);
         let source = AudioMixerSource {
             ssrc: next_ssrc,
             sample_rate: SAMPLE_RATE,
@@ -98,6 +123,23 @@ impl AudioStack {
         }
     }
 
+    fn start_output(&self) -> Arc<Task<()>> {
+        if let Some(task) = self._output_task.borrow().upgrade() {
+            return task;
+        }
+        let task = Arc::new(self.executor.spawn({
+            let apm = self.apm.clone();
+            let mixer = self.mixer.clone();
+            async move {
+                Self::play_output(apm, mixer, SAMPLE_RATE, NUM_CHANNELS)
+                    .await
+                    .log_err();
+            }
+        }));
+        *self._output_task.borrow_mut() = Arc::downgrade(&task);
+        task
+    }
+
     pub(crate) fn capture_local_microphone_track(
         &self,
     ) -> Result<(crate::LocalAudioTrack, AudioStream)> {
@@ -118,7 +160,6 @@ impl AudioStack {
 
         let (frame_tx, mut frame_rx) = futures::channel::mpsc::unbounded();
         let transmit_task = self.executor.spawn({
-            let source = source.clone();
             async move {
                 while let Some(frame) = frame_rx.next().await {
                     source.capture_frame(&frame).await.log_err();
@@ -133,29 +174,12 @@ impl AudioStack {
             drop(transmit_task);
             drop(capture_task);
         });
-        return Ok((
+        Ok((
             super::LocalAudioTrack(track),
             AudioStream::Output {
                 _drop: Box::new(on_drop),
             },
-        ));
-    }
-
-    fn start_output(&self) -> Arc<Task<()>> {
-        if let Some(task) = self._output_task.borrow().upgrade() {
-            return task;
-        }
-        let task = Arc::new(self.executor.spawn({
-            let apm = self.apm.clone();
-            let mixer = self.mixer.clone();
-            async move {
-                Self::play_output(apm, mixer, SAMPLE_RATE, NUM_CHANNELS)
-                    .await
-                    .log_err();
-            }
-        }));
-        *self._output_task.borrow_mut() = Arc::downgrade(&task);
-        task
+        ))
     }
 
     async fn play_output(
@@ -166,7 +190,7 @@ impl AudioStack {
     ) -> Result<()> {
         loop {
             let mut device_change_listener = DeviceChangeListener::new(false)?;
-            let (output_device, output_config) = default_device(false)?;
+            let (output_device, output_config) = crate::default_device(false)?;
             let (end_on_drop_tx, end_on_drop_rx) = std::sync::mpsc::channel::<()>();
             let mixer = mixer.clone();
             let apm = apm.clone();
@@ -238,7 +262,7 @@ impl AudioStack {
     ) -> Result<()> {
         loop {
             let mut device_change_listener = DeviceChangeListener::new(true)?;
-            let (device, config) = default_device(true)?;
+            let (device, config) = crate::default_device(true)?;
             let (end_on_drop_tx, end_on_drop_rx) = std::sync::mpsc::channel::<()>();
             let apm = apm.clone();
             let frame_tx = frame_tx.clone();
@@ -262,7 +286,7 @@ impl AudioStack {
                             config.sample_format(),
                             move |data, _: &_| {
                                 let data =
-                                    Self::get_sample_data(config.sample_format(), data).log_err();
+                                    crate::get_sample_data(config.sample_format(), data).log_err();
                                 let Some(data) = data else {
                                     return;
                                 };
@@ -320,33 +344,6 @@ impl AudioStack {
             drop(end_on_drop_tx)
         }
     }
-
-    fn get_sample_data(sample_format: SampleFormat, data: &Data) -> Result<Vec<i16>> {
-        match sample_format {
-            SampleFormat::I8 => Ok(Self::convert_sample_data::<i8, i16>(data)),
-            SampleFormat::I16 => Ok(data.as_slice::<i16>().unwrap().to_vec()),
-            SampleFormat::I24 => Ok(Self::convert_sample_data::<I24, i16>(data)),
-            SampleFormat::I32 => Ok(Self::convert_sample_data::<i32, i16>(data)),
-            SampleFormat::I64 => Ok(Self::convert_sample_data::<i64, i16>(data)),
-            SampleFormat::U8 => Ok(Self::convert_sample_data::<u8, i16>(data)),
-            SampleFormat::U16 => Ok(Self::convert_sample_data::<u16, i16>(data)),
-            SampleFormat::U32 => Ok(Self::convert_sample_data::<u32, i16>(data)),
-            SampleFormat::U64 => Ok(Self::convert_sample_data::<u64, i16>(data)),
-            SampleFormat::F32 => Ok(Self::convert_sample_data::<f32, i16>(data)),
-            SampleFormat::F64 => Ok(Self::convert_sample_data::<f64, i16>(data)),
-            _ => anyhow::bail!("Unsupported sample format"),
-        }
-    }
-
-    fn convert_sample_data<TSource: SizedSample, TDest: SizedSample + FromSample<TSource>>(
-        data: &Data,
-    ) -> Vec<TDest> {
-        data.as_slice::<TSource>()
-            .unwrap()
-            .iter()
-            .map(|e| e.to_sample::<TDest>())
-            .collect()
-    }
 }
 
 use super::LocalVideoTrack;
@@ -393,27 +390,6 @@ pub(crate) async fn capture_local_video_track(
     ))
 }
 
-fn default_device(input: bool) -> Result<(cpal::Device, cpal::SupportedStreamConfig)> {
-    let device;
-    let config;
-    if input {
-        device = cpal::default_host()
-            .default_input_device()
-            .context("no audio input device available")?;
-        config = device
-            .default_input_config()
-            .context("failed to get default input config")?;
-    } else {
-        device = cpal::default_host()
-            .default_output_device()
-            .context("no audio output device available")?;
-        config = device
-            .default_output_config()
-            .context("failed to get default output config")?;
-    }
-    Ok((device, config))
-}
-
 #[derive(Clone)]
 struct AudioMixerSource {
     ssrc: i32,

crates/livekit_client/src/livekit_client/playback/source.rs 🔗

@@ -0,0 +1,67 @@
+use futures::StreamExt;
+use libwebrtc::{audio_stream::native::NativeAudioStream, prelude::AudioFrame};
+use livekit::track::RemoteAudioTrack;
+use rodio::{Source, buffer::SamplesBuffer, conversions::SampleTypeConverter};
+
+use crate::livekit_client::playback::{NUM_CHANNELS, SAMPLE_RATE};
+
+fn frame_to_samplesbuffer(frame: AudioFrame) -> SamplesBuffer {
+    let samples = frame.data.iter().copied();
+    let samples = SampleTypeConverter::<_, _>::new(samples);
+    let samples: Vec<f32> = samples.collect();
+    SamplesBuffer::new(frame.num_channels as u16, frame.sample_rate, samples)
+}
+
+pub struct LiveKitStream {
+    // shared_buffer: SharedBuffer,
+    inner: rodio::queue::SourcesQueueOutput,
+    _receiver_task: gpui::Task<()>,
+}
+
+impl LiveKitStream {
+    pub fn new(executor: &gpui::BackgroundExecutor, track: &RemoteAudioTrack) -> Self {
+        let mut stream =
+            NativeAudioStream::new(track.rtc_track(), SAMPLE_RATE as i32, NUM_CHANNELS as i32);
+        let (queue_input, queue_output) = rodio::queue::queue(true);
+        // spawn rtc stream
+        let receiver_task = executor.spawn({
+            async move {
+                while let Some(frame) = stream.next().await {
+                    let samples = frame_to_samplesbuffer(frame);
+                    queue_input.append(samples);
+                }
+            }
+        });
+
+        LiveKitStream {
+            _receiver_task: receiver_task,
+            inner: queue_output,
+        }
+    }
+}
+
+impl Iterator for LiveKitStream {
+    type Item = rodio::Sample;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        self.inner.next()
+    }
+}
+
+impl Source for LiveKitStream {
+    fn current_span_len(&self) -> Option<usize> {
+        self.inner.current_span_len()
+    }
+
+    fn channels(&self) -> rodio::ChannelCount {
+        self.inner.channels()
+    }
+
+    fn sample_rate(&self) -> rodio::SampleRate {
+        self.inner.sample_rate()
+    }
+
+    fn total_duration(&self) -> Option<std::time::Duration> {
+        self.inner.total_duration()
+    }
+}

crates/livekit_client/src/record.rs 🔗

@@ -0,0 +1,91 @@
+use std::{
+    env,
+    path::{Path, PathBuf},
+    sync::{Arc, Mutex},
+    time::Duration,
+};
+
+use anyhow::{Context, Result};
+use cpal::traits::{DeviceTrait, StreamTrait};
+use rodio::{buffer::SamplesBuffer, conversions::SampleTypeConverter};
+use util::ResultExt;
+
+pub struct CaptureInput {
+    pub name: String,
+    config: cpal::SupportedStreamConfig,
+    samples: Arc<Mutex<Vec<i16>>>,
+    _stream: cpal::Stream,
+}
+
+impl CaptureInput {
+    pub fn start() -> anyhow::Result<Self> {
+        let (device, config) = crate::default_device(true)?;
+        let name = device.name().unwrap_or("<unknown>".to_string());
+        log::info!("Using microphone: {}", name);
+
+        let samples = Arc::new(Mutex::new(Vec::new()));
+        let stream = start_capture(device, config.clone(), samples.clone())?;
+
+        Ok(Self {
+            name,
+            _stream: stream,
+            config,
+            samples,
+        })
+    }
+
+    pub fn finish(self) -> Result<PathBuf> {
+        let name = self.name;
+        let mut path = env::current_dir().context("Could not get current dir")?;
+        path.push(&format!("test_recording_{name}.wav"));
+        log::info!("Test recording written to: {}", path.display());
+        write_out(self.samples, self.config, &path)?;
+        Ok(path)
+    }
+}
+
+fn start_capture(
+    device: cpal::Device,
+    config: cpal::SupportedStreamConfig,
+    samples: Arc<Mutex<Vec<i16>>>,
+) -> Result<cpal::Stream> {
+    let stream = device
+        .build_input_stream_raw(
+            &config.config(),
+            config.sample_format(),
+            move |data, _: &_| {
+                let data = crate::get_sample_data(config.sample_format(), data).log_err();
+                let Some(data) = data else {
+                    return;
+                };
+                samples
+                    .try_lock()
+                    .expect("Only locked after stream ends")
+                    .extend_from_slice(&data);
+            },
+            |err| log::error!("error capturing audio track: {:?}", err),
+            Some(Duration::from_millis(100)),
+        )
+        .context("failed to build input stream")?;
+
+    stream.play()?;
+    Ok(stream)
+}
+
+fn write_out(
+    samples: Arc<Mutex<Vec<i16>>>,
+    config: cpal::SupportedStreamConfig,
+    path: &Path,
+) -> Result<()> {
+    let samples = std::mem::take(
+        &mut *samples
+            .try_lock()
+            .expect("Stream has ended, callback cant hold the lock"),
+    );
+    let samples: Vec<f32> = SampleTypeConverter::<_, f32>::new(samples.into_iter()).collect();
+    let mut samples = SamplesBuffer::new(config.channels(), config.sample_rate().0, samples);
+    match rodio::output_to_wav(&mut samples, path) {
+        Ok(_) => Ok(()),
+        Err(e) => Err(anyhow::anyhow!("Failed to write wav file: {}", e)),
+    }
+}

crates/livekit_client/src/test.rs 🔗

@@ -421,7 +421,7 @@ impl TestServer {
         track_sid: &TrackSid,
         muted: bool,
     ) -> Result<()> {
-        let claims = livekit_api::token::validate(&token, &self.secret_key)?;
+        let claims = livekit_api::token::validate(token, &self.secret_key)?;
         let room_name = claims.video.room.unwrap();
         let identity = ParticipantIdentity(claims.sub.unwrap().to_string());
         let mut server_rooms = self.rooms.lock();
@@ -475,7 +475,7 @@ impl TestServer {
     }
 
     pub(crate) fn is_track_muted(&self, token: &str, track_sid: &TrackSid) -> Option<bool> {
-        let claims = livekit_api::token::validate(&token, &self.secret_key).ok()?;
+        let claims = livekit_api::token::validate(token, &self.secret_key).ok()?;
         let room_name = claims.video.room.unwrap();
 
         let mut server_rooms = self.rooms.lock();
@@ -736,14 +736,14 @@ impl Room {
 
 impl Drop for RoomState {
     fn drop(&mut self) {
-        if self.connection_state == ConnectionState::Connected {
-            if let Ok(server) = TestServer::get(&self.url) {
-                let executor = server.executor.clone();
-                let token = self.token.clone();
-                executor
-                    .spawn(async move { server.leave_room(token).await.ok() })
-                    .detach();
-            }
+        if self.connection_state == ConnectionState::Connected
+            && let Ok(server) = TestServer::get(&self.url)
+        {
+            let executor = server.executor.clone();
+            let token = self.token.clone();
+            executor
+                .spawn(async move { server.leave_room(token).await.ok() })
+                .detach();
         }
     }
 }

crates/lsp/src/lsp.rs 🔗

@@ -45,7 +45,7 @@ use util::{ConnectionResult, ResultExt, TryFutureExt, redact};
 const JSON_RPC_VERSION: &str = "2.0";
 const CONTENT_LEN_HEADER: &str = "Content-Length: ";
 
-const LSP_REQUEST_TIMEOUT: Duration = Duration::from_secs(60 * 2);
+pub const LSP_REQUEST_TIMEOUT: Duration = Duration::from_secs(60 * 2);
 const SERVER_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5);
 
 type NotificationHandler = Box<dyn Send + FnMut(Option<RequestId>, Value, &mut AsyncApp)>;
@@ -318,6 +318,8 @@ impl LanguageServer {
         } else {
             root_path.parent().unwrap_or_else(|| Path::new("/"))
         };
+        let root_uri = Url::from_file_path(&working_dir)
+            .map_err(|()| anyhow!("{working_dir:?} is not a valid URI"))?;
 
         log::info!(
             "starting language server process. binary path: {:?}, working directory: {:?}, args: {:?}",
@@ -345,8 +347,6 @@ impl LanguageServer {
         let stdin = server.stdin.take().unwrap();
         let stdout = server.stdout.take().unwrap();
         let stderr = server.stderr.take().unwrap();
-        let root_uri = Url::from_file_path(&working_dir)
-            .map_err(|()| anyhow!("{working_dir:?} is not a valid URI"))?;
         let server = Self::new_internal(
             server_id,
             server_name,
@@ -651,7 +651,7 @@ impl LanguageServer {
             capabilities: ClientCapabilities {
                 general: Some(GeneralClientCapabilities {
                     position_encodings: Some(vec![PositionEncodingKind::UTF16]),
-                    ..Default::default()
+                    ..GeneralClientCapabilities::default()
                 }),
                 workspace: Some(WorkspaceClientCapabilities {
                     configuration: Some(true),
@@ -665,6 +665,7 @@ impl LanguageServer {
                     workspace_folders: Some(true),
                     symbol: Some(WorkspaceSymbolClientCapabilities {
                         resolve_support: None,
+                        dynamic_registration: Some(true),
                         ..WorkspaceSymbolClientCapabilities::default()
                     }),
                     inlay_hint: Some(InlayHintWorkspaceClientCapabilities {
@@ -688,21 +689,21 @@ impl LanguageServer {
                         ..WorkspaceEditClientCapabilities::default()
                     }),
                     file_operations: Some(WorkspaceFileOperationsClientCapabilities {
-                        dynamic_registration: Some(false),
+                        dynamic_registration: Some(true),
                         did_rename: Some(true),
                         will_rename: Some(true),
-                        ..Default::default()
+                        ..WorkspaceFileOperationsClientCapabilities::default()
                     }),
                     apply_edit: Some(true),
                     execute_command: Some(ExecuteCommandClientCapabilities {
-                        dynamic_registration: Some(false),
+                        dynamic_registration: Some(true),
                     }),
-                    ..Default::default()
+                    ..WorkspaceClientCapabilities::default()
                 }),
                 text_document: Some(TextDocumentClientCapabilities {
                     definition: Some(GotoCapability {
                         link_support: Some(true),
-                        dynamic_registration: None,
+                        dynamic_registration: Some(true),
                     }),
                     code_action: Some(CodeActionClientCapabilities {
                         code_action_literal_support: Some(CodeActionLiteralSupport {
@@ -725,7 +726,8 @@ impl LanguageServer {
                                 "command".to_string(),
                             ],
                         }),
-                        ..Default::default()
+                        dynamic_registration: Some(true),
+                        ..CodeActionClientCapabilities::default()
                     }),
                     completion: Some(CompletionClientCapabilities {
                         completion_item: Some(CompletionItemCapability {
@@ -751,7 +753,7 @@ impl LanguageServer {
                                 MarkupKind::Markdown,
                                 MarkupKind::PlainText,
                             ]),
-                            ..Default::default()
+                            ..CompletionItemCapability::default()
                         }),
                         insert_text_mode: Some(InsertTextMode::ADJUST_INDENTATION),
                         completion_list: Some(CompletionListCapability {
@@ -764,18 +766,20 @@ impl LanguageServer {
                             ]),
                         }),
                         context_support: Some(true),
-                        ..Default::default()
+                        dynamic_registration: Some(true),
+                        ..CompletionClientCapabilities::default()
                     }),
                     rename: Some(RenameClientCapabilities {
                         prepare_support: Some(true),
                         prepare_support_default_behavior: Some(
                             PrepareSupportDefaultBehavior::IDENTIFIER,
                         ),
-                        ..Default::default()
+                        dynamic_registration: Some(true),
+                        ..RenameClientCapabilities::default()
                     }),
                     hover: Some(HoverClientCapabilities {
                         content_format: Some(vec![MarkupKind::Markdown]),
-                        dynamic_registration: None,
+                        dynamic_registration: Some(true),
                     }),
                     inlay_hint: Some(InlayHintClientCapabilities {
                         resolve_support: Some(InlayHintResolveClientCapabilities {
@@ -787,7 +791,7 @@ impl LanguageServer {
                                 "label.command".to_string(),
                             ],
                         }),
-                        dynamic_registration: Some(false),
+                        dynamic_registration: Some(true),
                     }),
                     publish_diagnostics: Some(PublishDiagnosticsClientCapabilities {
                         related_information: Some(true),
@@ -818,26 +822,29 @@ impl LanguageServer {
                             }),
                             active_parameter_support: Some(true),
                         }),
+                        dynamic_registration: Some(true),
                         ..SignatureHelpClientCapabilities::default()
                     }),
                     synchronization: Some(TextDocumentSyncClientCapabilities {
                         did_save: Some(true),
+                        dynamic_registration: Some(true),
                         ..TextDocumentSyncClientCapabilities::default()
                     }),
                     code_lens: Some(CodeLensClientCapabilities {
-                        dynamic_registration: Some(false),
+                        dynamic_registration: Some(true),
                     }),
                     document_symbol: Some(DocumentSymbolClientCapabilities {
                         hierarchical_document_symbol_support: Some(true),
+                        dynamic_registration: Some(true),
                         ..DocumentSymbolClientCapabilities::default()
                     }),
                     diagnostic: Some(DiagnosticClientCapabilities {
-                        dynamic_registration: Some(false),
+                        dynamic_registration: Some(true),
                         related_document_support: Some(true),
                     })
                     .filter(|_| pull_diagnostics),
                     color_provider: Some(DocumentColorClientCapabilities {
-                        dynamic_registration: Some(false),
+                        dynamic_registration: Some(true),
                     }),
                     ..TextDocumentClientCapabilities::default()
                 }),
@@ -850,7 +857,7 @@ impl LanguageServer {
                     show_message: Some(ShowMessageRequestClientCapabilities {
                         message_action_item: None,
                     }),
-                    ..Default::default()
+                    ..WindowClientCapabilities::default()
                 }),
             },
             trace: None,
@@ -862,8 +869,7 @@ impl LanguageServer {
                 }
             }),
             locale: None,
-
-            ..Default::default()
+            ..InitializeParams::default()
         }
     }
 
@@ -1672,7 +1678,7 @@ impl LanguageServer {
             workspace_symbol_provider: Some(OneOf::Left(true)),
             implementation_provider: Some(ImplementationProviderCapability::Simple(true)),
             type_definition_provider: Some(TypeDefinitionProviderCapability::Simple(true)),
-            ..Default::default()
+            ..ServerCapabilities::default()
         }
     }
 }

crates/markdown/examples/markdown.rs 🔗

@@ -77,16 +77,16 @@ impl Render for MarkdownExample {
     fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
         let markdown_style = MarkdownStyle {
             base_text_style: gpui::TextStyle {
-                font_family: "Zed Plex Sans".into(),
+                font_family: ".ZedSans".into(),
                 color: cx.theme().colors().terminal_ansi_black,
                 ..Default::default()
             },
             code_block: StyleRefinement::default()
-                .font_family("Zed Plex Mono")
+                .font_family(".ZedMono")
                 .m(rems(1.))
                 .bg(rgb(0xAAAAAAA)),
             inline_code: gpui::TextStyleRefinement {
-                font_family: Some("Zed Mono".into()),
+                font_family: Some(".ZedMono".into()),
                 color: Some(cx.theme().colors().editor_foreground),
                 background_color: Some(cx.theme().colors().editor_background),
                 ..Default::default()

crates/markdown/examples/markdown_as_child.rs 🔗

@@ -30,7 +30,7 @@ pub fn main() {
 
         let node_runtime = NodeRuntime::unavailable();
         let language_registry = Arc::new(LanguageRegistry::new(cx.background_executor().clone()));
-        languages::init(language_registry.clone(), node_runtime, cx);
+        languages::init(language_registry, node_runtime, cx);
         theme::init(LoadThemes::JustBase, cx);
         Assets.load_fonts(cx).unwrap();
 

crates/markdown/src/markdown.rs 🔗

@@ -340,27 +340,26 @@ impl Markdown {
             }
 
             for (range, event) in &events {
-                if let MarkdownEvent::Start(MarkdownTag::Image { dest_url, .. }) = event {
-                    if let Some(data_url) = dest_url.strip_prefix("data:") {
-                        let Some((mime_info, data)) = data_url.split_once(',') else {
-                            continue;
-                        };
-                        let Some((mime_type, encoding)) = mime_info.split_once(';') else {
-                            continue;
-                        };
-                        let Some(format) = ImageFormat::from_mime_type(mime_type) else {
-                            continue;
-                        };
-                        let is_base64 = encoding == "base64";
-                        if is_base64 {
-                            if let Some(bytes) = base64::prelude::BASE64_STANDARD
-                                .decode(data)
-                                .log_with_level(Level::Debug)
-                            {
-                                let image = Arc::new(Image::from_bytes(format, bytes));
-                                images_by_source_offset.insert(range.start, image);
-                            }
-                        }
+                if let MarkdownEvent::Start(MarkdownTag::Image { dest_url, .. }) = event
+                    && let Some(data_url) = dest_url.strip_prefix("data:")
+                {
+                    let Some((mime_info, data)) = data_url.split_once(',') else {
+                        continue;
+                    };
+                    let Some((mime_type, encoding)) = mime_info.split_once(';') else {
+                        continue;
+                    };
+                    let Some(format) = ImageFormat::from_mime_type(mime_type) else {
+                        continue;
+                    };
+                    let is_base64 = encoding == "base64";
+                    if is_base64
+                        && let Some(bytes) = base64::prelude::BASE64_STANDARD
+                            .decode(data)
+                            .log_with_level(Level::Debug)
+                    {
+                        let image = Arc::new(Image::from_bytes(format, bytes));
+                        images_by_source_offset.insert(range.start, image);
                     }
                 }
             }
@@ -659,13 +658,13 @@ impl MarkdownElement {
             let rendered_text = rendered_text.clone();
             move |markdown, event: &MouseUpEvent, phase, window, cx| {
                 if phase.bubble() {
-                    if let Some(pressed_link) = markdown.pressed_link.take() {
-                        if Some(&pressed_link) == rendered_text.link_for_position(event.position) {
-                            if let Some(open_url) = on_open_url.as_ref() {
-                                open_url(pressed_link.destination_url, window, cx);
-                            } else {
-                                cx.open_url(&pressed_link.destination_url);
-                            }
+                    if let Some(pressed_link) = markdown.pressed_link.take()
+                        && Some(&pressed_link) == rendered_text.link_for_position(event.position)
+                    {
+                        if let Some(open_url) = on_open_url.as_ref() {
+                            open_url(pressed_link.destination_url, window, cx);
+                        } else {
+                            cx.open_url(&pressed_link.destination_url);
                         }
                     }
                 } else if markdown.selection.pending {
@@ -758,10 +757,10 @@ impl Element for MarkdownElement {
         let mut current_img_block_range: Option<Range<usize>> = None;
         for (range, event) in parsed_markdown.events.iter() {
             // Skip alt text for images that rendered
-            if let Some(current_img_block_range) = &current_img_block_range {
-                if current_img_block_range.end > range.end {
-                    continue;
-                }
+            if let Some(current_img_block_range) = &current_img_block_range
+                && current_img_block_range.end > range.end
+            {
+                continue;
             }
 
             match event {
@@ -875,7 +874,7 @@ impl Element for MarkdownElement {
                                 (CodeBlockRenderer::Custom { render, .. }, _) => {
                                     let parent_container = render(
                                         kind,
-                                        &parsed_markdown,
+                                        parsed_markdown,
                                         range.clone(),
                                         metadata.clone(),
                                         window,
@@ -1084,7 +1083,15 @@ impl Element for MarkdownElement {
                                     self.markdown.clone(),
                                     cx,
                                 );
-                                el.child(div().absolute().top_1().right_1().w_5().child(codeblock))
+                                el.child(
+                                    h_flex()
+                                        .w_5()
+                                        .absolute()
+                                        .top_1()
+                                        .right_1()
+                                        .justify_center()
+                                        .child(codeblock),
+                                )
                             });
                         }
 
@@ -1312,11 +1319,11 @@ fn render_copy_code_block_button(
         },
     )
     .icon_color(Color::Muted)
+    .icon_size(IconSize::Small)
     .shape(ui::IconButtonShape::Square)
     .tooltip(Tooltip::text("Copy Code"))
     .on_click({
-        let id = id.clone();
-        let markdown = markdown.clone();
+        let markdown = markdown;
         move |_event, _window, cx| {
             let id = id.clone();
             markdown.update(cx, |this, cx| {
@@ -1693,10 +1700,10 @@ impl RenderedText {
         while let Some(line) = lines.next() {
             let line_bounds = line.layout.bounds();
             if position.y > line_bounds.bottom() {
-                if let Some(next_line) = lines.peek() {
-                    if position.y < next_line.layout.bounds().top() {
-                        return Err(line.source_end);
-                    }
+                if let Some(next_line) = lines.peek()
+                    && position.y < next_line.layout.bounds().top()
+                {
+                    return Err(line.source_end);
                 }
 
                 continue;

crates/markdown/src/parser.rs 🔗

@@ -247,7 +247,7 @@ pub fn parse_markdown(
                             events.push(event_for(
                                 text,
                                 range.source_range.start..range.source_range.start + prefix_len,
-                                &head,
+                                head,
                             ));
                             range.parsed = CowStr::Boxed(tail.into());
                             range.merged_range.start += prefix_len;

crates/markdown_preview/src/markdown_parser.rs 🔗

@@ -76,22 +76,22 @@ impl<'a> MarkdownParser<'a> {
         if self.eof() || (steps + self.cursor) >= self.tokens.len() {
             return self.tokens.last();
         }
-        return self.tokens.get(self.cursor + steps);
+        self.tokens.get(self.cursor + steps)
     }
 
     fn previous(&self) -> Option<&(Event<'_>, Range<usize>)> {
         if self.cursor == 0 || self.cursor > self.tokens.len() {
             return None;
         }
-        return self.tokens.get(self.cursor - 1);
+        self.tokens.get(self.cursor - 1)
     }
 
     fn current(&self) -> Option<&(Event<'_>, Range<usize>)> {
-        return self.peek(0);
+        self.peek(0)
     }
 
     fn current_event(&self) -> Option<&Event<'_>> {
-        return self.current().map(|(event, _)| event);
+        self.current().map(|(event, _)| event)
     }
 
     fn is_text_like(event: &Event) -> bool {
@@ -178,7 +178,6 @@ impl<'a> MarkdownParser<'a> {
                 _ => None,
             },
             Event::Rule => {
-                let source_range = source_range.clone();
                 self.cursor += 1;
                 Some(vec![ParsedMarkdownElement::HorizontalRule(source_range)])
             }
@@ -300,13 +299,12 @@ impl<'a> MarkdownParser<'a> {
 
                     if style != MarkdownHighlightStyle::default() && last_run_len < text.len() {
                         let mut new_highlight = true;
-                        if let Some((last_range, last_style)) = highlights.last_mut() {
-                            if last_range.end == last_run_len
-                                && last_style == &MarkdownHighlight::Style(style.clone())
-                            {
-                                last_range.end = text.len();
-                                new_highlight = false;
-                            }
+                        if let Some((last_range, last_style)) = highlights.last_mut()
+                            && last_range.end == last_run_len
+                            && last_style == &MarkdownHighlight::Style(style.clone())
+                        {
+                            last_range.end = text.len();
+                            new_highlight = false;
                         }
                         if new_highlight {
                             highlights.push((
@@ -402,7 +400,7 @@ impl<'a> MarkdownParser<'a> {
         }
         if !text.is_empty() {
             markdown_text_like.push(MarkdownParagraphChunk::Text(ParsedMarkdownText {
-                source_range: source_range.clone(),
+                source_range,
                 contents: text,
                 highlights,
                 regions,
@@ -421,7 +419,7 @@ impl<'a> MarkdownParser<'a> {
         self.cursor += 1;
 
         ParsedMarkdownHeading {
-            source_range: source_range.clone(),
+            source_range,
             level: match level {
                 pulldown_cmark::HeadingLevel::H1 => HeadingLevel::H1,
                 pulldown_cmark::HeadingLevel::H2 => HeadingLevel::H2,
@@ -579,10 +577,10 @@ impl<'a> MarkdownParser<'a> {
                             }
                         } else {
                             let block = self.parse_block().await;
-                            if let Some(block) = block {
-                                if let Some(list_item) = items_stack.last_mut() {
-                                    list_item.content.extend(block);
-                                }
+                            if let Some(block) = block
+                                && let Some(list_item) = items_stack.last_mut()
+                            {
+                                list_item.content.extend(block);
                             }
                         }
                     }

crates/markdown_preview/src/markdown_preview_view.rs 🔗

@@ -115,8 +115,7 @@ impl MarkdownPreviewView {
                         pane.activate_item(existing_follow_view_idx, true, true, window, cx);
                     });
                 } else {
-                    let view =
-                        Self::create_following_markdown_view(workspace, editor.clone(), window, cx);
+                    let view = Self::create_following_markdown_view(workspace, editor, window, cx);
                     workspace.active_pane().update(cx, |pane, cx| {
                         pane.add_item(Box::new(view.clone()), true, true, None, window, cx)
                     });
@@ -151,10 +150,9 @@ impl MarkdownPreviewView {
         if let Some(editor) = workspace
             .active_item(cx)
             .and_then(|item| item.act_as::<Editor>(cx))
+            && Self::is_markdown_file(&editor, cx)
         {
-            if Self::is_markdown_file(&editor, cx) {
-                return Some(editor);
-            }
+            return Some(editor);
         }
         None
     }
@@ -243,32 +241,30 @@ impl MarkdownPreviewView {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        if let Some(item) = active_item {
-            if item.item_id() != cx.entity_id() {
-                if let Some(editor) = item.act_as::<Editor>(cx) {
-                    if Self::is_markdown_file(&editor, cx) {
-                        self.set_editor(editor, window, cx);
-                    }
-                }
-            }
+        if let Some(item) = active_item
+            && item.item_id() != cx.entity_id()
+            && let Some(editor) = item.act_as::<Editor>(cx)
+            && Self::is_markdown_file(&editor, cx)
+        {
+            self.set_editor(editor, window, cx);
         }
     }
 
     pub fn is_markdown_file<V>(editor: &Entity<Editor>, cx: &mut Context<V>) -> bool {
         let buffer = editor.read(cx).buffer().read(cx);
-        if let Some(buffer) = buffer.as_singleton() {
-            if let Some(language) = buffer.read(cx).language() {
-                return language.name() == "Markdown".into();
-            }
+        if let Some(buffer) = buffer.as_singleton()
+            && let Some(language) = buffer.read(cx).language()
+        {
+            return language.name() == "Markdown".into();
         }
         false
     }
 
     fn set_editor(&mut self, editor: Entity<Editor>, window: &mut Window, cx: &mut Context<Self>) {
-        if let Some(active) = &self.active_editor {
-            if active.editor == editor {
-                return;
-            }
+        if let Some(active) = &self.active_editor
+            && active.editor == editor
+        {
+            return;
         }
 
         let subscription = cx.subscribe_in(
@@ -552,21 +548,20 @@ impl Render for MarkdownPreviewView {
                                 .group("markdown-block")
                                 .on_click(cx.listener(
                                     move |this, event: &ClickEvent, window, cx| {
-                                        if event.click_count() == 2 {
-                                            if let Some(source_range) = this
+                                        if event.click_count() == 2
+                                            && let Some(source_range) = this
                                                 .contents
                                                 .as_ref()
                                                 .and_then(|c| c.children.get(ix))
                                                 .and_then(|block: &ParsedMarkdownElement| {
                                                     block.source_range()
                                                 })
-                                            {
-                                                this.move_cursor_to_block(
-                                                    window,
-                                                    cx,
-                                                    source_range.start..source_range.start,
-                                                );
-                                            }
+                                        {
+                                            this.move_cursor_to_block(
+                                                window,
+                                                cx,
+                                                source_range.start..source_range.start,
+                                            );
                                         }
                                     },
                                 ))

crates/markdown_preview/src/markdown_renderer.rs 🔗

@@ -111,11 +111,10 @@ impl RenderContext {
     /// buffer font size changes. The callees of this function should be reimplemented to use real
     /// relative sizing once that is implemented in GPUI
     pub fn scaled_rems(&self, rems: f32) -> Rems {
-        return self
-            .buffer_text_style
+        self.buffer_text_style
             .font_size
             .to_rems(self.window_rem_size)
-            .mul(rems);
+            .mul(rems)
     }
 
     /// This ensures that children inside of block quotes
@@ -459,13 +458,13 @@ fn render_markdown_table(parsed: &ParsedMarkdownTable, cx: &mut RenderContext) -
     let mut max_lengths: Vec<usize> = vec![0; parsed.header.children.len()];
 
     for (index, cell) in parsed.header.children.iter().enumerate() {
-        let length = paragraph_len(&cell);
+        let length = paragraph_len(cell);
         max_lengths[index] = length;
     }
 
     for row in &parsed.body {
         for (index, cell) in row.children.iter().enumerate() {
-            let length = paragraph_len(&cell);
+            let length = paragraph_len(cell);
 
             if length > max_lengths[index] {
                 max_lengths[index] = length;

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

@@ -20,14 +20,14 @@ fn replace_deprecated_settings_values(
         .nodes_for_capture_index(parent_object_capture_ix)
         .next()?
         .byte_range();
-    let parent_object_name = contents.get(parent_object_range.clone())?;
+    let parent_object_name = contents.get(parent_object_range)?;
 
     let setting_name_ix = query.capture_index_for_name("setting_name")?;
     let setting_name_range = mat
         .nodes_for_capture_index(setting_name_ix)
         .next()?
         .byte_range();
-    let setting_name = contents.get(setting_name_range.clone())?;
+    let setting_name = contents.get(setting_name_range)?;
 
     let setting_value_ix = query.capture_index_for_name("setting_value")?;
     let setting_value_range = mat

crates/migrator/src/migrations/m_2025_01_29/keymap.rs 🔗

@@ -242,22 +242,22 @@ static STRING_REPLACE: LazyLock<HashMap<&str, &str>> = LazyLock::new(|| {
             "inline_completion::ToggleMenu",
             "edit_prediction::ToggleMenu",
         ),
-        ("editor::NextEditPrediction", "editor::NextEditPrediction"),
+        ("editor::NextInlineCompletion", "editor::NextEditPrediction"),
         (
-            "editor::PreviousEditPrediction",
+            "editor::PreviousInlineCompletion",
             "editor::PreviousEditPrediction",
         ),
         (
-            "editor::AcceptPartialEditPrediction",
+            "editor::AcceptPartialInlineCompletion",
             "editor::AcceptPartialEditPrediction",
         ),
-        ("editor::ShowEditPrediction", "editor::ShowEditPrediction"),
+        ("editor::ShowInlineCompletion", "editor::ShowEditPrediction"),
         (
-            "editor::AcceptEditPrediction",
+            "editor::AcceptInlineCompletion",
             "editor::AcceptEditPrediction",
         ),
         (
-            "editor::ToggleEditPredictions",
+            "editor::ToggleInlineCompletions",
             "editor::ToggleEditPrediction",
         ),
     ])
@@ -279,7 +279,7 @@ fn rename_context_key(
         new_predicate = new_predicate.replace(old_key, new_key);
     }
     if new_predicate != old_predicate {
-        Some((context_predicate_range, new_predicate.to_string()))
+        Some((context_predicate_range, new_predicate))
     } else {
         None
     }

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

@@ -57,7 +57,7 @@ pub fn replace_edit_prediction_provider_setting(
         .nodes_for_capture_index(parent_object_capture_ix)
         .next()?
         .byte_range();
-    let parent_object_name = contents.get(parent_object_range.clone())?;
+    let parent_object_name = contents.get(parent_object_range)?;
 
     let setting_name_ix = query.capture_index_for_name("setting_name")?;
     let setting_range = mat

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

@@ -25,7 +25,7 @@ fn replace_tab_close_button_setting_key(
         .nodes_for_capture_index(parent_object_capture_ix)
         .next()?
         .byte_range();
-    let parent_object_name = contents.get(parent_object_range.clone())?;
+    let parent_object_name = contents.get(parent_object_range)?;
 
     let setting_name_ix = query.capture_index_for_name("setting_name")?;
     let setting_range = mat
@@ -51,14 +51,14 @@ fn replace_tab_close_button_setting_value(
         .nodes_for_capture_index(parent_object_capture_ix)
         .next()?
         .byte_range();
-    let parent_object_name = contents.get(parent_object_range.clone())?;
+    let parent_object_name = contents.get(parent_object_range)?;
 
     let setting_name_ix = query.capture_index_for_name("setting_name")?;
     let setting_name_range = mat
         .nodes_for_capture_index(setting_name_ix)
         .next()?
         .byte_range();
-    let setting_name = contents.get(setting_name_range.clone())?;
+    let setting_name = contents.get(setting_name_range)?;
 
     let setting_value_ix = query.capture_index_for_name("setting_value")?;
     let setting_value_range = mat

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

@@ -19,7 +19,7 @@ fn replace_setting_value(
         .nodes_for_capture_index(setting_capture_ix)
         .next()?
         .byte_range();
-    let setting_name = contents.get(setting_name_range.clone())?;
+    let setting_name = contents.get(setting_name_range)?;
 
     if setting_name != "hide_mouse_while_typing" {
         return None;

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

@@ -24,7 +24,7 @@ fn rename_assistant(
         .nodes_for_capture_index(key_capture_ix)
         .next()?
         .byte_range();
-    return Some((key_range, "agent".to_string()));
+    Some((key_range, "agent".to_string()))
 }
 
 fn rename_edit_prediction_assistant(
@@ -37,5 +37,5 @@ fn rename_edit_prediction_assistant(
         .nodes_for_capture_index(key_capture_ix)
         .next()?
         .byte_range();
-    return Some((key_range, "enabled_in_text_threads".to_string()));
+    Some((key_range, "enabled_in_text_threads".to_string()))
 }

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

@@ -19,7 +19,7 @@ fn replace_preferred_completion_mode_value(
         .nodes_for_capture_index(parent_object_capture_ix)
         .next()?
         .byte_range();
-    let parent_object_name = contents.get(parent_object_range.clone())?;
+    let parent_object_name = contents.get(parent_object_range)?;
 
     if parent_object_name != "agent" {
         return None;
@@ -30,7 +30,7 @@ fn replace_preferred_completion_mode_value(
         .nodes_for_capture_index(setting_name_capture_ix)
         .next()?
         .byte_range();
-    let setting_name = contents.get(setting_name_range.clone())?;
+    let setting_name = contents.get(setting_name_range)?;
 
     if setting_name != "preferred_completion_mode" {
         return None;

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

@@ -40,20 +40,20 @@ fn migrate_context_server_settings(
     // Parse the server settings to check what keys it contains
     let mut cursor = server_settings.walk();
     for child in server_settings.children(&mut cursor) {
-        if child.kind() == "pair" {
-            if let Some(key_node) = child.child_by_field_name("key") {
-                if let (None, Some(quote_content)) = (column, key_node.child(0)) {
-                    column = Some(quote_content.start_position().column);
-                }
-                if let Some(string_content) = key_node.child(1) {
-                    let key = &contents[string_content.byte_range()];
-                    match key {
-                        // If it already has a source key, don't modify it
-                        "source" => return None,
-                        "command" => has_command = true,
-                        "settings" => has_settings = true,
-                        _ => other_keys += 1,
-                    }
+        if child.kind() == "pair"
+            && let Some(key_node) = child.child_by_field_name("key")
+        {
+            if let (None, Some(quote_content)) = (column, key_node.child(0)) {
+                column = Some(quote_content.start_position().column);
+            }
+            if let Some(string_content) = key_node.child(1) {
+                let key = &contents[string_content.byte_range()];
+                match key {
+                    // If it already has a source key, don't modify it
+                    "source" => return None,
+                    "command" => has_command = true,
+                    "settings" => has_settings = true,
+                    _ => other_keys += 1,
                 }
             }
         }

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

@@ -84,10 +84,10 @@ fn remove_pair_with_whitespace(
         }
     } else {
         // If no next sibling, check if there's a comma before
-        if let Some(prev_sibling) = pair_node.prev_sibling() {
-            if prev_sibling.kind() == "," {
-                range_to_remove.start = prev_sibling.start_byte();
-            }
+        if let Some(prev_sibling) = pair_node.prev_sibling()
+            && prev_sibling.kind() == ","
+        {
+            range_to_remove.start = prev_sibling.start_byte();
         }
     }
 
@@ -123,10 +123,10 @@ fn remove_pair_with_whitespace(
 
     // Also check if we need to include trailing whitespace up to the next line
     let text_after = &contents[range_to_remove.end..];
-    if let Some(newline_pos) = text_after.find('\n') {
-        if text_after[..newline_pos].chars().all(|c| c.is_whitespace()) {
-            range_to_remove.end += newline_pos + 1;
-        }
+    if let Some(newline_pos) = text_after.find('\n')
+        && text_after[..newline_pos].chars().all(|c| c.is_whitespace())
+    {
+        range_to_remove.end += newline_pos + 1;
     }
 
     Some((range_to_remove, String::new()))

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

@@ -56,19 +56,18 @@ fn flatten_context_server_command(
 
     let mut cursor = command_object.walk();
     for child in command_object.children(&mut cursor) {
-        if child.kind() == "pair" {
-            if let Some(key_node) = child.child_by_field_name("key") {
-                if let Some(string_content) = key_node.child(1) {
-                    let key = &contents[string_content.byte_range()];
-                    if let Some(value_node) = child.child_by_field_name("value") {
-                        let value_range = value_node.byte_range();
-                        match key {
-                            "path" => path_value = Some(&contents[value_range]),
-                            "args" => args_value = Some(&contents[value_range]),
-                            "env" => env_value = Some(&contents[value_range]),
-                            _ => {}
-                        }
-                    }
+        if child.kind() == "pair"
+            && let Some(key_node) = child.child_by_field_name("key")
+            && let Some(string_content) = key_node.child(1)
+        {
+            let key = &contents[string_content.byte_range()];
+            if let Some(value_node) = child.child_by_field_name("value") {
+                let value_range = value_node.byte_range();
+                match key {
+                    "path" => path_value = Some(&contents[value_range]),
+                    "args" => args_value = Some(&contents[value_range]),
+                    "env" => env_value = Some(&contents[value_range]),
+                    _ => {}
                 }
             }
         }

crates/migrator/src/migrator.rs 🔗

@@ -28,7 +28,7 @@ fn migrate(text: &str, patterns: MigrationPatterns, query: &Query) -> Result<Opt
     let mut parser = tree_sitter::Parser::new();
     parser.set_language(&tree_sitter_json::LANGUAGE.into())?;
     let syntax_tree = parser
-        .parse(&text, None)
+        .parse(text, None)
         .context("failed to parse settings")?;
 
     let mut cursor = tree_sitter::QueryCursor::new();
@@ -37,7 +37,7 @@ fn migrate(text: &str, patterns: MigrationPatterns, query: &Query) -> Result<Opt
     let mut edits = vec![];
     while let Some(mat) = matches.next() {
         if let Some((_, callback)) = patterns.get(mat.pattern_index) {
-            edits.extend(callback(&text, &mat, query));
+            edits.extend(callback(text, mat, query));
         }
     }
 
@@ -170,7 +170,7 @@ pub fn migrate_settings(text: &str) -> Result<Option<String>> {
 
 pub fn migrate_edit_prediction_provider_settings(text: &str) -> Result<Option<String>> {
     migrate(
-        &text,
+        text,
         &[(
             SETTINGS_NESTED_KEY_VALUE_PATTERN,
             migrations::m_2025_01_29::replace_edit_prediction_provider_setting,
@@ -293,12 +293,12 @@ mod tests {
     use super::*;
 
     fn assert_migrate_keymap(input: &str, output: Option<&str>) {
-        let migrated = migrate_keymap(&input).unwrap();
+        let migrated = migrate_keymap(input).unwrap();
         pretty_assertions::assert_eq!(migrated.as_deref(), output);
     }
 
     fn assert_migrate_settings(input: &str, output: Option<&str>) {
-        let migrated = migrate_settings(&input).unwrap();
+        let migrated = migrate_settings(input).unwrap();
         pretty_assertions::assert_eq!(migrated.as_deref(), output);
     }
 

crates/mistral/src/mistral.rs 🔗

@@ -86,6 +86,7 @@ pub enum Model {
         max_completion_tokens: Option<u64>,
         supports_tools: Option<bool>,
         supports_images: Option<bool>,
+        supports_thinking: Option<bool>,
     },
 }
 
@@ -214,6 +215,16 @@ impl Model {
             } => supports_images.unwrap_or(false),
         }
     }
+
+    pub fn supports_thinking(&self) -> bool {
+        match self {
+            Self::MagistralMediumLatest | Self::MagistralSmallLatest => true,
+            Self::Custom {
+                supports_thinking, ..
+            } => supports_thinking.unwrap_or(false),
+            _ => false,
+        }
+    }
 }
 
 #[derive(Debug, Serialize, Deserialize)]
@@ -288,7 +299,9 @@ pub enum ToolChoice {
 #[serde(tag = "role", rename_all = "lowercase")]
 pub enum RequestMessage {
     Assistant {
-        content: Option<String>,
+        #[serde(flatten)]
+        #[serde(default, skip_serializing_if = "Option::is_none")]
+        content: Option<MessageContent>,
         #[serde(default, skip_serializing_if = "Vec::is_empty")]
         tool_calls: Vec<ToolCall>,
     },
@@ -297,7 +310,8 @@ pub enum RequestMessage {
         content: MessageContent,
     },
     System {
-        content: String,
+        #[serde(flatten)]
+        content: MessageContent,
     },
     Tool {
         content: String,
@@ -305,7 +319,7 @@ pub enum RequestMessage {
     },
 }
 
-#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
+#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)]
 #[serde(untagged)]
 pub enum MessageContent {
     #[serde(rename = "content")]
@@ -346,11 +360,21 @@ impl MessageContent {
     }
 }
 
-#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
+#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)]
 #[serde(tag = "type", rename_all = "snake_case")]
 pub enum MessagePart {
     Text { text: String },
     ImageUrl { image_url: String },
+    Thinking { thinking: Vec<ThinkingPart> },
+}
+
+// Backwards-compatibility alias for provider code that refers to ContentPart
+pub type ContentPart = MessagePart;
+
+#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)]
+#[serde(tag = "type", rename_all = "snake_case")]
+pub enum ThinkingPart {
+    Text { text: String },
 }
 
 #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
@@ -418,24 +442,30 @@ pub struct StreamChoice {
     pub finish_reason: Option<String>,
 }
 
-#[derive(Serialize, Deserialize, Debug)]
+#[derive(Serialize, Deserialize, Debug, Clone)]
 pub struct StreamDelta {
     pub role: Option<Role>,
-    pub content: Option<String>,
     #[serde(default, skip_serializing_if = "Option::is_none")]
-    pub tool_calls: Option<Vec<ToolCallChunk>>,
+    pub content: Option<MessageContentDelta>,
     #[serde(default, skip_serializing_if = "Option::is_none")]
-    pub reasoning_content: Option<String>,
+    pub tool_calls: Option<Vec<ToolCallChunk>>,
 }
 
-#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
+#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)]
+#[serde(untagged)]
+pub enum MessageContentDelta {
+    Text(String),
+    Parts(Vec<MessagePart>),
+}
+
+#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)]
 pub struct ToolCallChunk {
     pub index: usize,
     pub id: Option<String>,
     pub function: Option<FunctionChunk>,
 }
 
-#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
+#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)]
 pub struct FunctionChunk {
     pub name: Option<String>,
     pub arguments: Option<String>,

crates/multi_buffer/src/anchor.rs 🔗

@@ -76,27 +76,26 @@ impl Anchor {
             if text_cmp.is_ne() {
                 return text_cmp;
             }
-            if self.diff_base_anchor.is_some() || other.diff_base_anchor.is_some() {
-                if let Some(base_text) = snapshot
+            if (self.diff_base_anchor.is_some() || other.diff_base_anchor.is_some())
+                && let Some(base_text) = snapshot
                     .diffs
                     .get(&excerpt.buffer_id)
                     .map(|diff| diff.base_text())
-                {
-                    let self_anchor = self.diff_base_anchor.filter(|a| base_text.can_resolve(a));
-                    let other_anchor = other.diff_base_anchor.filter(|a| base_text.can_resolve(a));
-                    return match (self_anchor, other_anchor) {
-                        (Some(a), Some(b)) => a.cmp(&b, base_text),
-                        (Some(_), None) => match other.text_anchor.bias {
-                            Bias::Left => Ordering::Greater,
-                            Bias::Right => Ordering::Less,
-                        },
-                        (None, Some(_)) => match self.text_anchor.bias {
-                            Bias::Left => Ordering::Less,
-                            Bias::Right => Ordering::Greater,
-                        },
-                        (None, None) => Ordering::Equal,
-                    };
-                }
+            {
+                let self_anchor = self.diff_base_anchor.filter(|a| base_text.can_resolve(a));
+                let other_anchor = other.diff_base_anchor.filter(|a| base_text.can_resolve(a));
+                return match (self_anchor, other_anchor) {
+                    (Some(a), Some(b)) => a.cmp(&b, base_text),
+                    (Some(_), None) => match other.text_anchor.bias {
+                        Bias::Left => Ordering::Greater,
+                        Bias::Right => Ordering::Less,
+                    },
+                    (None, Some(_)) => match self.text_anchor.bias {
+                        Bias::Left => Ordering::Less,
+                        Bias::Right => Ordering::Greater,
+                    },
+                    (None, None) => Ordering::Equal,
+                };
             }
         }
         Ordering::Equal
@@ -107,51 +106,49 @@ impl Anchor {
     }
 
     pub fn bias_left(&self, snapshot: &MultiBufferSnapshot) -> Anchor {
-        if self.text_anchor.bias != Bias::Left {
-            if let Some(excerpt) = snapshot.excerpt(self.excerpt_id) {
-                return Self {
-                    buffer_id: self.buffer_id,
-                    excerpt_id: self.excerpt_id,
-                    text_anchor: self.text_anchor.bias_left(&excerpt.buffer),
-                    diff_base_anchor: self.diff_base_anchor.map(|a| {
-                        if let Some(base_text) = snapshot
-                            .diffs
-                            .get(&excerpt.buffer_id)
-                            .map(|diff| diff.base_text())
-                        {
-                            if a.buffer_id == Some(base_text.remote_id()) {
-                                return a.bias_left(base_text);
-                            }
-                        }
-                        a
-                    }),
-                };
-            }
+        if self.text_anchor.bias != Bias::Left
+            && let Some(excerpt) = snapshot.excerpt(self.excerpt_id)
+        {
+            return Self {
+                buffer_id: self.buffer_id,
+                excerpt_id: self.excerpt_id,
+                text_anchor: self.text_anchor.bias_left(&excerpt.buffer),
+                diff_base_anchor: self.diff_base_anchor.map(|a| {
+                    if let Some(base_text) = snapshot
+                        .diffs
+                        .get(&excerpt.buffer_id)
+                        .map(|diff| diff.base_text())
+                        && a.buffer_id == Some(base_text.remote_id())
+                    {
+                        return a.bias_left(base_text);
+                    }
+                    a
+                }),
+            };
         }
         *self
     }
 
     pub fn bias_right(&self, snapshot: &MultiBufferSnapshot) -> Anchor {
-        if self.text_anchor.bias != Bias::Right {
-            if let Some(excerpt) = snapshot.excerpt(self.excerpt_id) {
-                return Self {
-                    buffer_id: self.buffer_id,
-                    excerpt_id: self.excerpt_id,
-                    text_anchor: self.text_anchor.bias_right(&excerpt.buffer),
-                    diff_base_anchor: self.diff_base_anchor.map(|a| {
-                        if let Some(base_text) = snapshot
-                            .diffs
-                            .get(&excerpt.buffer_id)
-                            .map(|diff| diff.base_text())
-                        {
-                            if a.buffer_id == Some(base_text.remote_id()) {
-                                return a.bias_right(&base_text);
-                            }
-                        }
-                        a
-                    }),
-                };
-            }
+        if self.text_anchor.bias != Bias::Right
+            && let Some(excerpt) = snapshot.excerpt(self.excerpt_id)
+        {
+            return Self {
+                buffer_id: self.buffer_id,
+                excerpt_id: self.excerpt_id,
+                text_anchor: self.text_anchor.bias_right(&excerpt.buffer),
+                diff_base_anchor: self.diff_base_anchor.map(|a| {
+                    if let Some(base_text) = snapshot
+                        .diffs
+                        .get(&excerpt.buffer_id)
+                        .map(|diff| diff.base_text())
+                        && a.buffer_id == Some(base_text.remote_id())
+                    {
+                        return a.bias_right(base_text);
+                    }
+                    a
+                }),
+            };
         }
         *self
     }
@@ -212,7 +209,7 @@ impl AnchorRangeExt for Range<Anchor> {
     }
 
     fn includes(&self, other: &Range<Anchor>, buffer: &MultiBufferSnapshot) -> bool {
-        self.start.cmp(&other.start, &buffer).is_le() && other.end.cmp(&self.end, &buffer).is_le()
+        self.start.cmp(&other.start, buffer).is_le() && other.end.cmp(&self.end, buffer).is_le()
     }
 
     fn overlaps(&self, other: &Range<Anchor>, buffer: &MultiBufferSnapshot) -> bool {

crates/multi_buffer/src/multi_buffer.rs 🔗

@@ -1082,11 +1082,11 @@ impl MultiBuffer {
 
                 let mut ranges: Vec<Range<usize>> = Vec::new();
                 for edit in edits {
-                    if let Some(last_range) = ranges.last_mut() {
-                        if edit.range.start <= last_range.end {
-                            last_range.end = last_range.end.max(edit.range.end);
-                            continue;
-                        }
+                    if let Some(last_range) = ranges.last_mut()
+                        && edit.range.start <= last_range.end
+                    {
+                        last_range.end = last_range.end.max(edit.range.end);
+                        continue;
                     }
                     ranges.push(edit.range);
                 }
@@ -1146,13 +1146,13 @@ impl MultiBuffer {
 
     pub fn last_transaction_id(&self, cx: &App) -> Option<TransactionId> {
         if let Some(buffer) = self.as_singleton() {
-            return buffer
+            buffer
                 .read(cx)
                 .peek_undo_stack()
-                .map(|history_entry| history_entry.transaction_id());
+                .map(|history_entry| history_entry.transaction_id())
         } else {
             let last_transaction = self.history.undo_stack.last()?;
-            return Some(last_transaction.id);
+            Some(last_transaction.id)
         }
     }
 
@@ -1212,25 +1212,24 @@ impl MultiBuffer {
             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() {
-                        if 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);
+                    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));
+                            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.push(start..end);
+                            break;
                         }
                     }
                 }
@@ -1251,25 +1250,25 @@ impl MultiBuffer {
             buffer.update(cx, |buffer, _| {
                 buffer.merge_transactions(transaction, destination)
             });
-        } else if let Some(transaction) = self.history.forget(transaction) {
-            if 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);
+        } 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);
                 }
             }
         }
@@ -1562,11 +1561,11 @@ impl MultiBuffer {
             });
             let mut merged_ranges: Vec<ExcerptRange<Point>> = Vec::new();
             for range in expanded_ranges {
-                if let Some(last_range) = merged_ranges.last_mut() {
-                    if last_range.context.end >= range.context.start {
-                        last_range.context.end = range.context.end;
-                        continue;
-                    }
+                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)
             }
@@ -1686,7 +1685,7 @@ impl MultiBuffer {
         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);
+            self.update_path_excerpts(path, buffer, buffer_snapshot, new, cx);
 
         let mut result = Vec::new();
         let mut ranges = ranges.into_iter();
@@ -1726,7 +1725,7 @@ impl MultiBuffer {
             merged_ranges.push(range.clone());
             counts.push(1);
         }
-        return (merged_ranges, counts);
+        (merged_ranges, counts)
     }
 
     fn update_path_excerpts(
@@ -1784,7 +1783,7 @@ impl MultiBuffer {
                     }
                     Some((
                         *existing_id,
-                        excerpt.range.context.to_point(&buffer_snapshot),
+                        excerpt.range.context.to_point(buffer_snapshot),
                     ))
                 } else {
                     None
@@ -1794,25 +1793,25 @@ impl MultiBuffer {
             };
 
             if let Some((last_id, last)) = to_insert.last_mut() {
-                if let Some(new) = new {
-                    if 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(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 {
-                    if 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;
-                    }
+                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;
                 }
             }
 
@@ -2105,10 +2104,10 @@ impl MultiBuffer {
             .flatten()
         {
             cursor.seek_forward(&Some(locator), Bias::Left);
-            if let Some(excerpt) = cursor.item() {
-                if excerpt.locator == *locator {
-                    excerpts.push((excerpt.id, excerpt.range.clone()));
-                }
+            if let Some(excerpt) = cursor.item()
+                && excerpt.locator == *locator
+            {
+                excerpts.push((excerpt.id, excerpt.range.clone()));
             }
         }
 
@@ -2132,22 +2131,21 @@ impl MultiBuffer {
         let mut result = Vec::new();
         for locator in locators {
             excerpts.seek_forward(&Some(locator), Bias::Left);
-            if let Some(excerpt) = excerpts.item() {
-                if excerpt.locator == *locator {
-                    let excerpt_start = excerpts.start().1.clone();
-                    let excerpt_end =
-                        ExcerptDimension(excerpt_start.0 + excerpt.text_summary.lines);
+            if let Some(excerpt) = excerpts.item()
+                && excerpt.locator == *locator
+            {
+                let excerpt_start = excerpts.start().1.clone();
+                let excerpt_end = ExcerptDimension(excerpt_start.0 + excerpt.text_summary.lines);
 
-                    diff_transforms.seek_forward(&excerpt_start, Bias::Left);
-                    let overshoot = excerpt_start.0 - diff_transforms.start().0.0;
-                    let start = diff_transforms.start().1.0 + overshoot;
+                diff_transforms.seek_forward(&excerpt_start, Bias::Left);
+                let overshoot = excerpt_start.0 - diff_transforms.start().0.0;
+                let start = diff_transforms.start().1.0 + overshoot;
 
-                    diff_transforms.seek_forward(&excerpt_end, Bias::Right);
-                    let overshoot = excerpt_end.0 - diff_transforms.start().0.0;
-                    let end = diff_transforms.start().1.0 + overshoot;
+                diff_transforms.seek_forward(&excerpt_end, Bias::Right);
+                let overshoot = excerpt_end.0 - diff_transforms.start().0.0;
+                let end = diff_transforms.start().1.0 + overshoot;
 
-                    result.push(start..end)
-                }
+                result.push(start..end)
             }
         }
         result
@@ -2198,6 +2196,15 @@ impl MultiBuffer {
             })
     }
 
+    pub fn buffer_for_anchor(&self, anchor: Anchor, cx: &App) -> Option<Entity<Buffer>> {
+        if let Some(buffer_id) = anchor.buffer_id {
+            self.buffer(buffer_id)
+        } else {
+            let (_, buffer, _) = self.excerpt_containing(anchor, cx)?;
+            Some(buffer)
+        }
+    }
+
     // If point is at the end of the buffer, the last excerpt is returned
     pub fn point_to_buffer_offset<T: ToOffset>(
         &self,
@@ -2316,12 +2323,12 @@ impl MultiBuffer {
                     // Skip over any subsequent excerpts that are also removed.
                     if let Some(&next_excerpt_id) = excerpt_ids.peek() {
                         let next_locator = snapshot.excerpt_locator_for_id(next_excerpt_id);
-                        if let Some(next_excerpt) = cursor.item() {
-                            if next_excerpt.locator == *next_locator {
-                                excerpt_ids.next();
-                                excerpt = next_excerpt;
-                                continue 'remove_excerpts;
-                            }
+                        if let Some(next_excerpt) = cursor.item()
+                            && next_excerpt.locator == *next_locator
+                        {
+                            excerpt_ids.next();
+                            excerpt = next_excerpt;
+                            continue 'remove_excerpts;
                         }
                     }
 
@@ -2429,7 +2436,7 @@ impl MultiBuffer {
         cx.emit(match event {
             language::BufferEvent::Edited => Event::Edited {
                 singleton_buffer_edited: true,
-                edited_buffer: Some(buffer.clone()),
+                edited_buffer: Some(buffer),
             },
             language::BufferEvent::DirtyChanged => Event::DirtyChanged,
             language::BufferEvent::Saved => Event::Saved,
@@ -2484,7 +2491,7 @@ impl MultiBuffer {
         let base_text_changed = snapshot
             .diffs
             .get(&buffer_id)
-            .map_or(true, |old_diff| !new_diff.base_texts_eq(old_diff));
+            .is_none_or(|old_diff| !new_diff.base_texts_eq(old_diff));
 
         snapshot.diffs.insert(buffer_id, new_diff);
 
@@ -2494,33 +2501,33 @@ impl MultiBuffer {
                 .excerpts
                 .cursor::<Dimensions<Option<&Locator>, ExcerptOffset>>(&());
             cursor.seek_forward(&Some(locator), Bias::Left);
-            if let Some(excerpt) = cursor.item() {
-                if excerpt.locator == *locator {
-                    let excerpt_buffer_range = excerpt.range.context.to_offset(&excerpt.buffer);
-                    if diff_change_range.end < excerpt_buffer_range.start
-                        || diff_change_range.start > excerpt_buffer_range.end
-                    {
-                        continue;
-                    }
-                    let excerpt_start = cursor.start().1;
-                    let excerpt_len = ExcerptOffset::new(excerpt.text_summary.len);
-                    let diff_change_start_in_excerpt = ExcerptOffset::new(
-                        diff_change_range
-                            .start
-                            .saturating_sub(excerpt_buffer_range.start),
-                    );
-                    let diff_change_end_in_excerpt = ExcerptOffset::new(
-                        diff_change_range
-                            .end
-                            .saturating_sub(excerpt_buffer_range.start),
-                    );
-                    let edit_start = excerpt_start + diff_change_start_in_excerpt.min(excerpt_len);
-                    let edit_end = excerpt_start + diff_change_end_in_excerpt.min(excerpt_len);
-                    excerpt_edits.push(Edit {
-                        old: edit_start..edit_end,
-                        new: edit_start..edit_end,
-                    });
+            if let Some(excerpt) = cursor.item()
+                && excerpt.locator == *locator
+            {
+                let excerpt_buffer_range = excerpt.range.context.to_offset(&excerpt.buffer);
+                if diff_change_range.end < excerpt_buffer_range.start
+                    || diff_change_range.start > excerpt_buffer_range.end
+                {
+                    continue;
                 }
+                let excerpt_start = cursor.start().1;
+                let excerpt_len = ExcerptOffset::new(excerpt.text_summary.len);
+                let diff_change_start_in_excerpt = ExcerptOffset::new(
+                    diff_change_range
+                        .start
+                        .saturating_sub(excerpt_buffer_range.start),
+                );
+                let diff_change_end_in_excerpt = ExcerptOffset::new(
+                    diff_change_range
+                        .end
+                        .saturating_sub(excerpt_buffer_range.start),
+                );
+                let edit_start = excerpt_start + diff_change_start_in_excerpt.min(excerpt_len);
+                let edit_end = excerpt_start + diff_change_end_in_excerpt.min(excerpt_len);
+                excerpt_edits.push(Edit {
+                    old: edit_start..edit_end,
+                    new: edit_start..edit_end,
+                });
             }
         }
 
@@ -2778,7 +2785,7 @@ impl MultiBuffer {
                 if diff_hunk.excerpt_id.cmp(&end_excerpt_id, &snapshot).is_gt() {
                     continue;
                 }
-                if last_hunk_row.map_or(false, |row| row >= diff_hunk.row_range.start) {
+                if last_hunk_row.is_some_and(|row| row >= diff_hunk.row_range.start) {
                     continue;
                 }
                 let start = Anchor::in_buffer(
@@ -3042,7 +3049,7 @@ impl MultiBuffer {
             is_dirty |= buffer.is_dirty();
             has_deleted_file |= buffer
                 .file()
-                .map_or(false, |file| file.disk_state() == DiskState::Deleted);
+                .is_some_and(|file| file.disk_state() == DiskState::Deleted);
             has_conflict |= buffer.has_conflict();
         }
         if edited {
@@ -3056,7 +3063,7 @@ impl MultiBuffer {
         snapshot.has_conflict = has_conflict;
 
         for (id, diff) in self.diffs.iter() {
-            if snapshot.diffs.get(&id).is_none() {
+            if snapshot.diffs.get(id).is_none() {
                 snapshot.diffs.insert(*id, diff.diff.read(cx).snapshot(cx));
             }
         }
@@ -3155,13 +3162,12 @@ impl MultiBuffer {
                 at_transform_boundary = false;
                 let transforms_before_edit = old_diff_transforms.slice(&edit.old.start, Bias::Left);
                 self.append_diff_transforms(&mut new_diff_transforms, transforms_before_edit);
-                if let Some(transform) = old_diff_transforms.item() {
-                    if old_diff_transforms.end().0 == edit.old.start
-                        && old_diff_transforms.start().0 < edit.old.start
-                    {
-                        self.push_diff_transform(&mut new_diff_transforms, transform.clone());
-                        old_diff_transforms.next();
-                    }
+                if let Some(transform) = old_diff_transforms.item()
+                    && old_diff_transforms.end().0 == edit.old.start
+                    && old_diff_transforms.start().0 < edit.old.start
+                {
+                    self.push_diff_transform(&mut new_diff_transforms, transform.clone());
+                    old_diff_transforms.next();
                 }
             }
 
@@ -3177,7 +3183,7 @@ impl MultiBuffer {
                 &mut new_diff_transforms,
                 &mut end_of_current_insert,
                 &mut old_expanded_hunks,
-                &snapshot,
+                snapshot,
                 change_kind,
             );
 
@@ -3201,9 +3207,10 @@ impl MultiBuffer {
             // If this is the last edit that intersects the current diff transform,
             // then recreate the content up to the end of this transform, to prepare
             // for reusing additional slices of the old transforms.
-            if excerpt_edits.peek().map_or(true, |next_edit| {
-                next_edit.old.start >= old_diff_transforms.end().0
-            }) {
+            if excerpt_edits
+                .peek()
+                .is_none_or(|next_edit| next_edit.old.start >= old_diff_transforms.end().0)
+            {
                 let keep_next_old_transform = (old_diff_transforms.start().0 >= edit.old.end)
                     && match old_diff_transforms.item() {
                         Some(DiffTransform::BufferContent {
@@ -3223,7 +3230,7 @@ impl MultiBuffer {
 
                 old_expanded_hunks.clear();
                 self.push_buffer_content_transform(
-                    &snapshot,
+                    snapshot,
                     &mut new_diff_transforms,
                     excerpt_offset,
                     end_of_current_insert,
@@ -3431,18 +3438,17 @@ impl MultiBuffer {
             inserted_hunk_info,
             summary,
         }) = subtree.first()
-        {
-            if self.extend_last_buffer_content_transform(
+            && self.extend_last_buffer_content_transform(
                 new_transforms,
                 *inserted_hunk_info,
                 *summary,
-            ) {
-                let mut cursor = subtree.cursor::<()>(&());
-                cursor.next();
-                cursor.next();
-                new_transforms.append(cursor.suffix(), &());
-                return;
-            }
+            )
+        {
+            let mut cursor = subtree.cursor::<()>(&());
+            cursor.next();
+            cursor.next();
+            new_transforms.append(cursor.suffix(), &());
+            return;
         }
         new_transforms.append(subtree, &());
     }
@@ -3456,14 +3462,13 @@ impl MultiBuffer {
             inserted_hunk_info: inserted_hunk_anchor,
             summary,
         } = transform
-        {
-            if self.extend_last_buffer_content_transform(
+            && self.extend_last_buffer_content_transform(
                 new_transforms,
                 inserted_hunk_anchor,
                 summary,
-            ) {
-                return;
-            }
+            )
+        {
+            return;
         }
         new_transforms.push(transform, &());
     }
@@ -3518,11 +3523,10 @@ impl MultiBuffer {
                     summary,
                     inserted_hunk_info: inserted_hunk_anchor,
                 } = last_transform
+                    && *inserted_hunk_anchor == new_inserted_hunk_info
                 {
-                    if *inserted_hunk_anchor == new_inserted_hunk_info {
-                        *summary += summary_to_add;
-                        did_extend = true;
-                    }
+                    *summary += summary_to_add;
+                    did_extend = true;
                 }
             },
             &(),
@@ -3565,9 +3569,7 @@ impl MultiBuffer {
         let multi = cx.new(|_| Self::new(Capability::ReadWrite));
         for (text, ranges) in excerpts {
             let buffer = cx.new(|cx| Buffer::local(text, cx));
-            let excerpt_ranges = ranges
-                .into_iter()
-                .map(|range| ExcerptRange::new(range.clone()));
+            let excerpt_ranges = ranges.into_iter().map(ExcerptRange::new);
             multi.update(cx, |multi, cx| {
                 multi.push_excerpts(buffer, excerpt_ranges, cx)
             });
@@ -3601,7 +3603,7 @@ impl MultiBuffer {
         let mut edits: Vec<(Range<usize>, Arc<str>)> = Vec::new();
         let mut last_end = None;
         for _ in 0..edit_count {
-            if last_end.map_or(false, |last_end| last_end >= snapshot.len()) {
+            if last_end.is_some_and(|last_end| last_end >= snapshot.len()) {
                 break;
             }
 
@@ -3916,8 +3918,8 @@ impl MultiBufferSnapshot {
         &self,
         range: Range<T>,
     ) -> Vec<(&BufferSnapshot, Range<usize>, ExcerptId)> {
-        let start = range.start.to_offset(&self);
-        let end = range.end.to_offset(&self);
+        let start = range.start.to_offset(self);
+        let end = range.end.to_offset(self);
 
         let mut cursor = self.cursor::<usize>();
         cursor.seek(&start);
@@ -3955,8 +3957,8 @@ impl MultiBufferSnapshot {
         &self,
         range: Range<T>,
     ) -> impl Iterator<Item = (&BufferSnapshot, Range<usize>, ExcerptId, Option<Anchor>)> + '_ {
-        let start = range.start.to_offset(&self);
-        let end = range.end.to_offset(&self);
+        let start = range.start.to_offset(self);
+        let end = range.end.to_offset(self);
 
         let mut cursor = self.cursor::<usize>();
         cursor.seek(&start);
@@ -4037,10 +4039,10 @@ impl MultiBufferSnapshot {
 
         cursor.seek(&query_range.start);
 
-        if let Some(region) = cursor.region().filter(|region| !region.is_main_buffer) {
-            if region.range.start > D::zero(&()) {
-                cursor.prev()
-            }
+        if let Some(region) = cursor.region().filter(|region| !region.is_main_buffer)
+            && region.range.start > D::zero(&())
+        {
+            cursor.prev()
         }
 
         iter::from_fn(move || {
@@ -4070,19 +4072,15 @@ impl MultiBufferSnapshot {
                         buffer_start = cursor.main_buffer_position()?;
                     };
                     let mut buffer_end = excerpt.range.context.end.summary::<D>(&excerpt.buffer);
-                    if let Some((end_excerpt_id, end_buffer_offset)) = range_end {
-                        if excerpt.id == end_excerpt_id {
-                            buffer_end = buffer_end.min(end_buffer_offset);
-                        }
-                    }
-
-                    if let Some(iterator) =
-                        get_buffer_metadata(&excerpt.buffer, buffer_start..buffer_end)
+                    if let Some((end_excerpt_id, end_buffer_offset)) = range_end
+                        && excerpt.id == end_excerpt_id
                     {
-                        Some(&mut current_excerpt_metadata.insert((excerpt.id, iterator)).1)
-                    } else {
-                        None
+                        buffer_end = buffer_end.min(end_buffer_offset);
                     }
+
+                    get_buffer_metadata(&excerpt.buffer, buffer_start..buffer_end).map(|iterator| {
+                        &mut current_excerpt_metadata.insert((excerpt.id, iterator)).1
+                    })
                 };
 
                 // Visit each metadata item.
@@ -4144,10 +4142,10 @@ impl MultiBufferSnapshot {
                 // When there are no more metadata items for this excerpt, move to the next excerpt.
                 else {
                     current_excerpt_metadata.take();
-                    if let Some((end_excerpt_id, _)) = range_end {
-                        if excerpt.id == end_excerpt_id {
-                            return None;
-                        }
+                    if let Some((end_excerpt_id, _)) = range_end
+                        && excerpt.id == end_excerpt_id
+                    {
+                        return None;
                     }
                     cursor.next_excerpt();
                 }
@@ -4186,7 +4184,7 @@ impl MultiBufferSnapshot {
                 }
                 let start =
                     Anchor::in_buffer(excerpt.id, excerpt.buffer_id, hunk.buffer_range.start)
-                        .to_point(&self);
+                        .to_point(self);
                 return Some(MultiBufferRow(start.row));
             }
         }
@@ -4204,7 +4202,7 @@ impl MultiBufferSnapshot {
                 continue;
             };
             let start = Anchor::in_buffer(excerpt.id, excerpt.buffer_id, hunk.buffer_range.start)
-                .to_point(&self);
+                .to_point(self);
             return Some(MultiBufferRow(start.row));
         }
     }
@@ -4455,7 +4453,7 @@ impl MultiBufferSnapshot {
             let mut buffer_position = region.buffer_range.start;
             buffer_position.add_assign(&overshoot);
             let clipped_buffer_position =
-                clip_buffer_position(&region.buffer, buffer_position, bias);
+                clip_buffer_position(region.buffer, buffer_position, bias);
             let mut position = region.range.start;
             position.add_assign(&(clipped_buffer_position - region.buffer_range.start));
             position
@@ -4485,7 +4483,7 @@ impl MultiBufferSnapshot {
             let buffer_start_value = region.buffer_range.start.value.unwrap();
             let mut buffer_key = buffer_start_key;
             buffer_key.add_assign(&(key - start_key));
-            let buffer_value = convert_buffer_dimension(&region.buffer, buffer_key);
+            let buffer_value = convert_buffer_dimension(region.buffer, buffer_key);
             let mut result = start_value;
             result.add_assign(&(buffer_value - buffer_start_value));
             result
@@ -4622,20 +4620,20 @@ impl MultiBufferSnapshot {
     pub fn indent_and_comment_for_line(&self, row: MultiBufferRow, cx: &App) -> String {
         let mut indent = self.indent_size_for_line(row).chars().collect::<String>();
 
-        if self.language_settings(cx).extend_comment_on_newline {
-            if let Some(language_scope) = self.language_scope_at(Point::new(row.0, 0)) {
-                let delimiters = language_scope.line_comment_prefixes();
-                for delimiter in delimiters {
-                    if *self
-                        .chars_at(Point::new(row.0, indent.len() as u32))
-                        .take(delimiter.chars().count())
-                        .collect::<String>()
-                        .as_str()
-                        == **delimiter
-                    {
-                        indent.push_str(&delimiter);
-                        break;
-                    }
+        if self.language_settings(cx).extend_comment_on_newline
+            && let Some(language_scope) = self.language_scope_at(Point::new(row.0, 0))
+        {
+            let delimiters = language_scope.line_comment_prefixes();
+            for delimiter in delimiters {
+                if *self
+                    .chars_at(Point::new(row.0, indent.len() as u32))
+                    .take(delimiter.chars().count())
+                    .collect::<String>()
+                    .as_str()
+                    == **delimiter
+                {
+                    indent.push_str(delimiter);
+                    break;
                 }
             }
         }
@@ -4655,7 +4653,7 @@ impl MultiBufferSnapshot {
                 return true;
             }
         }
-        return true;
+        true
     }
 
     pub fn prev_non_blank_row(&self, mut row: MultiBufferRow) -> Option<MultiBufferRow> {
@@ -4893,25 +4891,22 @@ impl MultiBufferSnapshot {
                     base_text_byte_range,
                     ..
                 }) => {
-                    if let Some(diff_base_anchor) = &anchor.diff_base_anchor {
-                        if let Some(base_text) =
+                    if let Some(diff_base_anchor) = &anchor.diff_base_anchor
+                        && let Some(base_text) =
                             self.diffs.get(buffer_id).map(|diff| diff.base_text())
+                        && base_text.can_resolve(diff_base_anchor)
+                    {
+                        let base_text_offset = diff_base_anchor.to_offset(base_text);
+                        if base_text_offset >= base_text_byte_range.start
+                            && base_text_offset <= base_text_byte_range.end
                         {
-                            if base_text.can_resolve(&diff_base_anchor) {
-                                let base_text_offset = diff_base_anchor.to_offset(&base_text);
-                                if base_text_offset >= base_text_byte_range.start
-                                    && base_text_offset <= base_text_byte_range.end
-                                {
-                                    let position_in_hunk = base_text
-                                        .text_summary_for_range::<D, _>(
-                                            base_text_byte_range.start..base_text_offset,
-                                        );
-                                    position.add_assign(&position_in_hunk);
-                                } else if at_transform_end {
-                                    diff_transforms.next();
-                                    continue;
-                                }
-                            }
+                            let position_in_hunk = base_text.text_summary_for_range::<D, _>(
+                                base_text_byte_range.start..base_text_offset,
+                            );
+                            position.add_assign(&position_in_hunk);
+                        } else if at_transform_end {
+                            diff_transforms.next();
+                            continue;
                         }
                     }
                 }
@@ -4941,20 +4936,19 @@ impl MultiBufferSnapshot {
         }
 
         let mut position = cursor.start().1;
-        if let Some(excerpt) = cursor.item() {
-            if excerpt.id == anchor.excerpt_id {
-                let excerpt_buffer_start = excerpt
-                    .buffer
-                    .offset_for_anchor(&excerpt.range.context.start);
-                let excerpt_buffer_end =
-                    excerpt.buffer.offset_for_anchor(&excerpt.range.context.end);
-                let buffer_position = cmp::min(
-                    excerpt_buffer_end,
-                    excerpt.buffer.offset_for_anchor(&anchor.text_anchor),
-                );
-                if buffer_position > excerpt_buffer_start {
-                    position.value += buffer_position - excerpt_buffer_start;
-                }
+        if let Some(excerpt) = cursor.item()
+            && excerpt.id == anchor.excerpt_id
+        {
+            let excerpt_buffer_start = excerpt
+                .buffer
+                .offset_for_anchor(&excerpt.range.context.start);
+            let excerpt_buffer_end = excerpt.buffer.offset_for_anchor(&excerpt.range.context.end);
+            let buffer_position = cmp::min(
+                excerpt_buffer_end,
+                excerpt.buffer.offset_for_anchor(&anchor.text_anchor),
+            );
+            if buffer_position > excerpt_buffer_start {
+                position.value += buffer_position - excerpt_buffer_start;
             }
         }
         position
@@ -4964,7 +4958,7 @@ impl MultiBufferSnapshot {
         while let Some(replacement) = self.replaced_excerpts.get(&excerpt_id) {
             excerpt_id = *replacement;
         }
-        return excerpt_id;
+        excerpt_id
     }
 
     pub fn summaries_for_anchors<'a, D, I>(&'a self, anchors: I) -> Vec<D>
@@ -5082,9 +5076,9 @@ impl MultiBufferSnapshot {
                 if point == region.range.end.key && region.has_trailing_newline {
                     position.add_assign(&D::from_text_summary(&TextSummary::newline()));
                 }
-                return Some(position);
+                Some(position)
             } else {
-                return Some(D::from_text_summary(&self.text_summary()));
+                Some(D::from_text_summary(&self.text_summary()))
             }
         })
     }
@@ -5124,7 +5118,7 @@ impl MultiBufferSnapshot {
                 // Leave min and max anchors unchanged if invalid or
                 // if the old excerpt still exists at this location
                 let mut kept_position = next_excerpt
-                    .map_or(false, |e| e.id == old_excerpt_id && e.contains(&anchor))
+                    .is_some_and(|e| e.id == old_excerpt_id && e.contains(&anchor))
                     || old_excerpt_id == ExcerptId::max()
                     || old_excerpt_id == ExcerptId::min();
 
@@ -5211,15 +5205,12 @@ impl MultiBufferSnapshot {
             .cursor::<Dimensions<usize, ExcerptOffset>>(&());
         diff_transforms.seek(&offset, Bias::Right);
 
-        if offset == diff_transforms.start().0 && bias == Bias::Left {
-            if let Some(prev_item) = diff_transforms.prev_item() {
-                match prev_item {
-                    DiffTransform::DeletedHunk { .. } => {
-                        diff_transforms.prev();
-                    }
-                    _ => {}
-                }
-            }
+        if offset == diff_transforms.start().0
+            && bias == Bias::Left
+            && let Some(prev_item) = diff_transforms.prev_item()
+            && let DiffTransform::DeletedHunk { .. } = prev_item
+        {
+            diff_transforms.prev();
         }
         let offset_in_transform = offset - diff_transforms.start().0;
         let mut excerpt_offset = diff_transforms.start().1;
@@ -5246,15 +5237,6 @@ impl MultiBufferSnapshot {
             excerpt_offset += ExcerptOffset::new(offset_in_transform);
         };
 
-        if let Some((excerpt_id, buffer_id, buffer)) = self.as_singleton() {
-            return Anchor {
-                buffer_id: Some(buffer_id),
-                excerpt_id: *excerpt_id,
-                text_anchor: buffer.anchor_at(excerpt_offset.value, bias),
-                diff_base_anchor,
-            };
-        }
-
         let mut excerpts = self
             .excerpts
             .cursor::<Dimensions<ExcerptOffset, Option<ExcerptId>>>(&());
@@ -5278,10 +5260,17 @@ impl MultiBufferSnapshot {
                 text_anchor,
                 diff_base_anchor,
             }
-        } else if excerpt_offset.is_zero() && bias == Bias::Left {
-            Anchor::min()
         } else {
-            Anchor::max()
+            let mut anchor = if excerpt_offset.is_zero() && bias == Bias::Left {
+                Anchor::min()
+            } else {
+                Anchor::max()
+            };
+            // TODO this is a hack, remove it
+            if let Some((excerpt_id, _, _)) = self.as_singleton() {
+                anchor.excerpt_id = *excerpt_id;
+            }
+            anchor
         }
     }
 
@@ -5296,17 +5285,17 @@ impl MultiBufferSnapshot {
         let locator = self.excerpt_locator_for_id(excerpt_id);
         let mut cursor = self.excerpts.cursor::<Option<&Locator>>(&());
         cursor.seek(locator, Bias::Left);
-        if let Some(excerpt) = cursor.item() {
-            if excerpt.id == excerpt_id {
-                let text_anchor = excerpt.clip_anchor(text_anchor);
-                drop(cursor);
-                return Some(Anchor {
-                    buffer_id: Some(excerpt.buffer_id),
-                    excerpt_id,
-                    text_anchor,
-                    diff_base_anchor: None,
-                });
-            }
+        if let Some(excerpt) = cursor.item()
+            && excerpt.id == excerpt_id
+        {
+            let text_anchor = excerpt.clip_anchor(text_anchor);
+            drop(cursor);
+            return Some(Anchor {
+                buffer_id: Some(excerpt.buffer_id),
+                excerpt_id,
+                text_anchor,
+                diff_base_anchor: None,
+            });
         }
         None
     }
@@ -5491,7 +5480,7 @@ impl MultiBufferSnapshot {
         let range_filter = |open: Range<usize>, close: Range<usize>| -> bool {
             excerpt_buffer_range.contains(&open.start)
                 && excerpt_buffer_range.contains(&close.end)
-                && range_filter.map_or(true, |filter| filter(buffer, open, close))
+                && range_filter.is_none_or(|filter| filter(buffer, open, close))
         };
 
         let (open, close) = excerpt.buffer().innermost_enclosing_bracket_ranges(
@@ -5651,10 +5640,10 @@ impl MultiBufferSnapshot {
                 .buffer
                 .line_indents_in_row_range(buffer_start_row..buffer_end_row);
             cursor.next();
-            return Some(line_indents.map(move |(buffer_row, indent)| {
+            Some(line_indents.map(move |(buffer_row, indent)| {
                 let row = region.range.start.row + (buffer_row - region.buffer_range.start.row);
                 (MultiBufferRow(row), indent, &region.excerpt.buffer)
-            }));
+            }))
         })
         .flatten()
     }
@@ -5691,10 +5680,10 @@ impl MultiBufferSnapshot {
                 .buffer
                 .reversed_line_indents_in_row_range(buffer_start_row..buffer_end_row);
             cursor.prev();
-            return Some(line_indents.map(move |(buffer_row, indent)| {
+            Some(line_indents.map(move |(buffer_row, indent)| {
                 let row = region.range.start.row + (buffer_row - region.buffer_range.start.row);
                 (MultiBufferRow(row), indent, &region.excerpt.buffer)
-            }));
+            }))
         })
         .flatten()
     }
@@ -5860,10 +5849,10 @@ impl MultiBufferSnapshot {
             let current_depth = indent_stack.len() as u32;
 
             // Avoid retrieving the language settings repeatedly for every buffer row.
-            if let Some((prev_buffer_id, _)) = &prev_settings {
-                if prev_buffer_id != &buffer.remote_id() {
-                    prev_settings.take();
-                }
+            if let Some((prev_buffer_id, _)) = &prev_settings
+                && prev_buffer_id != &buffer.remote_id()
+            {
+                prev_settings.take();
             }
             let settings = &prev_settings
                 .get_or_insert_with(|| {
@@ -6192,10 +6181,10 @@ impl MultiBufferSnapshot {
         } else {
             let mut cursor = self.excerpt_ids.cursor::<ExcerptId>(&());
             cursor.seek(&id, Bias::Left);
-            if let Some(entry) = cursor.item() {
-                if entry.id == id {
-                    return &entry.locator;
-                }
+            if let Some(entry) = cursor.item()
+                && entry.id == id
+            {
+                return &entry.locator;
             }
             panic!("invalid excerpt id {id:?}")
         }
@@ -6272,10 +6261,10 @@ impl MultiBufferSnapshot {
     pub fn buffer_range_for_excerpt(&self, excerpt_id: ExcerptId) -> Option<Range<text::Anchor>> {
         let mut cursor = self.excerpts.cursor::<Option<&Locator>>(&());
         let locator = self.excerpt_locator_for_id(excerpt_id);
-        if cursor.seek(&Some(locator), Bias::Left) {
-            if let Some(excerpt) = cursor.item() {
-                return Some(excerpt.range.context.clone());
-            }
+        if cursor.seek(&Some(locator), Bias::Left)
+            && let Some(excerpt) = cursor.item()
+        {
+            return Some(excerpt.range.context.clone());
         }
         None
     }
@@ -6284,10 +6273,10 @@ impl MultiBufferSnapshot {
         let mut cursor = self.excerpts.cursor::<Option<&Locator>>(&());
         let locator = self.excerpt_locator_for_id(excerpt_id);
         cursor.seek(&Some(locator), Bias::Left);
-        if let Some(excerpt) = cursor.item() {
-            if excerpt.id == excerpt_id {
-                return Some(excerpt);
-            }
+        if let Some(excerpt) = cursor.item()
+            && excerpt.id == excerpt_id
+        {
+            return Some(excerpt);
         }
         None
     }
@@ -6323,6 +6312,14 @@ impl MultiBufferSnapshot {
         })
     }
 
+    pub fn buffer_id_for_anchor(&self, anchor: Anchor) -> Option<BufferId> {
+        if let Some(id) = anchor.buffer_id {
+            return Some(id);
+        }
+        let excerpt = self.excerpt_containing(anchor..anchor)?;
+        Some(excerpt.buffer_id())
+    }
+
     pub fn selections_in_range<'a>(
         &'a self,
         range: &'a Range<Anchor>,
@@ -6418,7 +6415,7 @@ impl MultiBufferSnapshot {
 
         for (ix, entry) in excerpt_ids.iter().enumerate() {
             if ix == 0 {
-                if entry.id.cmp(&ExcerptId::min(), &self).is_le() {
+                if entry.id.cmp(&ExcerptId::min(), self).is_le() {
                     panic!("invalid first excerpt id {:?}", entry.id);
                 }
             } else if entry.id <= excerpt_ids[ix - 1].id {

crates/multi_buffer/src/multi_buffer_tests.rs 🔗

@@ -473,7 +473,7 @@ fn test_editing_text_in_diff_hunks(cx: &mut TestAppContext) {
     let base_text = "one\ntwo\nfour\nfive\nsix\nseven\n";
     let text = "one\ntwo\nTHREE\nfour\nfive\nseven\n";
     let buffer = cx.new(|cx| Buffer::local(text, cx));
-    let diff = cx.new(|cx| BufferDiff::new_with_base_text(&base_text, &buffer, cx));
+    let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
     let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
 
     let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| {
@@ -2250,11 +2250,11 @@ impl ReferenceMultibuffer {
             let base_buffer = diff.base_text();
 
             let mut offset = buffer_range.start;
-            let mut hunks = diff
+            let hunks = diff
                 .hunks_intersecting_range(excerpt.range.clone(), buffer, cx)
                 .peekable();
 
-            while let Some(hunk) = hunks.next() {
+            for hunk in hunks {
                 // Ignore hunks that are outside the excerpt range.
                 let mut hunk_range = hunk.buffer_range.to_offset(buffer);
 
@@ -2265,14 +2265,14 @@ impl ReferenceMultibuffer {
                 }
 
                 if !excerpt.expanded_diff_hunks.iter().any(|expanded_anchor| {
-                    expanded_anchor.to_offset(&buffer).max(buffer_range.start)
+                    expanded_anchor.to_offset(buffer).max(buffer_range.start)
                         == hunk_range.start.max(buffer_range.start)
                 }) {
                     log::trace!("skipping a hunk that's not marked as expanded");
                     continue;
                 }
 
-                if !hunk.buffer_range.start.is_valid(&buffer) {
+                if !hunk.buffer_range.start.is_valid(buffer) {
                     log::trace!("skipping hunk with deleted start: {:?}", hunk.range);
                     continue;
                 }
@@ -2449,7 +2449,7 @@ impl ReferenceMultibuffer {
                     return false;
                 }
                 while let Some(hunk) = hunks.peek() {
-                    match hunk.buffer_range.start.cmp(&hunk_anchor, &buffer) {
+                    match hunk.buffer_range.start.cmp(hunk_anchor, &buffer) {
                         cmp::Ordering::Less => {
                             hunks.next();
                         }
@@ -2519,8 +2519,8 @@ async fn test_random_set_ranges(cx: &mut TestAppContext, mut rng: StdRng) {
         let mut seen_ranges = Vec::default();
 
         for (_, buf, range) in snapshot.excerpts() {
-            let start = range.context.start.to_point(&buf);
-            let end = range.context.end.to_point(&buf);
+            let start = range.context.start.to_point(buf);
+            let end = range.context.end.to_point(buf);
             seen_ranges.push(start..end);
 
             if let Some(last_end) = last_end.take() {
@@ -2739,9 +2739,8 @@ async fn test_random_multibuffer(cx: &mut TestAppContext, mut rng: StdRng) {
                     let id = buffer_handle.read(cx).remote_id();
                     if multibuffer.diff_for(id).is_none() {
                         let base_text = base_texts.get(&id).unwrap();
-                        let diff = cx.new(|cx| {
-                            BufferDiff::new_with_base_text(base_text, &buffer_handle, cx)
-                        });
+                        let diff = cx
+                            .new(|cx| BufferDiff::new_with_base_text(base_text, buffer_handle, cx));
                         reference.add_diff(diff.clone(), cx);
                         multibuffer.add_diff(diff, cx)
                     }
@@ -3593,24 +3592,20 @@ fn assert_position_translation(snapshot: &MultiBufferSnapshot) {
 
     for (anchors, bias) in [(&left_anchors, Bias::Left), (&right_anchors, Bias::Right)] {
         for (ix, (offset, anchor)) in offsets.iter().zip(anchors).enumerate() {
-            if ix > 0 {
-                if *offset == 252 {
-                    if offset > &offsets[ix - 1] {
-                        let prev_anchor = left_anchors[ix - 1];
-                        assert!(
-                            anchor.cmp(&prev_anchor, snapshot).is_gt(),
-                            "anchor({}, {bias:?}).cmp(&anchor({}, {bias:?}).is_gt()",
-                            offsets[ix],
-                            offsets[ix - 1],
-                        );
-                        assert!(
-                            prev_anchor.cmp(&anchor, snapshot).is_lt(),
-                            "anchor({}, {bias:?}).cmp(&anchor({}, {bias:?}).is_lt()",
-                            offsets[ix - 1],
-                            offsets[ix],
-                        );
-                    }
-                }
+            if ix > 0 && *offset == 252 && offset > &offsets[ix - 1] {
+                let prev_anchor = left_anchors[ix - 1];
+                assert!(
+                    anchor.cmp(&prev_anchor, snapshot).is_gt(),
+                    "anchor({}, {bias:?}).cmp(&anchor({}, {bias:?}).is_gt()",
+                    offsets[ix],
+                    offsets[ix - 1],
+                );
+                assert!(
+                    prev_anchor.cmp(anchor, snapshot).is_lt(),
+                    "anchor({}, {bias:?}).cmp(&anchor({}, {bias:?}).is_lt()",
+                    offsets[ix - 1],
+                    offsets[ix],
+                );
             }
         }
     }

crates/multi_buffer/src/position.rs 🔗

@@ -126,17 +126,17 @@ impl<T> Default for TypedRow<T> {
 
 impl<T> PartialOrd for TypedOffset<T> {
     fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
-        Some(self.cmp(&other))
+        Some(self.cmp(other))
     }
 }
 impl<T> PartialOrd for TypedPoint<T> {
     fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
-        Some(self.cmp(&other))
+        Some(self.cmp(other))
     }
 }
 impl<T> PartialOrd for TypedRow<T> {
     fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
-        Some(self.cmp(&other))
+        Some(self.cmp(other))
     }
 }
 

crates/node_runtime/src/node_runtime.rs 🔗

@@ -29,6 +29,13 @@ pub struct NodeBinaryOptions {
     pub use_paths: Option<(PathBuf, PathBuf)>,
 }
 
+pub enum VersionStrategy<'a> {
+    /// Install if current version doesn't match pinned version
+    Pin(&'a str),
+    /// Install if current version is older than latest version
+    Latest(&'a str),
+}
+
 #[derive(Clone)]
 pub struct NodeRuntime(Arc<Mutex<NodeRuntimeState>>);
 
@@ -69,9 +76,8 @@ impl NodeRuntime {
         let mut state = self.0.lock().await;
 
         let options = loop {
-            match state.options.borrow().as_ref() {
-                Some(options) => break options.clone(),
-                None => {}
+            if let Some(options) = state.options.borrow().as_ref() {
+                break options.clone();
             }
             match state.options.changed().await {
                 Ok(()) => {}
@@ -190,7 +196,7 @@ impl NodeRuntime {
 
         state.instance = Some(instance.boxed_clone());
         state.last_options = Some(options);
-        return instance;
+        instance
     }
 
     pub async fn binary_path(&self) -> Result<PathBuf> {
@@ -286,7 +292,7 @@ impl NodeRuntime {
         package_name: &str,
         local_executable_path: &Path,
         local_package_directory: &Path,
-        latest_version: &str,
+        version_strategy: VersionStrategy<'_>,
     ) -> bool {
         // In the case of the local system not having the package installed,
         // or in the instances where we fail to parse package.json data,
@@ -307,11 +313,21 @@ impl NodeRuntime {
         let Some(installed_version) = Version::parse(&installed_version).log_err() else {
             return true;
         };
-        let Some(latest_version) = Version::parse(latest_version).log_err() else {
-            return true;
-        };
 
-        installed_version < latest_version
+        match version_strategy {
+            VersionStrategy::Pin(pinned_version) => {
+                let Some(pinned_version) = Version::parse(pinned_version).log_err() else {
+                    return true;
+                };
+                installed_version != pinned_version
+            }
+            VersionStrategy::Latest(latest_version) => {
+                let Some(latest_version) = Version::parse(latest_version).log_err() else {
+                    return true;
+                };
+                installed_version < latest_version
+            }
+        }
     }
 }
 

crates/notifications/src/notification_store.rs 🔗

@@ -138,10 +138,10 @@ impl NotificationStore {
     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() {
-            if item.id == id {
-                return Some(item);
-            }
+        if let Some(item) = cursor.item()
+            && item.id == id
+        {
+            return Some(item);
         }
         None
     }
@@ -229,25 +229,24 @@ impl NotificationStore {
         mut cx: AsyncApp,
     ) -> Result<()> {
         this.update(&mut cx, |this, cx| {
-            if let Some(notification) = envelope.payload.notification {
-                if let Some(rpc::Notification::ChannelMessageMention { message_id, .. }) =
+            if let Some(notification) = envelope.payload.notification
+                && let Some(rpc::Notification::ChannelMessageMention { message_id, .. }) =
                     Notification::from_proto(&notification)
-                {
-                    let fetch_message_task = this.channel_store.update(cx, |this, cx| {
-                        this.fetch_channel_messages(vec![message_id], cx)
-                    });
-
-                    cx.spawn(async move |this, cx| {
-                        let messages = fetch_message_task.await?;
-                        this.update(cx, move |this, cx| {
-                            for message in messages {
-                                this.channel_messages.insert(message_id, message);
-                            }
-                            cx.notify();
-                        })
+            {
+                let fetch_message_task = this.channel_store.update(cx, |this, cx| {
+                    this.fetch_channel_messages(vec![message_id], cx)
+                });
+
+                cx.spawn(async move |this, cx| {
+                    let messages = fetch_message_task.await?;
+                    this.update(cx, move |this, cx| {
+                        for message in messages {
+                            this.channel_messages.insert(message_id, message);
+                        }
+                        cx.notify();
                     })
-                    .detach_and_log_err(cx)
-                }
+                })
+                .detach_and_log_err(cx)
             }
             Ok(())
         })?
@@ -390,12 +389,12 @@ impl NotificationStore {
                         });
                     }
                 }
-            } else if let Some(new_notification) = &new_notification {
-                if is_new {
-                    cx.emit(NotificationEvent::NewNotification {
-                        entry: new_notification.clone(),
-                    });
-                }
+            } else if let Some(new_notification) = &new_notification
+                && is_new
+            {
+                cx.emit(NotificationEvent::NewNotification {
+                    entry: new_notification.clone(),
+                });
             }
 
             if let Some(notification) = new_notification {

crates/notifications/src/status_toast.rs 🔗

@@ -205,7 +205,7 @@ impl Component for StatusToast {
 
         let pr_example =
             StatusToast::new("`zed/new-notification-system` created!", cx, |this, _cx| {
-                this.icon(ToastIcon::new(IconName::GitBranchSmall).color(Color::Muted))
+                this.icon(ToastIcon::new(IconName::GitBranchAlt).color(Color::Muted))
                     .action("Open Pull Request", |_, cx| {
                         cx.open_url("https://github.com/")
                     })

crates/onboarding/Cargo.toml 🔗

@@ -18,14 +18,13 @@ default = []
 ai_onboarding.workspace = true
 anyhow.workspace = true
 client.workspace = true
-command_palette_hooks.workspace = true
 component.workspace = true
 db.workspace = true
 documented.workspace = true
 editor.workspace = true
-feature_flags.workspace = true
 fs.workspace = true
 fuzzy.workspace = true
+git.workspace = true
 gpui.workspace = true
 itertools.workspace = true
 language.workspace = true
@@ -37,6 +36,7 @@ project.workspace = true
 schemars.workspace = true
 serde.workspace = true
 settings.workspace = true
+telemetry.workspace = true
 theme.workspace = true
 ui.workspace = true
 util.workspace = true

crates/onboarding/src/ai_setup_page.rs 🔗

@@ -1,7 +1,7 @@
 use std::sync::Arc;
 
 use ai_onboarding::AiUpsellCard;
-use client::{Client, UserStore};
+use client::{Client, UserStore, zed_urls};
 use fs::Fs;
 use gpui::{
     Action, AnyView, App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, WeakEntity,
@@ -19,7 +19,7 @@ use util::ResultExt;
 use workspace::{ModalView, Workspace};
 use zed_actions::agent::OpenSettings;
 
-const FEATURED_PROVIDERS: [&'static str; 4] = ["anthropic", "google", "openai", "ollama"];
+const FEATURED_PROVIDERS: [&str; 4] = ["anthropic", "google", "openai", "ollama"];
 
 fn render_llm_provider_section(
     tab_index: &mut isize,
@@ -42,10 +42,16 @@ fn render_llm_provider_section(
 }
 
 fn render_privacy_card(tab_index: &mut isize, disabled: bool, cx: &mut App) -> impl IntoElement {
-    let privacy_badge = || {
-        Badge::new("Privacy")
-            .icon(IconName::ShieldCheck)
-            .tooltip(move |_, cx| cx.new(|_| AiPrivacyTooltip::new()).into())
+    let (title, description) = if disabled {
+        (
+            "AI is disabled across Zed",
+            "Re-enable it any time in Settings.",
+        )
+    } else {
+        (
+            "Privacy is the default for Zed",
+            "Any use or storage of your data is with your explicit, single-use, opt-in consent.",
+        )
     };
 
     v_flex()
@@ -60,62 +66,41 @@ fn render_privacy_card(tab_index: &mut isize, disabled: bool, cx: &mut App) -> i
         .bg(cx.theme().colors().surface_background.opacity(0.3))
         .rounded_lg()
         .overflow_hidden()
-        .map(|this| {
-            if disabled {
-                this.child(
+        .child(
+            h_flex()
+                .gap_2()
+                .justify_between()
+                .child(Label::new(title))
+                .child(
                     h_flex()
-                        .gap_2()
-                        .justify_between()
+                        .gap_1()
                         .child(
-                            h_flex()
-                                .gap_1()
-                                .child(Label::new("AI is disabled across Zed"))
-                                .child(
-                                    Icon::new(IconName::Check)
-                                        .color(Color::Success)
-                                        .size(IconSize::XSmall),
-                                ),
+                            Badge::new("Privacy")
+                                .icon(IconName::ShieldCheck)
+                                .tooltip(move |_, cx| cx.new(|_| AiPrivacyTooltip::new()).into()),
                         )
-                        .child(privacy_badge()),
-                )
-                .child(
-                    Label::new("Re-enable it any time in Settings.")
-                        .size(LabelSize::Small)
-                        .color(Color::Muted),
-                )
-            } else {
-                this.child(
-                    h_flex()
-                        .gap_2()
-                        .justify_between()
-                        .child(Label::new("Privacy is the default for Zed"))
                         .child(
-                            h_flex().gap_1().child(privacy_badge()).child(
-                                Button::new("learn_more", "Learn More")
-                                    .style(ButtonStyle::Outlined)
-                                    .label_size(LabelSize::Small)
-                                    .icon(IconName::ArrowUpRight)
-                                    .icon_size(IconSize::XSmall)
-                                    .icon_color(Color::Muted)
-                                    .on_click(|_, _, cx| {
-                                        cx.open_url("https://zed.dev/docs/ai/privacy-and-security");
-                                    })
-                                    .tab_index({
-                                        *tab_index += 1;
-                                        *tab_index - 1
-                                    }),
-                            ),
+                            Button::new("learn_more", "Learn More")
+                                .style(ButtonStyle::Outlined)
+                                .label_size(LabelSize::Small)
+                                .icon(IconName::ArrowUpRight)
+                                .icon_size(IconSize::XSmall)
+                                .icon_color(Color::Muted)
+                                .on_click(|_, _, cx| {
+                                    cx.open_url(&zed_urls::ai_privacy_and_security(cx))
+                                })
+                                .tab_index({
+                                    *tab_index += 1;
+                                    *tab_index - 1
+                                }),
                         ),
-                )
-                .child(
-                    Label::new(
-                        "Any use or storage of your data is with your explicit, single-use, opt-in consent.",
-                    )
-                    .size(LabelSize::Small)
-                    .color(Color::Muted),
-                )
-            }
-        })
+                ),
+        )
+        .child(
+            Label::new(description)
+                .size(LabelSize::Small)
+                .color(Color::Muted),
+        )
 }
 
 fn render_llm_provider_card(
@@ -203,6 +188,11 @@ fn render_llm_provider_card(
                                 workspace
                                     .update(cx, |workspace, cx| {
                                         workspace.toggle_modal(window, cx, |window, cx| {
+                                            telemetry::event!(
+                                                "Welcome AI Modal Opened",
+                                                provider = provider.name().0,
+                                            );
+
                                             let modal = AiConfigurationModal::new(
                                                 provider.clone(),
                                                 window,
@@ -260,16 +250,25 @@ pub(crate) fn render_ai_setup_page(
                     ToggleState::Selected
                 },
                 |&toggle_state, _, cx| {
+                    let enabled = match toggle_state {
+                        ToggleState::Indeterminate => {
+                            return;
+                        }
+                        ToggleState::Unselected => true,
+                        ToggleState::Selected => false,
+                    };
+
+                    telemetry::event!(
+                        "Welcome AI Enabled",
+                        toggle = if enabled { "on" } else { "off" },
+                    );
+
                     let fs = <dyn Fs>::global(cx);
                     update_settings_file::<DisableAiSettings>(
                         fs,
                         cx,
                         move |ai_settings: &mut Option<bool>, _| {
-                            *ai_settings = match toggle_state {
-                                ToggleState::Indeterminate => None,
-                                ToggleState::Unselected => Some(true),
-                                ToggleState::Selected => Some(false),
-                            };
+                            *ai_settings = Some(enabled);
                         },
                     );
                 },
@@ -330,7 +329,11 @@ impl AiConfigurationModal {
         cx: &mut Context<Self>,
     ) -> Self {
         let focus_handle = cx.focus_handle();
-        let configuration_view = selected_provider.configuration_view(window, cx);
+        let configuration_view = selected_provider.configuration_view(
+            language_model::ConfigurationViewTargetAgent::ZedAgent,
+            window,
+            cx,
+        );
 
         Self {
             focus_handle,
@@ -407,7 +410,7 @@ impl AiPrivacyTooltip {
 
 impl Render for AiPrivacyTooltip {
     fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
-        const DESCRIPTION: &'static str = "We believe in opt-in data sharing as the default for building AI products, rather than opt-out. We'll only use or store your data if you affirmatively send it to us. ";
+        const DESCRIPTION: &str = "We believe in opt-in data sharing as the default for building AI products, rather than opt-out. We'll only use or store your data if you affirmatively send it to us. ";
 
         tooltip_container(window, cx, move |this, _, _| {
             this.child(

crates/welcome/src/base_keymap_picker.rs → crates/onboarding/src/base_keymap_picker.rs 🔗

@@ -12,7 +12,7 @@ use util::ResultExt;
 use workspace::{ModalView, Workspace, ui::HighlightedLabel};
 
 actions!(
-    welcome,
+    zed,
     [
         /// Toggles the base keymap selector modal.
         ToggleBaseKeymapSelector

crates/onboarding/src/basics_page.rs 🔗

@@ -16,6 +16,23 @@ use vim_mode_setting::VimModeSetting;
 
 use crate::theme_preview::{ThemePreviewStyle, ThemePreviewTile};
 
+const LIGHT_THEMES: [&str; 3] = ["One Light", "Ayu Light", "Gruvbox Light"];
+const DARK_THEMES: [&str; 3] = ["One Dark", "Ayu Dark", "Gruvbox Dark"];
+const FAMILY_NAMES: [SharedString; 3] = [
+    SharedString::new_static("One"),
+    SharedString::new_static("Ayu"),
+    SharedString::new_static("Gruvbox"),
+];
+
+fn get_theme_family_themes(theme_name: &str) -> Option<(&'static str, &'static str)> {
+    for i in 0..LIGHT_THEMES.len() {
+        if LIGHT_THEMES[i] == theme_name || DARK_THEMES[i] == theme_name {
+            return Some((LIGHT_THEMES[i], DARK_THEMES[i]));
+        }
+    }
+    None
+}
+
 fn render_theme_section(tab_index: &mut isize, cx: &mut App) -> impl IntoElement {
     let theme_selection = ThemeSettings::get_global(cx).theme_selection.clone();
     let system_appearance = theme::SystemAppearance::global(cx);
@@ -58,7 +75,7 @@ fn render_theme_section(tab_index: &mut isize, cx: &mut App) -> impl IntoElement
                 .tab_index(tab_index)
                 .selected_index(theme_mode as usize)
                 .style(ui::ToggleButtonGroupStyle::Outlined)
-                .button_width(rems_from_px(64.)),
+                .width(rems_from_px(3. * 64.)),
             ),
         )
         .child(
@@ -90,14 +107,6 @@ fn render_theme_section(tab_index: &mut isize, cx: &mut App) -> impl IntoElement
         };
         let current_theme_name = theme_selection.theme(appearance);
 
-        const LIGHT_THEMES: [&'static str; 3] = ["One Light", "Ayu Light", "Gruvbox Light"];
-        const DARK_THEMES: [&'static str; 3] = ["One Dark", "Ayu Dark", "Gruvbox Dark"];
-        const FAMILY_NAMES: [SharedString; 3] = [
-            SharedString::new_static("One"),
-            SharedString::new_static("Ayu"),
-            SharedString::new_static("Gruvbox"),
-        ];
-
         let theme_names = match appearance {
             Appearance::Light => LIGHT_THEMES,
             Appearance::Dark => DARK_THEMES,
@@ -105,7 +114,7 @@ fn render_theme_section(tab_index: &mut isize, cx: &mut App) -> impl IntoElement
 
         let themes = theme_names.map(|theme| theme_registry.get(theme).unwrap());
 
-        let theme_previews = [0, 1, 2].map(|index| {
+        [0, 1, 2].map(|index| {
             let theme = &themes[index];
             let is_selected = theme.name == current_theme_name;
             let name = theme.name.clone();
@@ -117,7 +126,7 @@ fn render_theme_section(tab_index: &mut isize, cx: &mut App) -> impl IntoElement
                 .gap_1()
                 .child(
                     h_flex()
-                        .id(name.clone())
+                        .id(name)
                         .relative()
                         .w_full()
                         .border_2()
@@ -167,9 +176,7 @@ fn render_theme_section(tab_index: &mut isize, cx: &mut App) -> impl IntoElement
                         .color(Color::Muted)
                         .size(LabelSize::Small),
                 )
-        });
-
-        theme_previews
+        })
     }
 
     fn write_mode_change(mode: ThemeMode, cx: &mut App) {
@@ -184,14 +191,17 @@ fn render_theme_section(tab_index: &mut isize, cx: &mut App) -> impl IntoElement
         let theme = theme.into();
         update_settings_file::<ThemeSettings>(fs, cx, move |settings, cx| {
             if theme_mode == ThemeMode::System {
+                let (light_theme, dark_theme) =
+                    get_theme_family_themes(&theme).unwrap_or((theme.as_ref(), theme.as_ref()));
+
                 settings.theme = Some(ThemeSelection::Dynamic {
                     mode: ThemeMode::System,
-                    light: ThemeName(theme.clone()),
-                    dark: ThemeName(theme.clone()),
+                    light: ThemeName(light_theme.into()),
+                    dark: ThemeName(dark_theme.into()),
                 });
             } else {
                 let appearance = *SystemAppearance::global(cx);
-                settings.set_theme(theme.clone(), appearance);
+                settings.set_theme(theme, appearance);
             }
         });
     }
@@ -305,8 +315,8 @@ fn render_base_keymap_section(tab_index: &mut isize, cx: &mut App) -> impl IntoE
         .when_some(base_keymap, |this, base_keymap| {
             this.selected_index(base_keymap)
         })
+        .full_width()
         .tab_index(tab_index)
-        .button_width(rems_from_px(216.))
         .size(ui::ToggleButtonGroupSize::Medium)
         .style(ui::ToggleButtonGroupStyle::Outlined),
     );

crates/onboarding/src/editing_page.rs 🔗

@@ -35,6 +35,11 @@ fn write_show_mini_map(show: ShowMinimap, cx: &mut App) {
     EditorSettings::override_global(curr_settings, cx);
 
     update_settings_file::<EditorSettings>(fs, cx, move |editor_settings, _| {
+        telemetry::event!(
+            "Welcome Minimap Clicked",
+            from = editor_settings.minimap.unwrap_or_default(),
+            to = show
+        );
         editor_settings.minimap.get_or_insert_default().show = Some(show);
     });
 }
@@ -71,7 +76,7 @@ fn read_git_blame(cx: &App) -> bool {
     ProjectSettings::get_global(cx).git.inline_blame_enabled()
 }
 
-fn set_git_blame(enabled: bool, cx: &mut App) {
+fn write_git_blame(enabled: bool, cx: &mut App) {
     let fs = <dyn Fs>::global(cx);
 
     let mut curr_settings = ProjectSettings::get_global(cx).clone();
@@ -95,6 +100,12 @@ fn write_ui_font_family(font: SharedString, cx: &mut App) {
     let fs = <dyn Fs>::global(cx);
 
     update_settings_file::<ThemeSettings>(fs, cx, move |theme_settings, _| {
+        telemetry::event!(
+            "Welcome Font Changed",
+            type = "ui font",
+            old = theme_settings.ui_font_family,
+            new = font
+        );
         theme_settings.ui_font_family = Some(FontFamilyName(font.into()));
     });
 }
@@ -119,6 +130,13 @@ fn write_buffer_font_family(font_family: SharedString, cx: &mut App) {
     let fs = <dyn Fs>::global(cx);
 
     update_settings_file::<ThemeSettings>(fs, cx, move |theme_settings, _| {
+        telemetry::event!(
+            "Welcome Font Changed",
+            type = "editor font",
+            old = theme_settings.buffer_font_family,
+            new = font_family
+        );
+
         theme_settings.buffer_font_family = Some(FontFamilyName(font_family.into()));
     });
 }
@@ -197,7 +215,7 @@ fn render_setting_import_button(
                                     .color(Color::Muted)
                                     .size(IconSize::XSmall),
                             )
-                            .child(Label::new(label)),
+                            .child(Label::new(label.clone())),
                     )
                     .when(imported, |this| {
                         this.child(
@@ -212,7 +230,10 @@ fn render_setting_import_button(
                         )
                     }),
             )
-            .on_click(move |_, window, cx| window.dispatch_action(action.boxed_clone(), cx)),
+            .on_click(move |_, window, cx| {
+                telemetry::event!("Welcome Import Settings", import_source = label,);
+                window.dispatch_action(action.boxed_clone(), cx);
+            }),
     )
 }
 
@@ -293,7 +314,7 @@ fn render_font_customization_section(
                         .child(
                             PopoverMenu::new("ui-font-picker")
                                 .menu({
-                                    let ui_font_picker = ui_font_picker.clone();
+                                    let ui_font_picker = ui_font_picker;
                                     move |_window, _cx| Some(ui_font_picker.clone())
                                 })
                                 .trigger(
@@ -357,7 +378,7 @@ fn render_font_customization_section(
                         .child(
                             PopoverMenu::new("buffer-font-picker")
                                 .menu({
-                                    let buffer_font_picker = buffer_font_picker.clone();
+                                    let buffer_font_picker = buffer_font_picker;
                                     move |_window, _cx| Some(buffer_font_picker.clone())
                                 })
                                 .trigger(
@@ -573,7 +594,7 @@ fn font_picker(
 ) -> FontPicker {
     let delegate = FontPickerDelegate::new(current_font, on_font_changed, cx);
 
-    Picker::list(delegate, window, cx)
+    Picker::uniform_list(delegate, window, cx)
         .show_scrollbar(true)
         .width(rems_from_px(210.))
         .max_height(Some(rems(20.).into()))
@@ -584,7 +605,7 @@ fn render_popular_settings_section(
     window: &mut Window,
     cx: &mut App,
 ) -> impl IntoElement {
-    const LIGATURE_TOOLTIP: &'static str =
+    const LIGATURE_TOOLTIP: &str =
         "Font ligatures combine two characters into one. For example, turning =/= into ≠.";
 
     v_flex()
@@ -605,7 +626,13 @@ fn render_popular_settings_section(
                     ui::ToggleState::Unselected
                 },
                 |toggle_state, _, cx| {
-                    write_font_ligatures(toggle_state == &ToggleState::Selected, cx);
+                    let enabled = toggle_state == &ToggleState::Selected;
+                    telemetry::event!(
+                        "Welcome Font Ligature",
+                        options = if enabled { "on" } else { "off" },
+                    );
+
+                    write_font_ligatures(enabled, cx);
                 },
             )
             .tab_index({
@@ -625,7 +652,13 @@ fn render_popular_settings_section(
                     ui::ToggleState::Unselected
                 },
                 |toggle_state, _, cx| {
-                    write_format_on_save(toggle_state == &ToggleState::Selected, cx);
+                    let enabled = toggle_state == &ToggleState::Selected;
+                    telemetry::event!(
+                        "Welcome Format On Save Changed",
+                        options = if enabled { "on" } else { "off" },
+                    );
+
+                    write_format_on_save(enabled, cx);
                 },
             )
             .tab_index({
@@ -644,7 +677,13 @@ fn render_popular_settings_section(
                     ui::ToggleState::Unselected
                 },
                 |toggle_state, _, cx| {
-                    write_inlay_hints(toggle_state == &ToggleState::Selected, cx);
+                    let enabled = toggle_state == &ToggleState::Selected;
+                    telemetry::event!(
+                        "Welcome Inlay Hints Changed",
+                        options = if enabled { "on" } else { "off" },
+                    );
+
+                    write_inlay_hints(enabled, cx);
                 },
             )
             .tab_index({
@@ -655,7 +694,7 @@ fn render_popular_settings_section(
         .child(
             SwitchField::new(
                 "onboarding-git-blame-switch",
-                "Git Blame",
+                "Inline Git Blame",
                 Some("See who committed each line on a given file.".into()),
                 if read_git_blame(cx) {
                     ui::ToggleState::Selected
@@ -663,7 +702,13 @@ fn render_popular_settings_section(
                     ui::ToggleState::Unselected
                 },
                 |toggle_state, _, cx| {
-                    set_git_blame(toggle_state == &ToggleState::Selected, cx);
+                    let enabled = toggle_state == &ToggleState::Selected;
+                    telemetry::event!(
+                        "Welcome Git Blame Changed",
+                        options = if enabled { "on" } else { "off" },
+                    );
+
+                    write_git_blame(enabled, cx);
                 },
             )
             .tab_index({
@@ -676,7 +721,7 @@ fn render_popular_settings_section(
                 .items_start()
                 .justify_between()
                 .child(
-                    v_flex().child(Label::new("Mini Map")).child(
+                    v_flex().child(Label::new("Minimap")).child(
                         Label::new("See a high-level overview of your source code.")
                             .color(Color::Muted),
                     ),
@@ -706,7 +751,7 @@ fn render_popular_settings_section(
                     })
                     .tab_index(tab_index)
                     .style(ToggleButtonGroupStyle::Outlined)
-                    .button_width(ui::rems_from_px(64.)),
+                    .width(ui::rems_from_px(3. * 64.)),
                 ),
         )
 }

crates/welcome/src/multibuffer_hint.rs → crates/onboarding/src/multibuffer_hint.rs 🔗

@@ -159,7 +159,7 @@ impl Render for MultibufferHint {
                     .child(
                         Button::new("open_docs", "Learn More")
                             .icon(IconName::ArrowUpRight)
-                            .icon_size(IconSize::XSmall)
+                            .icon_size(IconSize::Small)
                             .icon_color(Color::Muted)
                             .icon_position(IconPosition::End)
                             .on_click(move |_event, _, cx| {

crates/onboarding/src/onboarding.rs 🔗

@@ -1,8 +1,7 @@
-use crate::welcome::{ShowWelcome, WelcomePage};
-use client::{Client, UserStore};
-use command_palette_hooks::CommandPaletteFilter;
+pub use crate::welcome::ShowWelcome;
+use crate::{multibuffer_hint::MultibufferHint, welcome::WelcomePage};
+use client::{Client, UserStore, zed_urls};
 use db::kvp::KEY_VALUE_STORE;
-use feature_flags::{FeatureFlag, FeatureFlagViewExt as _};
 use fs::Fs;
 use gpui::{
     Action, AnyElement, App, AppContext, AsyncWindowContext, Context, Entity, EventEmitter,
@@ -27,17 +26,13 @@ use workspace::{
 };
 
 mod ai_setup_page;
+mod base_keymap_picker;
 mod basics_page;
 mod editing_page;
+pub mod multibuffer_hint;
 mod theme_preview;
 mod welcome;
 
-pub struct OnBoardingFeatureFlag {}
-
-impl FeatureFlag for OnBoardingFeatureFlag {
-    const NAME: &'static str = "onboarding";
-}
-
 /// Imports settings from Visual Studio Code.
 #[derive(Copy, Clone, Debug, Default, PartialEq, Deserialize, JsonSchema, Action)]
 #[action(namespace = zed)]
@@ -57,6 +52,7 @@ pub struct ImportCursorSettings {
 }
 
 pub const FIRST_OPEN: &str = "first_open";
+pub const DOCS_URL: &str = "https://zed.dev/docs/";
 
 actions!(
     zed,
@@ -78,11 +74,21 @@ actions!(
         /// Finish the onboarding process.
         Finish,
         /// Sign in while in the onboarding flow.
-        SignIn
+        SignIn,
+        /// Open the user account in zed.dev while in the onboarding flow.
+        OpenAccount,
+        /// Resets the welcome screen hints to their initial state.
+        ResetHints
     ]
 );
 
 pub fn init(cx: &mut App) {
+    cx.observe_new(|workspace: &mut Workspace, _, _cx| {
+        workspace
+            .register_action(|_workspace, _: &ResetHints, _, cx| MultibufferHint::set_count(0, cx));
+    })
+    .detach();
+
     cx.on_action(|_: &OpenOnboarding, cx| {
         with_active_or_new_workspace(cx, |workspace, window, cx| {
             workspace
@@ -180,38 +186,14 @@ pub fn init(cx: &mut App) {
     })
     .detach();
 
-    cx.observe_new::<Workspace>(|_, window, cx| {
-        let Some(window) = window else {
-            return;
-        };
-
-        let onboarding_actions = [
-            std::any::TypeId::of::<OpenOnboarding>(),
-            std::any::TypeId::of::<ShowWelcome>(),
-        ];
-
-        CommandPaletteFilter::update_global(cx, |filter, _cx| {
-            filter.hide_action_types(&onboarding_actions);
-        });
+    base_keymap_picker::init(cx);
 
-        cx.observe_flag::<OnBoardingFeatureFlag, _>(window, move |is_enabled, _, _, cx| {
-            if is_enabled {
-                CommandPaletteFilter::update_global(cx, |filter, _cx| {
-                    filter.show_action_types(onboarding_actions.iter());
-                });
-            } else {
-                CommandPaletteFilter::update_global(cx, |filter, _cx| {
-                    filter.hide_action_types(&onboarding_actions);
-                });
-            }
-        })
-        .detach();
-    })
-    .detach();
     register_serializable_item::<Onboarding>(cx);
+    register_serializable_item::<WelcomePage>(cx);
 }
 
 pub fn show_onboarding_view(app_state: Arc<AppState>, cx: &mut App) -> Task<anyhow::Result<()>> {
+    telemetry::event!("Onboarding Page Opened");
     open_new(
         Default::default(),
         app_state,
@@ -240,6 +222,16 @@ enum SelectedPage {
     AiSetup,
 }
 
+impl SelectedPage {
+    fn name(&self) -> &'static str {
+        match self {
+            SelectedPage::Basics => "Basics",
+            SelectedPage::Editing => "Editing",
+            SelectedPage::AiSetup => "AI Setup",
+        }
+    }
+}
+
 struct Onboarding {
     workspace: WeakEntity<Workspace>,
     focus_handle: FocusHandle,
@@ -259,7 +251,21 @@ impl Onboarding {
         })
     }
 
-    fn set_page(&mut self, page: SelectedPage, cx: &mut Context<Self>) {
+    fn set_page(
+        &mut self,
+        page: SelectedPage,
+        clicked: Option<&'static str>,
+        cx: &mut Context<Self>,
+    ) {
+        if let Some(click) = clicked {
+            telemetry::event!(
+                "Welcome Tab Clicked",
+                from = self.selected_page.name(),
+                to = page.name(),
+                clicked = click,
+            );
+        }
+
         self.selected_page = page;
         cx.notify();
         cx.emit(ItemEvent::UpdateTab);
@@ -323,8 +329,13 @@ impl Onboarding {
                     gpui::Empty.into_any_element(),
                     IntoElement::into_any_element,
                 ))
-                .on_click(cx.listener(move |this, _, _, cx| {
-                    this.set_page(page, cx);
+                .on_click(cx.listener(move |this, click_event, _, cx| {
+                    let click = match click_event {
+                        gpui::ClickEvent::Mouse(_) => "mouse",
+                        gpui::ClickEvent::Keyboard(_) => "keyboard",
+                    };
+
+                    this.set_page(page, Some(click), cx);
                 }))
         })
     }
@@ -420,11 +431,40 @@ impl Onboarding {
             )
             .child(
                 if let Some(user) = self.user_store.read(cx).current_user() {
-                    h_flex()
-                        .pl_1p5()
-                        .gap_2()
-                        .child(Avatar::new(user.avatar_uri.clone()))
-                        .child(Label::new(user.github_login.clone()))
+                    v_flex()
+                        .gap_1()
+                        .child(
+                            h_flex()
+                                .ml_2()
+                                .gap_2()
+                                .max_w_full()
+                                .w_full()
+                                .child(Avatar::new(user.avatar_uri.clone()))
+                                .child(Label::new(user.github_login.clone()).truncate()),
+                        )
+                        .child(
+                            ButtonLike::new("open_account")
+                                .size(ButtonSize::Medium)
+                                .child(
+                                    h_flex()
+                                        .ml_1()
+                                        .w_full()
+                                        .justify_between()
+                                        .child(Label::new("Open Account"))
+                                        .children(
+                                            KeyBinding::for_action_in(
+                                                &OpenAccount,
+                                                &self.focus_handle,
+                                                window,
+                                                cx,
+                                            )
+                                            .map(|kb| kb.size(rems_from_px(12.))),
+                                        ),
+                                )
+                                .on_click(|_, window, cx| {
+                                    window.dispatch_action(OpenAccount.boxed_clone(), cx);
+                                }),
+                        )
                         .into_any_element()
                 } else {
                     Button::new("sign_in", "Sign In")
@@ -444,6 +484,7 @@ impl Onboarding {
     }
 
     fn on_finish(_: &Finish, _: &mut Window, cx: &mut App) {
+        telemetry::event!("Welcome Skip Clicked");
         go_to_welcome_page(cx);
     }
 
@@ -453,13 +494,17 @@ impl Onboarding {
         window
             .spawn(cx, async move |cx| {
                 client
-                    .sign_in_with_optional_connect(true, &cx)
+                    .sign_in_with_optional_connect(true, cx)
                     .await
                     .notify_async_err(cx);
             })
             .detach();
     }
 
+    fn handle_open_account(_: &OpenAccount, _: &mut Window, cx: &mut App) {
+        cx.open_url(&zed_urls::account_url(cx))
+    }
+
     fn render_page(&mut self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
         let client = Client::global(cx);
 
@@ -495,14 +540,15 @@ impl Render for Onboarding {
             .bg(cx.theme().colors().editor_background)
             .on_action(Self::on_finish)
             .on_action(Self::handle_sign_in)
+            .on_action(Self::handle_open_account)
             .on_action(cx.listener(|this, _: &ActivateBasicsPage, _, cx| {
-                this.set_page(SelectedPage::Basics, cx);
+                this.set_page(SelectedPage::Basics, Some("action"), cx);
             }))
             .on_action(cx.listener(|this, _: &ActivateEditingPage, _, cx| {
-                this.set_page(SelectedPage::Editing, cx);
+                this.set_page(SelectedPage::Editing, Some("action"), cx);
             }))
             .on_action(cx.listener(|this, _: &ActivateAISetupPage, _, cx| {
-                this.set_page(SelectedPage::AiSetup, cx);
+                this.set_page(SelectedPage::AiSetup, Some("action"), cx);
             }))
             .on_action(cx.listener(|_, _: &menu::SelectNext, window, cx| {
                 window.focus_next();
@@ -515,6 +561,7 @@ impl Render for Onboarding {
             .child(
                 h_flex()
                     .max_w(rems_from_px(1100.))
+                    .max_h(rems_from_px(850.))
                     .size_full()
                     .m_auto()
                     .py_20()
@@ -524,12 +571,14 @@ impl Render for Onboarding {
                     .child(self.render_nav(window, cx))
                     .child(
                         v_flex()
+                            .id("page-content")
+                            .size_full()
                             .max_w_full()
                             .min_w_0()
                             .pl_12()
                             .border_l_1()
                             .border_color(cx.theme().colors().border_variant.opacity(0.5))
-                            .size_full()
+                            .overflow_y_scroll()
                             .child(self.render_page(window, cx)),
                     ),
             )
@@ -565,9 +614,13 @@ impl Item for Onboarding {
         _: &mut Window,
         cx: &mut Context<Self>,
     ) -> Option<Entity<Self>> {
-        self.workspace
-            .update(cx, |workspace, cx| Onboarding::new(workspace, cx))
-            .ok()
+        Some(cx.new(|cx| Onboarding {
+            workspace: self.workspace.clone(),
+            user_store: self.user_store.clone(),
+            selected_page: self.selected_page,
+            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)) {
@@ -690,7 +743,7 @@ pub async fn handle_import_vscode_settings(
                     "Failed to import settings. See log for details",
                     cx,
                     |this, _| {
-                        this.icon(ToastIcon::new(IconName::X).color(Color::Error))
+                        this.icon(ToastIcon::new(IconName::Close).color(Color::Error))
                             .action("Open Log", |window, cx| {
                                 window.dispatch_action(workspace::OpenLog.boxed_clone(), cx)
                             })
@@ -763,7 +816,7 @@ impl workspace::SerializableItem for Onboarding {
                     if let Some(page) = page {
                         zlog::info!("Onboarding page {page:?} loaded");
                         onboarding_page.update(cx, |onboarding_page, cx| {
-                            onboarding_page.set_page(page, cx);
+                            onboarding_page.set_page(page, None, cx);
                         })
                     }
                     onboarding_page

crates/onboarding/src/theme_preview.rs 🔗

@@ -1,6 +1,9 @@
 #![allow(unused, dead_code)]
 use gpui::{Hsla, Length};
-use std::sync::Arc;
+use std::{
+    cell::LazyCell,
+    sync::{Arc, LazyLock, OnceLock},
+};
 use theme::{Theme, ThemeColors, ThemeRegistry};
 use ui::{
     IntoElement, RenderOnce, component_prelude::Documented, prelude::*, utils::inner_corner_radius,
@@ -22,6 +25,15 @@ pub struct ThemePreviewTile {
     style: ThemePreviewStyle,
 }
 
+static CHILD_RADIUS: LazyLock<Pixels> = LazyLock::new(|| {
+    inner_corner_radius(
+        ThemePreviewTile::ROOT_RADIUS,
+        ThemePreviewTile::ROOT_BORDER,
+        ThemePreviewTile::ROOT_PADDING,
+        ThemePreviewTile::CHILD_BORDER,
+    )
+});
+
 impl ThemePreviewTile {
     pub const SKELETON_HEIGHT_DEFAULT: Pixels = px(2.);
     pub const SIDEBAR_SKELETON_ITEM_COUNT: usize = 8;
@@ -30,14 +42,6 @@ impl ThemePreviewTile {
     pub const ROOT_BORDER: Pixels = px(2.0);
     pub const ROOT_PADDING: Pixels = px(2.0);
     pub const CHILD_BORDER: Pixels = px(1.0);
-    pub const CHILD_RADIUS: std::cell::LazyCell<Pixels> = std::cell::LazyCell::new(|| {
-        inner_corner_radius(
-            Self::ROOT_RADIUS,
-            Self::ROOT_BORDER,
-            Self::ROOT_PADDING,
-            Self::CHILD_BORDER,
-        )
-    });
 
     pub fn new(theme: Arc<Theme>, seed: f32) -> Self {
         Self {
@@ -202,16 +206,16 @@ impl ThemePreviewTile {
                 sidebar_width,
                 skeleton_height.clone(),
             ))
-            .child(Self::render_pane(seed, theme, skeleton_height.clone()))
+            .child(Self::render_pane(seed, theme, skeleton_height))
     }
 
     fn render_borderless(seed: f32, theme: Arc<Theme>) -> impl IntoElement {
-        return Self::render_editor(
+        Self::render_editor(
             seed,
             theme,
             Self::SIDEBAR_WIDTH_DEFAULT,
             Self::SKELETON_HEIGHT_DEFAULT,
-        );
+        )
     }
 
     fn render_border(seed: f32, theme: Arc<Theme>) -> impl IntoElement {
@@ -222,7 +226,7 @@ impl ThemePreviewTile {
             .child(
                 div()
                     .size_full()
-                    .rounded(*Self::CHILD_RADIUS)
+                    .rounded(*CHILD_RADIUS)
                     .border(Self::CHILD_BORDER)
                     .border_color(theme.colors().border)
                     .child(Self::render_editor(
@@ -242,7 +246,7 @@ impl ThemePreviewTile {
     ) -> impl IntoElement {
         let sidebar_width = relative(0.20);
 
-        return div()
+        div()
             .size_full()
             .p(Self::ROOT_PADDING)
             .rounded(Self::ROOT_RADIUS)
@@ -250,13 +254,13 @@ impl ThemePreviewTile {
                 h_flex()
                     .size_full()
                     .relative()
-                    .rounded(*Self::CHILD_RADIUS)
+                    .rounded(*CHILD_RADIUS)
                     .border(Self::CHILD_BORDER)
                     .border_color(border_color)
                     .overflow_hidden()
                     .child(div().size_full().child(Self::render_editor(
                         seed,
-                        theme.clone(),
+                        theme,
                         sidebar_width,
                         Self::SKELETON_HEIGHT_DEFAULT,
                     )))
@@ -274,7 +278,7 @@ impl ThemePreviewTile {
                             )),
                     ),
             )
-            .into_any_element();
+            .into_any_element()
     }
 }
 
@@ -325,9 +329,9 @@ impl Component for ThemePreviewTile {
 
         let themes_to_preview = vec![
             one_dark.clone().ok(),
-            one_light.clone().ok(),
-            gruvbox_dark.clone().ok(),
-            gruvbox_light.clone().ok(),
+            one_light.ok(),
+            gruvbox_dark.ok(),
+            gruvbox_light.ok(),
         ]
         .into_iter()
         .flatten()
@@ -344,7 +348,7 @@ impl Component for ThemePreviewTile {
                             div()
                                 .w(px(240.))
                                 .h(px(180.))
-                                .child(ThemePreviewTile::new(one_dark.clone(), 0.42))
+                                .child(ThemePreviewTile::new(one_dark, 0.42))
                                 .into_any_element(),
                         )])]
                     } else {
@@ -358,13 +362,12 @@ impl Component for ThemePreviewTile {
                             .gap_4()
                             .children(
                                 themes_to_preview
-                                    .iter()
-                                    .enumerate()
-                                    .map(|(_, theme)| {
+                                    .into_iter()
+                                    .map(|theme| {
                                         div()
                                             .w(px(200.))
                                             .h(px(140.))
-                                            .child(ThemePreviewTile::new(theme.clone(), 0.42))
+                                            .child(ThemePreviewTile::new(theme, 0.42))
                                     })
                                     .collect::<Vec<_>>(),
                             )

crates/onboarding/src/welcome.rs 🔗

@@ -1,6 +1,6 @@
 use gpui::{
     Action, App, Context, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement,
-    NoAction, ParentElement, Render, Styled, Window, actions,
+    ParentElement, Render, Styled, Task, Window, actions,
 };
 use menu::{SelectNext, SelectPrevious};
 use ui::{ButtonLike, Divider, DividerColor, KeyBinding, Vector, VectorName, prelude::*};
@@ -37,9 +37,8 @@ const CONTENT: (Section<4>, Section<3>) = (
             },
             SectionEntry {
                 icon: IconName::CloudDownload,
-                title: "Clone a Repo",
-                // TODO: use proper action
-                action: &NoAction,
+                title: "Clone Repository",
+                action: &git::Clone,
             },
             SectionEntry {
                 icon: IconName::ListCollapse,
@@ -105,7 +104,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, window, cx)),
             )
     }
 }
@@ -353,3 +352,109 @@ impl Item for WelcomePage {
         f(*event)
     }
 }
+
+impl workspace::SerializableItem for WelcomePage {
+    fn serialized_item_kind() -> &'static str {
+        "WelcomePage"
+    }
+
+    fn cleanup(
+        workspace_id: workspace::WorkspaceId,
+        alive_items: Vec<workspace::ItemId>,
+        _window: &mut Window,
+        cx: &mut App,
+    ) -> Task<gpui::Result<()>> {
+        workspace::delete_unloaded_items(
+            alive_items,
+            workspace_id,
+            "welcome_pages",
+            &persistence::WELCOME_PAGES,
+            cx,
+        )
+    }
+
+    fn deserialize(
+        _project: Entity<project::Project>,
+        _workspace: gpui::WeakEntity<workspace::Workspace>,
+        workspace_id: workspace::WorkspaceId,
+        item_id: workspace::ItemId,
+        window: &mut Window,
+        cx: &mut App,
+    ) -> Task<gpui::Result<Entity<Self>>> {
+        if persistence::WELCOME_PAGES
+            .get_welcome_page(item_id, workspace_id)
+            .ok()
+            .is_some_and(|is_open| is_open)
+        {
+            window.spawn(cx, async move |cx| cx.update(WelcomePage::new))
+        } else {
+            Task::ready(Err(anyhow::anyhow!("No welcome page to deserialize")))
+        }
+    }
+
+    fn serialize(
+        &mut self,
+        workspace: &mut workspace::Workspace,
+        item_id: workspace::ItemId,
+        _closing: bool,
+        _window: &mut Window,
+        cx: &mut Context<Self>,
+    ) -> Option<Task<gpui::Result<()>>> {
+        let workspace_id = workspace.database_id()?;
+        Some(cx.background_spawn(async move {
+            persistence::WELCOME_PAGES
+                .save_welcome_page(item_id, workspace_id, true)
+                .await
+        }))
+    }
+
+    fn should_serialize(&self, event: &Self::Event) -> bool {
+        event == &ItemEvent::UpdateTab
+    }
+}
+
+mod persistence {
+    use db::{define_connection, query, sqlez_macros::sql};
+    use workspace::WorkspaceDb;
+
+    define_connection! {
+        pub static ref WELCOME_PAGES: WelcomePagesDb<WorkspaceDb> =
+            &[
+                sql!(
+                    CREATE TABLE welcome_pages (
+                        workspace_id INTEGER,
+                        item_id INTEGER UNIQUE,
+                        is_open INTEGER DEFAULT FALSE,
+
+                        PRIMARY KEY(workspace_id, item_id),
+                        FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
+                        ON DELETE CASCADE
+                    ) STRICT;
+                ),
+            ];
+    }
+
+    impl WelcomePagesDb {
+        query! {
+            pub async fn save_welcome_page(
+                item_id: workspace::ItemId,
+                workspace_id: workspace::WorkspaceId,
+                is_open: bool
+            ) -> Result<()> {
+                INSERT OR REPLACE INTO welcome_pages(item_id, workspace_id, is_open)
+                VALUES (?, ?, ?)
+            }
+        }
+
+        query! {
+            pub fn get_welcome_page(
+                item_id: workspace::ItemId,
+                workspace_id: workspace::WorkspaceId
+            ) -> Result<bool> {
+                SELECT is_open
+                FROM welcome_pages
+                WHERE item_id = ? AND workspace_id = ?
+            }
+        }
+    }
+}

crates/open_ai/Cargo.toml 🔗

@@ -20,6 +20,7 @@ anyhow.workspace = true
 futures.workspace = true
 http_client.workspace = true
 schemars = { workspace = true, optional = true }
+log.workspace = true
 serde.workspace = true
 serde_json.workspace = true
 strum.workspace = true

crates/open_ai/src/open_ai.rs 🔗

@@ -9,7 +9,7 @@ use strum::EnumIter;
 pub const OPEN_AI_API_URL: &str = "https://api.openai.com/v1";
 
 fn is_none_or_empty<T: AsRef<[U]>, U>(opt: &Option<T>) -> bool {
-    opt.as_ref().map_or(true, |v| v.as_ref().is_empty())
+    opt.as_ref().is_none_or(|v| v.as_ref().is_empty())
 }
 
 #[derive(Clone, Copy, Serialize, Deserialize, Debug, Eq, PartialEq)]
@@ -89,11 +89,13 @@ pub enum Model {
         max_tokens: u64,
         max_output_tokens: Option<u64>,
         max_completion_tokens: Option<u64>,
+        reasoning_effort: Option<ReasoningEffort>,
     },
 }
 
 impl Model {
     pub fn default_fast() -> Self {
+        // TODO: Replace with FiveMini since all other models are deprecated
         Self::FourPointOneMini
     }
 
@@ -206,6 +208,15 @@ impl Model {
         }
     }
 
+    pub fn reasoning_effort(&self) -> Option<ReasoningEffort> {
+        match self {
+            Self::Custom {
+                reasoning_effort, ..
+            } => reasoning_effort.to_owned(),
+            _ => None,
+        }
+    }
+
     /// Returns whether the given model supports the `parallel_tool_calls` parameter.
     ///
     /// If the model does not support the parameter, do not pass it up, or the API will return an error.
@@ -225,6 +236,13 @@ impl Model {
             Self::O1 | Self::O3 | Self::O3Mini | Self::O4Mini | Model::Custom { .. } => false,
         }
     }
+
+    /// Returns whether the given model supports the `prompt_cache_key` parameter.
+    ///
+    /// If the model does not support the parameter, do not pass it up.
+    pub fn supports_prompt_cache_key(&self) -> bool {
+        true
+    }
 }
 
 #[derive(Debug, Serialize, Deserialize)]
@@ -244,6 +262,10 @@ pub struct Request {
     pub parallel_tool_calls: Option<bool>,
     #[serde(default, skip_serializing_if = "Vec::is_empty")]
     pub tools: Vec<ToolDefinition>,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub prompt_cache_key: Option<String>,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub reasoning_effort: Option<ReasoningEffort>,
 }
 
 #[derive(Debug, Serialize, Deserialize)]
@@ -255,6 +277,16 @@ pub enum ToolChoice {
     Other(ToolDefinition),
 }
 
+#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
+#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
+#[serde(rename_all = "lowercase")]
+pub enum ReasoningEffort {
+    Minimal,
+    Low,
+    Medium,
+    High,
+}
+
 #[derive(Clone, Deserialize, Serialize, Debug)]
 #[serde(tag = "type", rename_all = "snake_case")]
 pub enum ToolDefinition {
@@ -400,11 +432,16 @@ pub struct ChoiceDelta {
     pub finish_reason: Option<String>,
 }
 
+#[derive(Serialize, Deserialize, Debug)]
+pub struct OpenAiError {
+    message: String,
+}
+
 #[derive(Serialize, Deserialize, Debug)]
 #[serde(untagged)]
 pub enum ResponseStreamResult {
     Ok(ResponseStreamEvent),
-    Err { error: String },
+    Err { error: OpenAiError },
 }
 
 #[derive(Serialize, Deserialize, Debug)]
@@ -443,9 +480,17 @@ pub async fn stream_completion(
                             match serde_json::from_str(line) {
                                 Ok(ResponseStreamResult::Ok(response)) => Some(Ok(response)),
                                 Ok(ResponseStreamResult::Err { error }) => {
+                                    Some(Err(anyhow!(error.message)))
+                                }
+                                Err(error) => {
+                                    log::error!(
+                                        "Failed to parse OpenAI response into ResponseStreamResult: `{}`\n\
+                                        Response: `{}`",
+                                        error,
+                                        line,
+                                    );
                                     Some(Err(anyhow!(error)))
                                 }
-                                Err(error) => Some(Err(anyhow!(error))),
                             }
                         }
                     }
@@ -462,11 +507,6 @@ pub async fn stream_completion(
             error: OpenAiError,
         }
 
-        #[derive(Deserialize)]
-        struct OpenAiError {
-            message: String,
-        }
-
         match serde_json::from_str::<OpenAiResponse>(&body) {
             Ok(response) if !response.error.message.is_empty() => Err(anyhow!(
                 "API request to {} failed: {}",

crates/open_router/src/open_router.rs 🔗

@@ -8,7 +8,7 @@ use std::convert::TryFrom;
 pub const OPEN_ROUTER_API_URL: &str = "https://openrouter.ai/api/v1";
 
 fn is_none_or_empty<T: AsRef<[U]>, U>(opt: &Option<T>) -> bool {
-    opt.as_ref().map_or(true, |v| v.as_ref().is_empty())
+    opt.as_ref().is_none_or(|v| v.as_ref().is_empty())
 }
 
 #[derive(Clone, Copy, Serialize, Deserialize, Debug, Eq, PartialEq)]
@@ -240,10 +240,10 @@ impl MessageContent {
 
 impl From<Vec<MessagePart>> for MessageContent {
     fn from(parts: Vec<MessagePart>) -> Self {
-        if parts.len() == 1 {
-            if let MessagePart::Text { text } = &parts[0] {
-                return Self::Plain(text.clone());
-            }
+        if parts.len() == 1
+            && let MessagePart::Text { text } = &parts[0]
+        {
+            return Self::Plain(text.clone());
         }
         Self::Multipart(parts)
     }

crates/outline_panel/src/outline_panel.rs 🔗

@@ -503,16 +503,16 @@ impl SearchData {
             && multi_buffer_snapshot
                 .chars_at(extended_context_left_border)
                 .last()
-                .map_or(false, |c| !c.is_whitespace());
+                .is_some_and(|c| !c.is_whitespace());
         let truncated_right = entire_context_text
             .chars()
             .last()
-            .map_or(true, |c| !c.is_whitespace())
+            .is_none_or(|c| !c.is_whitespace())
             && extended_context_right_border > context_right_border
             && multi_buffer_snapshot
                 .chars_at(extended_context_right_border)
                 .next()
-                .map_or(false, |c| !c.is_whitespace());
+                .is_some_and(|c| !c.is_whitespace());
         search_match_indices.iter_mut().for_each(|range| {
             range.start = multi_buffer_snapshot.clip_offset(
                 range.start.saturating_sub(left_whitespaces_offset),
@@ -733,7 +733,8 @@ impl OutlinePanel {
     ) -> Entity<Self> {
         let project = workspace.project().clone();
         let workspace_handle = cx.entity().downgrade();
-        let outline_panel = cx.new(|cx| {
+
+        cx.new(|cx| {
             let filter_editor = cx.new(|cx| {
                 let mut editor = Editor::single_line(window, cx);
                 editor.set_placeholder_text("Filter...", cx);
@@ -912,9 +913,7 @@ impl OutlinePanel {
                 outline_panel.replace_active_editor(item, editor, window, cx);
             }
             outline_panel
-        });
-
-        outline_panel
+        })
     }
 
     fn serialization_key(workspace: &Workspace) -> Option<String> {
@@ -1170,12 +1169,11 @@ impl OutlinePanel {
                     });
                 } else {
                     let mut offset = Point::default();
-                    if let Some(buffer_id) = scroll_to_buffer {
-                        if multi_buffer_snapshot.as_singleton().is_none()
-                            && !active_editor.read(cx).is_buffer_folded(buffer_id, cx)
-                        {
-                            offset.y = -(active_editor.read(cx).file_header_size() as f32);
-                        }
+                    if let Some(buffer_id) = scroll_to_buffer
+                        && multi_buffer_snapshot.as_singleton().is_none()
+                        && !active_editor.read(cx).is_buffer_folded(buffer_id, cx)
+                    {
+                        offset.y = -(active_editor.read(cx).file_header_size() as f32);
                     }
 
                     active_editor.update(cx, |editor, cx| {
@@ -1260,7 +1258,7 @@ impl OutlinePanel {
                                 dirs_worktree_id == worktree_id
                                     && dirs
                                         .last()
-                                        .map_or(false, |dir| dir.path.as_ref() == parent_path)
+                                        .is_some_and(|dir| dir.path.as_ref() == parent_path)
                             }
                             _ => false,
                         })
@@ -1454,9 +1452,7 @@ impl OutlinePanel {
         if self
             .unfolded_dirs
             .get(&directory_worktree)
-            .map_or(true, |unfolded_dirs| {
-                !unfolded_dirs.contains(&directory_entry.id)
-            })
+            .is_none_or(|unfolded_dirs| !unfolded_dirs.contains(&directory_entry.id))
         {
             return false;
         }
@@ -1606,16 +1602,14 @@ impl OutlinePanel {
             }
             PanelEntry::FoldedDirs(folded_dirs) => {
                 let mut folded = false;
-                if let Some(dir_entry) = folded_dirs.entries.last() {
-                    if self
+                if let Some(dir_entry) = folded_dirs.entries.last()
+                    && self
                         .collapsed_entries
                         .insert(CollapsedEntry::Dir(folded_dirs.worktree_id, dir_entry.id))
-                    {
-                        folded = true;
-                        buffers_to_fold.extend(
-                            self.buffers_inside_directory(folded_dirs.worktree_id, dir_entry),
-                        );
-                    }
+                {
+                    folded = true;
+                    buffers_to_fold
+                        .extend(self.buffers_inside_directory(folded_dirs.worktree_id, dir_entry));
                 }
                 folded
             }
@@ -2108,11 +2102,11 @@ impl OutlinePanel {
                                 dirs_to_expand.push(current_entry.id);
                             }
 
-                            if traversal.back_to_parent() {
-                                if let Some(parent_entry) = traversal.entry() {
-                                    current_entry = parent_entry.clone();
-                                    continue;
-                                }
+                            if traversal.back_to_parent()
+                                && let Some(parent_entry) = traversal.entry()
+                            {
+                                current_entry = parent_entry.clone();
+                                continue;
                             }
                             break;
                         }
@@ -2159,7 +2153,7 @@ impl OutlinePanel {
                 ExcerptOutlines::Invalidated(outlines) => Some(outlines),
                 ExcerptOutlines::NotFetched => None,
             })
-            .map_or(false, |outlines| !outlines.is_empty());
+            .is_some_and(|outlines| !outlines.is_empty());
         let is_expanded = !self
             .collapsed_entries
             .contains(&CollapsedEntry::Excerpt(excerpt.buffer_id, excerpt.id));
@@ -2475,17 +2469,17 @@ impl OutlinePanel {
         let search_data = match render_data.get() {
             Some(search_data) => search_data,
             None => {
-                if let ItemsDisplayMode::Search(search_state) = &mut self.mode {
-                    if let Some(multi_buffer_snapshot) = multi_buffer_snapshot {
-                        search_state
-                            .highlight_search_match_tx
-                            .try_send(HighlightArguments {
-                                multi_buffer_snapshot: multi_buffer_snapshot.clone(),
-                                match_range: match_range.clone(),
-                                search_data: Arc::clone(render_data),
-                            })
-                            .ok();
-                    }
+                if let ItemsDisplayMode::Search(search_state) = &mut self.mode
+                    && let Some(multi_buffer_snapshot) = multi_buffer_snapshot
+                {
+                    search_state
+                        .highlight_search_match_tx
+                        .try_send(HighlightArguments {
+                            multi_buffer_snapshot: multi_buffer_snapshot.clone(),
+                            match_range: match_range.clone(),
+                            search_data: Arc::clone(render_data),
+                        })
+                        .ok();
                 }
                 return None;
             }
@@ -2629,7 +2623,7 @@ impl OutlinePanel {
     }
 
     fn entry_name(&self, worktree_id: &WorktreeId, entry: &Entry, cx: &App) -> String {
-        let name = match self.project.read(cx).worktree_for_id(*worktree_id, cx) {
+        match self.project.read(cx).worktree_for_id(*worktree_id, cx) {
             Some(worktree) => {
                 let worktree = worktree.read(cx);
                 match worktree.snapshot().root_entry() {
@@ -2650,8 +2644,7 @@ impl OutlinePanel {
                 }
             }
             None => file_name(entry.path.as_ref()),
-        };
-        name
+        }
     }
 
     fn update_fs_entries(
@@ -2686,7 +2679,8 @@ impl OutlinePanel {
                 new_collapsed_entries = outline_panel.collapsed_entries.clone();
                 new_unfolded_dirs = outline_panel.unfolded_dirs.clone();
                 let multi_buffer_snapshot = active_multi_buffer.read(cx).snapshot(cx);
-                let buffer_excerpts = multi_buffer_snapshot.excerpts().fold(
+
+                multi_buffer_snapshot.excerpts().fold(
                     HashMap::default(),
                     |mut buffer_excerpts, (excerpt_id, buffer_snapshot, excerpt_range)| {
                         let buffer_id = buffer_snapshot.remote_id();
@@ -2733,8 +2727,7 @@ impl OutlinePanel {
                         );
                         buffer_excerpts
                     },
-                );
-                buffer_excerpts
+                )
             }) else {
                 return;
             };
@@ -2833,11 +2826,12 @@ impl OutlinePanel {
                                         let new_entry_added = entries_to_add
                                             .insert(current_entry.id, current_entry)
                                             .is_none();
-                                        if new_entry_added && traversal.back_to_parent() {
-                                            if let Some(parent_entry) = traversal.entry() {
-                                                current_entry = parent_entry.to_owned();
-                                                continue;
-                                            }
+                                        if new_entry_added
+                                            && traversal.back_to_parent()
+                                            && let Some(parent_entry) = traversal.entry()
+                                        {
+                                            current_entry = parent_entry.to_owned();
+                                            continue;
                                         }
                                         break;
                                     }
@@ -2878,18 +2872,17 @@ impl OutlinePanel {
                                 entries
                                     .into_iter()
                                     .filter_map(|entry| {
-                                        if auto_fold_dirs {
-                                            if let Some(parent) = entry.path.parent() {
-                                                let children = new_children_count
-                                                    .entry(worktree_id)
-                                                    .or_default()
-                                                    .entry(Arc::from(parent))
-                                                    .or_default();
-                                                if entry.is_dir() {
-                                                    children.dirs += 1;
-                                                } else {
-                                                    children.files += 1;
-                                                }
+                                        if auto_fold_dirs && let Some(parent) = entry.path.parent()
+                                        {
+                                            let children = new_children_count
+                                                .entry(worktree_id)
+                                                .or_default()
+                                                .entry(Arc::from(parent))
+                                                .or_default();
+                                            if entry.is_dir() {
+                                                children.dirs += 1;
+                                            } else {
+                                                children.files += 1;
                                             }
                                         }
 
@@ -2956,7 +2949,7 @@ impl OutlinePanel {
                                                         .map(|(parent_dir_id, _)| {
                                                             new_unfolded_dirs
                                                                 .get(&directory.worktree_id)
-                                                                .map_or(true, |unfolded_dirs| {
+                                                                .is_none_or(|unfolded_dirs| {
                                                                     unfolded_dirs
                                                                         .contains(parent_dir_id)
                                                                 })
@@ -3409,30 +3402,29 @@ impl OutlinePanel {
                                 {
                                     excerpt.outlines = ExcerptOutlines::Outlines(fetched_outlines);
 
-                                    if let Some(default_depth) = pending_default_depth {
-                                        if let ExcerptOutlines::Outlines(outlines) =
+                                    if let Some(default_depth) = pending_default_depth
+                                        && let ExcerptOutlines::Outlines(outlines) =
                                             &excerpt.outlines
-                                        {
-                                            outlines
-                                                .iter()
-                                                .filter(|outline| {
-                                                    (default_depth == 0
-                                                        || outline.depth >= default_depth)
-                                                        && outlines_with_children.contains(&(
-                                                            outline.range.clone(),
-                                                            outline.depth,
-                                                        ))
-                                                })
-                                                .for_each(|outline| {
-                                                    outline_panel.collapsed_entries.insert(
-                                                        CollapsedEntry::Outline(
-                                                            buffer_id,
-                                                            excerpt_id,
-                                                            outline.range.clone(),
-                                                        ),
-                                                    );
-                                                });
-                                        }
+                                    {
+                                        outlines
+                                            .iter()
+                                            .filter(|outline| {
+                                                (default_depth == 0
+                                                    || outline.depth >= default_depth)
+                                                    && outlines_with_children.contains(&(
+                                                        outline.range.clone(),
+                                                        outline.depth,
+                                                    ))
+                                            })
+                                            .for_each(|outline| {
+                                                outline_panel.collapsed_entries.insert(
+                                                    CollapsedEntry::Outline(
+                                                        buffer_id,
+                                                        excerpt_id,
+                                                        outline.range.clone(),
+                                                    ),
+                                                );
+                                            });
                                     }
 
                                     // Even if no outlines to check, we still need to update cached entries
@@ -3448,9 +3440,8 @@ impl OutlinePanel {
     }
 
     fn is_singleton_active(&self, cx: &App) -> bool {
-        self.active_editor().map_or(false, |active_editor| {
-            active_editor.read(cx).buffer().read(cx).is_singleton()
-        })
+        self.active_editor()
+            .is_some_and(|active_editor| active_editor.read(cx).buffer().read(cx).is_singleton())
     }
 
     fn invalidate_outlines(&mut self, ids: &[ExcerptId]) {
@@ -3611,10 +3602,9 @@ impl OutlinePanel {
                 .update_in(cx, |outline_panel, window, cx| {
                     outline_panel.cached_entries = new_cached_entries;
                     outline_panel.max_width_item_index = max_width_item_index;
-                    if outline_panel.selected_entry.is_invalidated()
-                        || matches!(outline_panel.selected_entry, SelectedEntry::None)
-                    {
-                        if let Some(new_selected_entry) =
+                    if (outline_panel.selected_entry.is_invalidated()
+                        || matches!(outline_panel.selected_entry, SelectedEntry::None))
+                        && let Some(new_selected_entry) =
                             outline_panel.active_editor().and_then(|active_editor| {
                                 outline_panel.location_for_editor_selection(
                                     &active_editor,
@@ -3622,9 +3612,8 @@ impl OutlinePanel {
                                     cx,
                                 )
                             })
-                        {
-                            outline_panel.select_entry(new_selected_entry, false, window, cx);
-                        }
+                    {
+                        outline_panel.select_entry(new_selected_entry, false, window, cx);
                     }
 
                     outline_panel.autoscroll(cx);
@@ -3670,7 +3659,7 @@ impl OutlinePanel {
                             let is_root = project
                                 .read(cx)
                                 .worktree_for_id(directory_entry.worktree_id, cx)
-                                .map_or(false, |worktree| {
+                                .is_some_and(|worktree| {
                                     worktree.read(cx).root_entry() == Some(&directory_entry.entry)
                                 });
                             let folded = auto_fold_dirs
@@ -3678,7 +3667,7 @@ impl OutlinePanel {
                                 && outline_panel
                                     .unfolded_dirs
                                     .get(&directory_entry.worktree_id)
-                                    .map_or(true, |unfolded_dirs| {
+                                    .is_none_or(|unfolded_dirs| {
                                         !unfolded_dirs.contains(&directory_entry.entry.id)
                                     });
                             let fs_depth = outline_panel
@@ -3758,7 +3747,7 @@ impl OutlinePanel {
                                                 .iter()
                                                 .rev()
                                                 .nth(folded_dirs.entries.len() + 1)
-                                                .map_or(true, |parent| parent.expanded);
+                                                .is_none_or(|parent| parent.expanded);
                                         if start_of_collapsed_dir_sequence
                                             || parent_expanded
                                             || query.is_some()
@@ -3818,7 +3807,7 @@ impl OutlinePanel {
                                             .iter()
                                             .all(|entry| entry.path != parent.path)
                                     })
-                                    .map_or(true, |parent| parent.expanded);
+                                    .is_none_or(|parent| parent.expanded);
                                 if !is_singleton && (parent_expanded || query.is_some()) {
                                     outline_panel.push_entry(
                                         &mut generation_state,
@@ -3843,7 +3832,7 @@ impl OutlinePanel {
                                             .iter()
                                             .all(|entry| entry.path != parent.path)
                                     })
-                                    .map_or(true, |parent| parent.expanded);
+                                    .is_none_or(|parent| parent.expanded);
                                 if !is_singleton && (parent_expanded || query.is_some()) {
                                     outline_panel.push_entry(
                                         &mut generation_state,
@@ -3921,19 +3910,19 @@ impl OutlinePanel {
                                 } else {
                                     None
                                 };
-                            if let Some((buffer_id, entry_excerpts)) = excerpts_to_consider {
-                                if !active_editor.read(cx).is_buffer_folded(buffer_id, cx) {
-                                    outline_panel.add_excerpt_entries(
-                                        &mut generation_state,
-                                        buffer_id,
-                                        entry_excerpts,
-                                        depth,
-                                        track_matches,
-                                        is_singleton,
-                                        query.as_deref(),
-                                        cx,
-                                    );
-                                }
+                            if let Some((buffer_id, entry_excerpts)) = excerpts_to_consider
+                                && !active_editor.read(cx).is_buffer_folded(buffer_id, cx)
+                            {
+                                outline_panel.add_excerpt_entries(
+                                    &mut generation_state,
+                                    buffer_id,
+                                    entry_excerpts,
+                                    depth,
+                                    track_matches,
+                                    is_singleton,
+                                    query.as_deref(),
+                                    cx,
+                                );
                             }
                         }
                     }
@@ -3964,7 +3953,7 @@ impl OutlinePanel {
                                 .iter()
                                 .all(|entry| entry.path != parent.path)
                         })
-                        .map_or(true, |parent| parent.expanded);
+                        .is_none_or(|parent| parent.expanded);
                     if parent_expanded || query.is_some() {
                         outline_panel.push_entry(
                             &mut generation_state,
@@ -4404,15 +4393,16 @@ impl OutlinePanel {
             })
             .filter(|(match_range, _)| {
                 let editor = active_editor.read(cx);
-                if let Some(buffer_id) = match_range.start.buffer_id {
-                    if editor.is_buffer_folded(buffer_id, cx) {
-                        return false;
-                    }
+                let snapshot = editor.buffer().read(cx).snapshot(cx);
+                if let Some(buffer_id) = snapshot.buffer_id_for_anchor(match_range.start)
+                    && editor.is_buffer_folded(buffer_id, cx)
+                {
+                    return false;
                 }
-                if let Some(buffer_id) = match_range.start.buffer_id {
-                    if editor.is_buffer_folded(buffer_id, cx) {
-                        return false;
-                    }
+                if let Some(buffer_id) = snapshot.buffer_id_for_anchor(match_range.end)
+                    && editor.is_buffer_folded(buffer_id, cx)
+                {
+                    return false;
                 }
                 true
             });
@@ -4444,7 +4434,7 @@ impl OutlinePanel {
     }
 
     fn should_replace_active_item(&self, new_active_item: &dyn ItemHandle) -> bool {
-        self.active_item().map_or(true, |active_item| {
+        self.active_item().is_none_or(|active_item| {
             !self.pinned && active_item.item_id() != new_active_item.item_id()
         })
     }
@@ -4456,16 +4446,14 @@ impl OutlinePanel {
         cx: &mut Context<Self>,
     ) {
         self.pinned = !self.pinned;
-        if !self.pinned {
-            if let Some((active_item, active_editor)) = self
+        if !self.pinned
+            && let Some((active_item, active_editor)) = self
                 .workspace
                 .upgrade()
                 .and_then(|workspace| workspace_active_editor(workspace.read(cx), cx))
-            {
-                if self.should_replace_active_item(active_item.as_ref()) {
-                    self.replace_active_editor(active_item, active_editor, window, cx);
-                }
-            }
+            && self.should_replace_active_item(active_item.as_ref())
+        {
+            self.replace_active_editor(active_item, active_editor, window, cx);
         }
 
         cx.notify();
@@ -4815,51 +4803,45 @@ impl OutlinePanel {
                 .when(show_indent_guides, |list| {
                     list.with_decoration(
                         ui::indent_guides(px(indent_size), IndentGuideColors::panel(cx))
-                            .with_compute_indents_fn(
-                                cx.entity().clone(),
-                                |outline_panel, range, _, _| {
-                                    let entries = outline_panel.cached_entries.get(range);
-                                    if let Some(entries) = entries {
-                                        entries.into_iter().map(|item| item.depth).collect()
-                                    } else {
-                                        smallvec::SmallVec::new()
-                                    }
-                                },
-                            )
-                            .with_render_fn(
-                                cx.entity().clone(),
-                                move |outline_panel, params, _, _| {
-                                    const LEFT_OFFSET: Pixels = px(14.);
-
-                                    let indent_size = params.indent_size;
-                                    let item_height = params.item_height;
-                                    let active_indent_guide_ix = find_active_indent_guide_ix(
-                                        outline_panel,
-                                        &params.indent_guides,
-                                    );
+                            .with_compute_indents_fn(cx.entity(), |outline_panel, range, _, _| {
+                                let entries = outline_panel.cached_entries.get(range);
+                                if let Some(entries) = entries {
+                                    entries.iter().map(|item| item.depth).collect()
+                                } else {
+                                    smallvec::SmallVec::new()
+                                }
+                            })
+                            .with_render_fn(cx.entity(), move |outline_panel, params, _, _| {
+                                const LEFT_OFFSET: Pixels = px(14.);
+
+                                let indent_size = params.indent_size;
+                                let item_height = params.item_height;
+                                let active_indent_guide_ix = find_active_indent_guide_ix(
+                                    outline_panel,
+                                    &params.indent_guides,
+                                );
 
-                                    params
-                                        .indent_guides
-                                        .into_iter()
-                                        .enumerate()
-                                        .map(|(ix, layout)| {
-                                            let bounds = Bounds::new(
-                                                point(
-                                                    layout.offset.x * indent_size + LEFT_OFFSET,
-                                                    layout.offset.y * item_height,
-                                                ),
-                                                size(px(1.), layout.length * item_height),
-                                            );
-                                            ui::RenderedIndentGuide {
-                                                bounds,
-                                                layout,
-                                                is_active: active_indent_guide_ix == Some(ix),
-                                                hitbox: None,
-                                            }
-                                        })
-                                        .collect()
-                                },
-                            ),
+                                params
+                                    .indent_guides
+                                    .into_iter()
+                                    .enumerate()
+                                    .map(|(ix, layout)| {
+                                        let bounds = Bounds::new(
+                                            point(
+                                                layout.offset.x * indent_size + LEFT_OFFSET,
+                                                layout.offset.y * item_height,
+                                            ),
+                                            size(px(1.), layout.length * item_height),
+                                        );
+                                        ui::RenderedIndentGuide {
+                                            bounds,
+                                            layout,
+                                            is_active: active_indent_guide_ix == Some(ix),
+                                            hitbox: None,
+                                        }
+                                    })
+                                    .collect()
+                            }),
                     )
                 })
             };
@@ -5073,24 +5055,23 @@ impl Panel for OutlinePanel {
                     let old_active = outline_panel.active;
                     outline_panel.active = active;
                     if old_active != active {
-                        if active {
-                            if let Some((active_item, active_editor)) =
+                        if active
+                            && let Some((active_item, active_editor)) =
                                 outline_panel.workspace.upgrade().and_then(|workspace| {
                                     workspace_active_editor(workspace.read(cx), cx)
                                 })
-                            {
-                                if outline_panel.should_replace_active_item(active_item.as_ref()) {
-                                    outline_panel.replace_active_editor(
-                                        active_item,
-                                        active_editor,
-                                        window,
-                                        cx,
-                                    );
-                                } else {
-                                    outline_panel.update_fs_entries(active_editor, None, window, cx)
-                                }
-                                return;
+                        {
+                            if outline_panel.should_replace_active_item(active_item.as_ref()) {
+                                outline_panel.replace_active_editor(
+                                    active_item,
+                                    active_editor,
+                                    window,
+                                    cx,
+                                );
+                            } else {
+                                outline_panel.update_fs_entries(active_editor, None, window, cx)
                             }
+                            return;
                         }
 
                         if !outline_panel.pinned {
@@ -5111,7 +5092,7 @@ impl Panel for OutlinePanel {
 
 impl Focusable for OutlinePanel {
     fn focus_handle(&self, cx: &App) -> FocusHandle {
-        self.filter_editor.focus_handle(cx).clone()
+        self.filter_editor.focus_handle(cx)
     }
 }
 
@@ -5325,8 +5306,8 @@ fn subscribe_for_editor_events(
                             })
                             .copied(),
                     );
-                    if !ignore_selections_change {
-                        if let Some(entry_to_select) = latest_unfolded_buffer_id
+                    if !ignore_selections_change
+                        && let Some(entry_to_select) = latest_unfolded_buffer_id
                             .or(latest_folded_buffer_id)
                             .and_then(|toggled_buffer_id| {
                                 outline_panel.fs_entries.iter().find_map(
@@ -5350,16 +5331,15 @@ fn subscribe_for_editor_events(
                                 )
                             })
                             .map(PanelEntry::Fs)
-                        {
-                            outline_panel.select_entry(entry_to_select, true, window, cx);
-                        }
+                    {
+                        outline_panel.select_entry(entry_to_select, true, window, cx);
                     }
 
                     outline_panel.update_fs_entries(editor.clone(), debounce, window, cx);
                 }
                 EditorEvent::Reparsed(buffer_id) => {
                     if let Some(excerpts) = outline_panel.excerpts.get_mut(buffer_id) {
-                        for (_, excerpt) in excerpts {
+                        for excerpt in excerpts.values_mut() {
                             excerpt.invalidate_outlines();
                         }
                     }
@@ -5504,7 +5484,7 @@ mod tests {
             assert_eq!(
                 display_entries(
                     &project,
-                    &snapshot(&outline_panel, cx),
+                    &snapshot(outline_panel, cx),
                     &outline_panel.cached_entries,
                     outline_panel.selected_entry(),
                     cx,
@@ -5520,7 +5500,7 @@ mod tests {
             assert_eq!(
                 display_entries(
                     &project,
-                    &snapshot(&outline_panel, cx),
+                    &snapshot(outline_panel, cx),
                     &outline_panel.cached_entries,
                     outline_panel.selected_entry(),
                     cx,
@@ -5538,7 +5518,7 @@ mod tests {
             assert_eq!(
                 display_entries(
                     &project,
-                    &snapshot(&outline_panel, cx),
+                    &snapshot(outline_panel, cx),
                     &outline_panel.cached_entries,
                     outline_panel.selected_entry(),
                     cx,
@@ -5575,7 +5555,7 @@ mod tests {
             assert_eq!(
                 display_entries(
                     &project,
-                    &snapshot(&outline_panel, cx),
+                    &snapshot(outline_panel, cx),
                     &outline_panel.cached_entries,
                     outline_panel.selected_entry(),
                     cx,
@@ -5589,7 +5569,7 @@ mod tests {
             assert_eq!(
                 display_entries(
                     &project,
-                    &snapshot(&outline_panel, cx),
+                    &snapshot(outline_panel, cx),
                     &outline_panel.cached_entries,
                     outline_panel.selected_entry(),
                     cx,
@@ -5608,7 +5588,7 @@ mod tests {
             assert_eq!(
                 display_entries(
                     &project,
-                    &snapshot(&outline_panel, cx),
+                    &snapshot(outline_panel, cx),
                     &outline_panel.cached_entries,
                     outline_panel.selected_entry(),
                     cx,
@@ -5636,7 +5616,7 @@ mod tests {
             assert_eq!(
                 display_entries(
                     &project,
-                    &snapshot(&outline_panel, cx),
+                    &snapshot(outline_panel, cx),
                     &outline_panel.cached_entries,
                     outline_panel.selected_entry(),
                     cx,
@@ -5724,7 +5704,7 @@ mod tests {
             assert_eq!(
                 display_entries(
                     &project,
-                    &snapshot(&outline_panel, cx),
+                    &snapshot(outline_panel, cx),
                     &outline_panel.cached_entries,
                     None,
                     cx,
@@ -5747,7 +5727,7 @@ mod tests {
             assert_eq!(
                 display_entries(
                     &project,
-                    &snapshot(&outline_panel, cx),
+                    &snapshot(outline_panel, cx),
                     &outline_panel.cached_entries,
                     None,
                     cx,
@@ -5773,7 +5753,7 @@ mod tests {
             assert_eq!(
                 display_entries(
                     &project,
-                    &snapshot(&outline_panel, cx),
+                    &snapshot(outline_panel, cx),
                     &outline_panel.cached_entries,
                     None,
                     cx,
@@ -5879,7 +5859,7 @@ mod tests {
             assert_eq!(
                 display_entries(
                     &project,
-                    &snapshot(&outline_panel, cx),
+                    &snapshot(outline_panel, cx),
                     &outline_panel.cached_entries,
                     outline_panel.selected_entry(),
                     cx,
@@ -5902,7 +5882,7 @@ mod tests {
             assert_eq!(
                 display_entries(
                     &project,
-                    &snapshot(&outline_panel, cx),
+                    &snapshot(outline_panel, cx),
                     &outline_panel.cached_entries,
                     outline_panel.selected_entry(),
                     cx,
@@ -5939,7 +5919,7 @@ mod tests {
             assert_eq!(
                 display_entries(
                     &project,
-                    &snapshot(&outline_panel, cx),
+                    &snapshot(outline_panel, cx),
                     &outline_panel.cached_entries,
                     outline_panel.selected_entry(),
                     cx,
@@ -5976,7 +5956,7 @@ mod tests {
             assert_eq!(
                 display_entries(
                     &project,
-                    &snapshot(&outline_panel, cx),
+                    &snapshot(outline_panel, cx),
                     &outline_panel.cached_entries,
                     outline_panel.selected_entry(),
                     cx,
@@ -6079,7 +6059,7 @@ mod tests {
             assert_eq!(
                 display_entries(
                     &project,
-                    &snapshot(&outline_panel, cx),
+                    &snapshot(outline_panel, cx),
                     &outline_panel.cached_entries,
                     outline_panel.selected_entry(),
                     cx,
@@ -6105,7 +6085,7 @@ mod tests {
             assert_eq!(
                 display_entries(
                     &project,
-                    &snapshot(&outline_panel, cx),
+                    &snapshot(outline_panel, cx),
                     &outline_panel.cached_entries,
                     outline_panel.selected_entry(),
                     cx,
@@ -6129,7 +6109,7 @@ mod tests {
             assert_eq!(
                 display_entries(
                     &project,
-                    &snapshot(&outline_panel, cx),
+                    &snapshot(outline_panel, cx),
                     &outline_panel.cached_entries,
                     outline_panel.selected_entry(),
                     cx,
@@ -6150,7 +6130,7 @@ mod tests {
             assert_eq!(
                 display_entries(
                     &project,
-                    &snapshot(&outline_panel, cx),
+                    &snapshot(outline_panel, cx),
                     &outline_panel.cached_entries,
                     outline_panel.selected_entry(),
                     cx,
@@ -6238,7 +6218,7 @@ struct OutlineEntryExcerpt {
             assert_eq!(
                 display_entries(
                     &project,
-                    &snapshot(&outline_panel, cx),
+                    &snapshot(outline_panel, cx),
                     &outline_panel.cached_entries,
                     outline_panel.selected_entry(),
                     cx,
@@ -6265,7 +6245,7 @@ outline: struct OutlineEntryExcerpt
             assert_eq!(
                 display_entries(
                     &project,
-                    &snapshot(&outline_panel, cx),
+                    &snapshot(outline_panel, cx),
                     &outline_panel.cached_entries,
                     outline_panel.selected_entry(),
                     cx,
@@ -6292,7 +6272,7 @@ outline: struct OutlineEntryExcerpt  <==== selected
             assert_eq!(
                 display_entries(
                     &project,
-                    &snapshot(&outline_panel, cx),
+                    &snapshot(outline_panel, cx),
                     &outline_panel.cached_entries,
                     outline_panel.selected_entry(),
                     cx,
@@ -6319,7 +6299,7 @@ outline: struct OutlineEntryExcerpt
             assert_eq!(
                 display_entries(
                     &project,
-                    &snapshot(&outline_panel, cx),
+                    &snapshot(outline_panel, cx),
                     &outline_panel.cached_entries,
                     outline_panel.selected_entry(),
                     cx,
@@ -6346,7 +6326,7 @@ outline: struct OutlineEntryExcerpt
             assert_eq!(
                 display_entries(
                     &project,
-                    &snapshot(&outline_panel, cx),
+                    &snapshot(outline_panel, cx),
                     &outline_panel.cached_entries,
                     outline_panel.selected_entry(),
                     cx,
@@ -6373,7 +6353,7 @@ outline: struct OutlineEntryExcerpt
             assert_eq!(
                 display_entries(
                     &project,
-                    &snapshot(&outline_panel, cx),
+                    &snapshot(outline_panel, cx),
                     &outline_panel.cached_entries,
                     outline_panel.selected_entry(),
                     cx,
@@ -6400,7 +6380,7 @@ outline: struct OutlineEntryExcerpt  <==== selected
             assert_eq!(
                 display_entries(
                     &project,
-                    &snapshot(&outline_panel, cx),
+                    &snapshot(outline_panel, cx),
                     &outline_panel.cached_entries,
                     outline_panel.selected_entry(),
                     cx,
@@ -6427,7 +6407,7 @@ outline: struct OutlineEntryExcerpt
             assert_eq!(
                 display_entries(
                     &project,
-                    &snapshot(&outline_panel, cx),
+                    &snapshot(outline_panel, cx),
                     &outline_panel.cached_entries,
                     outline_panel.selected_entry(),
                     cx,
@@ -6454,7 +6434,7 @@ outline: struct OutlineEntryExcerpt
             assert_eq!(
                 display_entries(
                     &project,
-                    &snapshot(&outline_panel, cx),
+                    &snapshot(outline_panel, cx),
                     &outline_panel.cached_entries,
                     outline_panel.selected_entry(),
                     cx,
@@ -6481,7 +6461,7 @@ outline: struct OutlineEntryExcerpt
             assert_eq!(
                 display_entries(
                     &project,
-                    &snapshot(&outline_panel, cx),
+                    &snapshot(outline_panel, cx),
                     &outline_panel.cached_entries,
                     outline_panel.selected_entry(),
                     cx,
@@ -6508,7 +6488,7 @@ outline: struct OutlineEntryExcerpt  <==== selected
             assert_eq!(
                 display_entries(
                     &project,
-                    &snapshot(&outline_panel, cx),
+                    &snapshot(outline_panel, cx),
                     &outline_panel.cached_entries,
                     outline_panel.selected_entry(),
                     cx,
@@ -6614,7 +6594,7 @@ outline: struct OutlineEntryExcerpt
             assert_eq!(
                 display_entries(
                     &project,
-                    &snapshot(&outline_panel, cx),
+                    &snapshot(outline_panel, cx),
                     &outline_panel.cached_entries,
                     outline_panel.selected_entry(),
                     cx,
@@ -6651,7 +6631,7 @@ outline: struct OutlineEntryExcerpt
             assert_eq!(
                 display_entries(
                     &project,
-                    &snapshot(&outline_panel, cx),
+                    &snapshot(outline_panel, cx),
                     &outline_panel.cached_entries,
                     outline_panel.selected_entry(),
                     cx,
@@ -6679,7 +6659,7 @@ outline: struct OutlineEntryExcerpt
             assert_eq!(
                 display_entries(
                     &project,
-                    &snapshot(&outline_panel, cx),
+                    &snapshot(outline_panel, cx),
                     &outline_panel.cached_entries,
                     outline_panel.selected_entry(),
                     cx,
@@ -6711,7 +6691,7 @@ outline: struct OutlineEntryExcerpt
             assert_eq!(
                 display_entries(
                     &project,
-                    &snapshot(&outline_panel, cx),
+                    &snapshot(outline_panel, cx),
                     &outline_panel.cached_entries,
                     outline_panel.selected_entry(),
                     cx,
@@ -6742,7 +6722,7 @@ outline: struct OutlineEntryExcerpt
             assert_eq!(
                 display_entries(
                     &project,
-                    &snapshot(&outline_panel, cx),
+                    &snapshot(outline_panel, cx),
                     &outline_panel.cached_entries,
                     outline_panel.selected_entry(),
                     cx,
@@ -6870,7 +6850,7 @@ outline: struct OutlineEntryExcerpt
                             .render_data
                             .get_or_init(|| SearchData::new(
                                 &search_entry.match_range,
-                                &multi_buffer_snapshot
+                                multi_buffer_snapshot
                             ))
                             .context_text
                     )
@@ -7261,7 +7241,7 @@ outline: struct OutlineEntryExcerpt
             assert_eq!(
                 display_entries(
                     &project,
-                    &snapshot(&outline_panel, cx),
+                    &snapshot(outline_panel, cx),
                     &outline_panel.cached_entries,
                     outline_panel.selected_entry(),
                     cx,
@@ -7320,7 +7300,7 @@ outline: fn main()"
             assert_eq!(
                 display_entries(
                     &project,
-                    &snapshot(&outline_panel, cx),
+                    &snapshot(outline_panel, cx),
                     &outline_panel.cached_entries,
                     outline_panel.selected_entry(),
                     cx,
@@ -7344,7 +7324,7 @@ outline: fn main()"
             assert_eq!(
                 display_entries(
                     &project,
-                    &snapshot(&outline_panel, cx),
+                    &snapshot(outline_panel, cx),
                     &outline_panel.cached_entries,
                     outline_panel.selected_entry(),
                     cx,
@@ -7409,7 +7389,7 @@ outline: fn main()"
             assert_eq!(
                 display_entries(
                     &project,
-                    &snapshot(&outline_panel, cx),
+                    &snapshot(outline_panel, cx),
                     &outline_panel.cached_entries,
                     outline_panel.selected_entry(),
                     cx,

crates/panel/src/panel.rs 🔗

@@ -52,7 +52,7 @@ impl RenderOnce for PanelTab {
 
 pub fn panel_button(label: impl Into<SharedString>) -> ui::Button {
     let label = label.into();
-    let id = ElementId::Name(label.clone().to_lowercase().replace(' ', "_").into());
+    let id = ElementId::Name(label.to_lowercase().replace(' ', "_").into());
     ui::Button::new(id, label)
         .label_size(ui::LabelSize::Small)
         .icon_size(ui::IconSize::Small)

crates/paths/src/paths.rs 🔗

@@ -41,7 +41,7 @@ pub fn remote_server_dir_relative() -> &'static Path {
 /// # Arguments
 ///
 /// * `dir` - The path to use as the custom data directory. This will be used as the base
-///           directory for all user data, including databases, extensions, and logs.
+///   directory for all user data, including databases, extensions, and logs.
 ///
 /// # Returns
 ///

crates/picker/src/popover_menu.rs 🔗

@@ -85,7 +85,7 @@ where
             .menu(move |_window, _cx| Some(picker.clone()))
             .trigger_with_tooltip(self.trigger, self.tooltip)
             .anchor(self.anchor)
-            .when_some(self.handle.clone(), |menu, handle| menu.with_handle(handle))
+            .when_some(self.handle, |menu, handle| menu.with_handle(handle))
             .offset(gpui::Point {
                 x: px(0.0),
                 y: px(-2.0),

crates/prettier/src/prettier.rs 🔗

@@ -119,7 +119,7 @@ impl Prettier {
                                             None
                                         }
                                     }).any(|workspace_definition| {
-                                        workspace_definition == subproject_path.to_string_lossy() || PathMatcher::new(&[workspace_definition]).ok().map_or(false, |path_matcher| path_matcher.is_match(subproject_path))
+                                        workspace_definition == subproject_path.to_string_lossy() || PathMatcher::new(&[workspace_definition]).ok().is_some_and(|path_matcher| path_matcher.is_match(subproject_path))
                                     }) {
                                         anyhow::ensure!(has_prettier_in_node_modules(fs, &path_to_check).await?, "Path {path_to_check:?} is the workspace root for project in {closest_package_json_path:?}, but it has no prettier installed");
                                         log::info!("Found prettier path {path_to_check:?} in the workspace root for project in {closest_package_json_path:?}");
@@ -185,11 +185,11 @@ impl Prettier {
                     .metadata(&ignore_path)
                     .await
                     .with_context(|| format!("fetching metadata for {ignore_path:?}"))?
+                    && !metadata.is_dir
+                    && !metadata.is_symlink
                 {
-                    if !metadata.is_dir && !metadata.is_symlink {
-                        log::info!("Found prettier ignore at {ignore_path:?}");
-                        return Ok(ControlFlow::Continue(Some(path_to_check)));
-                    }
+                    log::info!("Found prettier ignore at {ignore_path:?}");
+                    return Ok(ControlFlow::Continue(Some(path_to_check)));
                 }
                 match &closest_package_json_path {
                     None => closest_package_json_path = Some(path_to_check.clone()),
@@ -217,19 +217,19 @@ impl Prettier {
                                     workspace_definition == subproject_path.to_string_lossy()
                                         || PathMatcher::new(&[workspace_definition])
                                             .ok()
-                                            .map_or(false, |path_matcher| {
+                                            .is_some_and(|path_matcher| {
                                                 path_matcher.is_match(subproject_path)
                                             })
                                 })
                             {
                                 let workspace_ignore = path_to_check.join(".prettierignore");
-                                if let Some(metadata) = fs.metadata(&workspace_ignore).await? {
-                                    if !metadata.is_dir {
-                                        log::info!(
-                                            "Found prettier ignore at workspace root {workspace_ignore:?}"
-                                        );
-                                        return Ok(ControlFlow::Continue(Some(path_to_check)));
-                                    }
+                                if let Some(metadata) = fs.metadata(&workspace_ignore).await?
+                                    && !metadata.is_dir
+                                {
+                                    log::info!(
+                                        "Found prettier ignore at workspace root {workspace_ignore:?}"
+                                    );
+                                    return Ok(ControlFlow::Continue(Some(path_to_check)));
                                 }
                             }
                         }
@@ -549,18 +549,16 @@ async fn read_package_json(
         .metadata(&possible_package_json)
         .await
         .with_context(|| format!("fetching metadata for package json {possible_package_json:?}"))?
+        && !package_json_metadata.is_dir
+        && !package_json_metadata.is_symlink
     {
-        if !package_json_metadata.is_dir && !package_json_metadata.is_symlink {
-            let package_json_contents = fs
-                .load(&possible_package_json)
-                .await
-                .with_context(|| format!("reading {possible_package_json:?} file contents"))?;
-            return serde_json::from_str::<HashMap<String, serde_json::Value>>(
-                &package_json_contents,
-            )
+        let package_json_contents = fs
+            .load(&possible_package_json)
+            .await
+            .with_context(|| format!("reading {possible_package_json:?} file contents"))?;
+        return serde_json::from_str::<HashMap<String, serde_json::Value>>(&package_json_contents)
             .map(Some)
             .with_context(|| format!("parsing {possible_package_json:?} file contents"));
-        }
     }
     Ok(None)
 }

crates/project/src/buffer_store.rs 🔗

@@ -88,9 +88,18 @@ pub enum BufferStoreEvent {
     },
 }
 
-#[derive(Default, Debug)]
+#[derive(Default, Debug, Clone)]
 pub struct ProjectTransaction(pub HashMap<Entity<Buffer>, language::Transaction>);
 
+impl PartialEq for ProjectTransaction {
+    fn eq(&self, other: &Self) -> bool {
+        self.0.len() == other.0.len()
+            && self.0.iter().all(|(buffer, transaction)| {
+                other.0.get(buffer).is_some_and(|t| t.id == transaction.id)
+            })
+    }
+}
+
 impl EventEmitter<BufferStoreEvent> for BufferStore {}
 
 impl RemoteBufferStore {
@@ -168,7 +177,7 @@ impl RemoteBufferStore {
                             .with_context(|| {
                                 format!("no worktree found for id {}", file.worktree_id)
                             })?;
-                        buffer_file = Some(Arc::new(File::from_proto(file, worktree.clone(), cx)?)
+                        buffer_file = Some(Arc::new(File::from_proto(file, worktree, cx)?)
                             as Arc<dyn language::File>);
                     }
                     Buffer::from_proto(replica_id, capability, state, buffer_file)
@@ -234,7 +243,7 @@ impl RemoteBufferStore {
                 }
             }
         }
-        return Ok(None);
+        Ok(None)
     }
 
     pub fn incomplete_buffer_ids(&self) -> Vec<BufferId> {
@@ -413,13 +422,10 @@ impl LocalBufferStore {
         cx: &mut Context<BufferStore>,
     ) {
         cx.subscribe(worktree, |this, worktree, event, cx| {
-            if worktree.read(cx).is_local() {
-                match event {
-                    worktree::Event::UpdatedEntries(changes) => {
-                        Self::local_worktree_entries_changed(this, &worktree, changes, cx);
-                    }
-                    _ => {}
-                }
+            if worktree.read(cx).is_local()
+                && let worktree::Event::UpdatedEntries(changes) = event
+            {
+                Self::local_worktree_entries_changed(this, &worktree, changes, cx);
             }
         })
         .detach();
@@ -594,7 +600,7 @@ impl LocalBufferStore {
         else {
             return Task::ready(Err(anyhow!("no such worktree")));
         };
-        self.save_local_buffer(buffer, worktree, path.path.clone(), true, cx)
+        self.save_local_buffer(buffer, worktree, path.path, true, cx)
     }
 
     fn open_buffer(
@@ -848,7 +854,7 @@ impl BufferStore {
     ) -> Task<Result<()>> {
         match &mut self.state {
             BufferStoreState::Local(this) => this.save_buffer(buffer, cx),
-            BufferStoreState::Remote(this) => this.save_remote_buffer(buffer.clone(), None, cx),
+            BufferStoreState::Remote(this) => this.save_remote_buffer(buffer, None, cx),
         }
     }
 
@@ -947,10 +953,9 @@ impl BufferStore {
     }
 
     pub fn get_by_path(&self, path: &ProjectPath) -> Option<Entity<Buffer>> {
-        self.path_to_buffer_id.get(path).and_then(|buffer_id| {
-            let buffer = self.get(*buffer_id);
-            buffer
-        })
+        self.path_to_buffer_id
+            .get(path)
+            .and_then(|buffer_id| self.get(*buffer_id))
     }
 
     pub fn get(&self, buffer_id: BufferId) -> Option<Entity<Buffer>> {
@@ -1094,10 +1099,10 @@ impl BufferStore {
                         .collect::<Vec<_>>()
                 })?;
                 for buffer_task in buffers {
-                    if let Some(buffer) = buffer_task.await.log_err() {
-                        if tx.send(buffer).await.is_err() {
-                            return anyhow::Ok(());
-                        }
+                    if let Some(buffer) = buffer_task.await.log_err()
+                        && tx.send(buffer).await.is_err()
+                    {
+                        return anyhow::Ok(());
                     }
                 }
             }
@@ -1142,7 +1147,7 @@ impl BufferStore {
         envelope: TypedEnvelope<proto::UpdateBuffer>,
         mut cx: AsyncApp,
     ) -> Result<proto::Ack> {
-        let payload = envelope.payload.clone();
+        let payload = envelope.payload;
         let buffer_id = BufferId::new(payload.buffer_id)?;
         let ops = payload
             .operations
@@ -1173,11 +1178,11 @@ impl BufferStore {
         buffer_id: BufferId,
         handle: OpenLspBufferHandle,
     ) {
-        if let Some(shared_buffers) = self.shared_buffers.get_mut(&peer_id) {
-            if let Some(buffer) = shared_buffers.get_mut(&buffer_id) {
-                buffer.lsp_handle = Some(handle);
-                return;
-            }
+        if let Some(shared_buffers) = self.shared_buffers.get_mut(&peer_id)
+            && let Some(buffer) = shared_buffers.get_mut(&buffer_id)
+        {
+            buffer.lsp_handle = Some(handle);
+            return;
         }
         debug_panic!("tried to register shared lsp handle, but buffer was not shared")
     }
@@ -1313,10 +1318,7 @@ impl BufferStore {
                     let new_path = file.path.clone();
 
                     buffer.file_updated(Arc::new(file), cx);
-                    if old_file
-                        .as_ref()
-                        .map_or(true, |old| *old.path() != new_path)
-                    {
+                    if old_file.as_ref().is_none_or(|old| *old.path() != new_path) {
                         Some(old_file)
                     } else {
                         None
@@ -1345,7 +1347,7 @@ impl BufferStore {
         mut cx: AsyncApp,
     ) -> Result<proto::BufferSaved> {
         let buffer_id = BufferId::new(envelope.payload.buffer_id)?;
-        let (buffer, project_id) = this.read_with(&mut cx, |this, _| {
+        let (buffer, project_id) = this.read_with(&cx, |this, _| {
             anyhow::Ok((
                 this.get_existing(buffer_id)?,
                 this.downstream_client
@@ -1359,7 +1361,7 @@ impl BufferStore {
                 buffer.wait_for_version(deserialize_version(&envelope.payload.version))
             })?
             .await?;
-        let buffer_id = buffer.read_with(&mut cx, |buffer, _| buffer.remote_id())?;
+        let buffer_id = buffer.read_with(&cx, |buffer, _| buffer.remote_id())?;
 
         if let Some(new_path) = envelope.payload.new_path {
             let new_path = ProjectPath::from_proto(new_path);
@@ -1372,7 +1374,7 @@ impl BufferStore {
                 .await?;
         }
 
-        buffer.read_with(&mut cx, |buffer, _| proto::BufferSaved {
+        buffer.read_with(&cx, |buffer, _| proto::BufferSaved {
             project_id,
             buffer_id: buffer_id.into(),
             version: serialize_version(buffer.saved_version()),
@@ -1388,14 +1390,14 @@ impl BufferStore {
         let peer_id = envelope.sender_id;
         let buffer_id = BufferId::new(envelope.payload.buffer_id)?;
         this.update(&mut cx, |this, cx| {
-            if let Some(shared) = this.shared_buffers.get_mut(&peer_id) {
-                if shared.remove(&buffer_id).is_some() {
-                    cx.emit(BufferStoreEvent::SharedBufferClosed(peer_id, buffer_id));
-                    if shared.is_empty() {
-                        this.shared_buffers.remove(&peer_id);
-                    }
-                    return;
+            if let Some(shared) = this.shared_buffers.get_mut(&peer_id)
+                && shared.remove(&buffer_id).is_some()
+            {
+                cx.emit(BufferStoreEvent::SharedBufferClosed(peer_id, buffer_id));
+                if shared.is_empty() {
+                    this.shared_buffers.remove(&peer_id);
                 }
+                return;
             }
             debug_panic!(
                 "peer_id {} closed buffer_id {} which was either not open or already closed",